使用 Jupyter Notebooks 教授 Python

借助 Jupyter、PyHamcrest 和一点胶带式的测试工具,您可以教授任何适用于单元测试的 Python 主题。
59 位读者喜欢这篇文章。
Person reading a book and digital copy

Ruby 社区的某些方面一直给我留下深刻印象。两个例子是对测试的承诺和强调入门的容易程度。最好的例子是 Ruby Koans,您可以通过修复测试来学习 Ruby。

有了我们为 Python 准备的出色工具,我们应该能够做得更好。我们可以。使用 Jupyter NotebookPyHamcrest 和一点点类似胶带的代码,我们可以制作一个教程,其中包含教学、可工作的代码和需要修复的代码。

首先,是一些胶带代码。通常,您使用一些不错的命令行测试运行器(如 pytestvirtue)进行测试。通常,您甚至不直接运行它。您使用像 toxnox 这样的工具来运行它。但是,对于 Jupyter,您需要编写一个小型的工具,可以直接在单元格中运行测试。

幸运的是,这个工具很短,即使不简单:

import unittest

def run_test(klass):
    suite = unittest.TestLoader().loadTestsFromTestCase(klass)
    unittest.TextTestRunner(verbosity=2).run(suite)
    return klass

现在工具完成了,是时候进行第一个练习了。

在教学中,从小处着手,进行一个简单的练习来建立信心总是一个好主意。

那么为什么不修复一个非常简单的测试呢?

@run_test
class TestNumbers(unittest.TestCase):
    
    def test_equality(self):
        expected_value = 3 # Only change this line
        self.assertEqual(1+1, expected_value)
    test_equality (__main__.TestNumbers) ... FAIL
    
    ======================================================================
    FAIL: test_equality (__main__.TestNumbers)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-7-5ebe25bc00f3>", line 6, in test_equality
        self.assertEqual(1+1, expected_value)
    AssertionError: 2 != 3
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.002s
    
    FAILED (failures=1)

仅更改此行 是对学生有用的标记。它准确地显示了需要更改的内容。否则,学生可以通过将第一行更改为 return 来修复测试。

在这种情况下,修复很容易:

@run_test
class TestNumbers(unittest.TestCase):
    
    def test_equality(self):
        expected_value = 2 # Fixed this line
        self.assertEqual(1+1, expected_value)
    test_equality (__main__.TestNumbers) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.002s
    
    OK

但是,很快,unittest 库的原生断言将被证明不足。在 pytest 中,这通过重写 assert 中的字节码以使其具有神奇的属性和各种启发式方法来解决。这在 Jupyter notebook 中不容易实现。是时候挖掘出一个好的断言库了:PyHamcrest

from hamcrest import *
@run_test
class TestList(unittest.TestCase):
    
    def test_equality(self):
        things = [1,
                  5, # Only change this line
                  3]
        assert_that(things, has_items(1, 2, 3))
    test_equality (__main__.TestList) ... FAIL
    
    ======================================================================
    FAIL: test_equality (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-11-96c91225ee7d>", line 8, in test_equality
        assert_that(things, has_items(1, 2, 3))
    AssertionError: 
    Expected: (a sequence containing <1> and a sequence containing <2> and a sequence containing <3>)
         but: a sequence containing <2> was <[1, 5, 3]>
    
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.004s
    
    FAILED (failures=1)

PyHamcrest 不仅擅长灵活的断言;它还擅长清晰的错误消息。因此,问题显而易见:[1, 5, 3] 不包含 2,而且看起来也很难看

@run_test
class TestList(unittest.TestCase):
    
    def test_equality(self):
        things = [1,
                  2, # Fixed this line
                  3]
        assert_that(things, has_items(1, 2, 3))
    test_equality (__main__.TestList) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK

借助 Jupyter、PyHamcrest 和一点胶带式的测试工具,您可以教授任何适用于单元测试的 Python 主题。

例如,以下内容可以帮助显示 Python 从字符串中剥离空格的不同方法之间的差异

source_string = "  hello world  "

@run_test
class TestList(unittest.TestCase):
    
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))

    def test_start_strip(self):
        result = source_string # Only change this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))

    def test_end_strip(self):
        result = source_string # Only change this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... FAIL
    test_start_strip (__main__.TestList) ... FAIL
    
    ======================================================================
    FAIL: test_end_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-16-3db7465bd5bf>", line 19, in test_end_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with '  hello' and a string ending with 'world')
         but: a string ending with 'world' was '  hello world  '
    
    
    ======================================================================
    FAIL: test_start_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-16-3db7465bd5bf>", line 14, in test_start_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with 'hello' and a string ending with 'world  ')
         but: a string starting with 'hello' was '  hello world  '
    
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.006s
    
    FAILED (failures=2)

理想情况下,学生会意识到 .lstrip().rstrip() 方法可以满足他们的需求。但是,如果他们不这样做,而是尝试在所有地方都使用 .strip()

source_string = "  hello world  "

@run_test
class TestList(unittest.TestCase):
    
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))

    def test_start_strip(self):
        result = source_string.strip() # Changed this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))

    def test_end_strip(self):
        result = source_string.strip() # Changed this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... FAIL
    test_start_strip (__main__.TestList) ... FAIL
    
    ======================================================================
    FAIL: test_end_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-17-6f9cfa1a997f>", line 19, in test_end_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with '  hello' and a string ending with 'world')
         but: a string starting with '  hello' was 'hello world'
    
    
    ======================================================================
    FAIL: test_start_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-17-6f9cfa1a997f>", line 14, in test_start_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with 'hello' and a string ending with 'world  ')
         but: a string ending with 'world  ' was 'hello world'
    
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.007s
    
    FAILED (failures=2)

他们会收到不同的错误消息,表明剥离了太多空格

source_string = "  hello world  "

@run_test
class TestList(unittest.TestCase):
    
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))

    def test_start_strip(self):
        result = source_string.lstrip() # Fixed this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))

    def test_end_strip(self):
        result = source_string.rstrip() # Fixed this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... ok
    test_start_strip (__main__.TestList) ... ok
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.005s
    
    OK

在一个更真实的教程中,会有更多的示例和更多的解释。这种使用带有工作示例和一些需要修复的示例的 notebook 的技术可以用于实时教学、基于视频的课程,甚至,如果加上更多的文字,学生可以自行完成的教程。

现在走出去分享你的知识吧!

接下来阅读什么
标签
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
Moshe 自 1998 年以来一直参与 Linux 社区,在 Linux “安装聚会” 中提供帮助。他自 1999 年以来一直在编写 Python 程序,并为核心 Python 解释器做出了贡献。Moshe 自这些术语出现之前就一直是 DevOps/SRE,非常关心软件可靠性、构建可重复性以及其他此类事情。

评论已关闭。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.