用 Jupyter Notebook 教 Python
有了 Jupyter、PyHamcrest,用一點測試的代碼把它們連在一起,你就可以教任何適用于單元測試的 Python 內(nèi)容。
關(guān)于 Ruby 社區(qū)的一些事情一直讓我印象深刻,其中兩個例子是對測試的承諾和對易于上手的強調(diào)。這兩方面最好的例子是 Ruby Koans,在這里你可以通過修復(fù)測試來學習 Ruby。
要是我們能把這些神奇的工具也用于 Python,我們應(yīng)該可以做得更好。是的,使用 Jupyter Notebook、PyHamcrest,再加上一點類似于膠帶的粘合代碼,我們可以做出一個包括教學、可工作的代碼和需要修復(fù)的代碼的教程。
首先,需要一些“膠布”。通常,你會使用一些漂亮的命令行測試器來做測試,比如 pytest 或 virtue。通常,你甚至不會直接運行它。你使用像 tox 或 nox 這樣的工具來運行它。然而,對于 Jupyter 來說,你需要寫一小段粘合代碼,可以直接在其中運行測試。
幸運的是,這個代碼又短又簡單:
import unittestdef run_test(klass):suite = unittest.TestLoader().loadTestsFromTestCase(klass)unittest.TextTestRunner(verbosity=2).run(suite)return klass
現(xiàn)在,裝備已經(jīng)就緒,可以進行第一次練習了。
在教學中,從一個簡單的練習開始,建立信心總是一個好主意。
那么,讓我們來修復(fù)一個非常簡單的測試:
@run_testclass TestNumbers(unittest.TestCase):def test_equality(self):expected_value = 3 # 只改這一行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_equalityself.assertEqual(1+1, expected_value)AssertionError: 2 != 3----------------------------------------------------------------------Ran 1 test in 0.002sFAILED (failures=1)
“只改這一行” 對學生來說是一個有用的標記。它準確地表明了需要修改的內(nèi)容。否則,學生可以通過將第一行改為 return 來修復(fù)測試。
在這種情況下,修復(fù)很容易:
@run_testclass TestNumbers(unittest.TestCase):def test_equality(self):expected_value = 2 # 修復(fù)后的代碼行self.assertEqual(1+1, expected_value)
test_equality (__main__.TestNumbers) ... ok----------------------------------------------------------------------Ran 1 test in 0.002sOK
然而,很快,unittest 庫的原生斷言將被證明是不夠的。在 pytest 中,通過重寫 assert 中的字節(jié)碼來解決這個問題,使其具有神奇的屬性和各種啟發(fā)式方法。但這在 Jupyter notebook 中就不容易實現(xiàn)了。是時候挖出一個好的斷言庫了:PyHamcrest。
from hamcrest import *@run_testclass TestList(unittest.TestCase):def test_equality(self):things = [1,5, # 只改這一行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_equalityassert_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.004sFAILED (failures=1)
PyHamcrest 不僅擅長靈活的斷言,它還擅長清晰的錯誤信息。正因為如此,問題就顯而易見了。[1, 5, 3] 不包含 2,而且看起來很丑:
@run_testclass TestList(unittest.TestCase):def test_equality(self):things = [1,2, # 改完的行3]assert_that(things, has_items(1, 2, 3))
test_equality (__main__.TestList) ... ok----------------------------------------------------------------------Ran 1 test in 0.001sOK
使用 Jupyter、PyHamcrest 和一點測試的粘合代碼,你可以教授任何適用于單元測試的 Python 主題。
例如,下面可以幫助展示 Python 從字符串中去掉空白的不同方法之間的差異。
source_string = " hello world "@run_testclass TestList(unittest.TestCase):# 這是個贈品:它可以工作!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 # 只改這一行assert_that(result,all_of(starts_with("hello"), ends_with("world ")))def test_end_strip(self):result = source_string # 只改這一行assert_that(result,all_of(starts_with(" hello"), ends_with("world")))
test_complete_strip (__main__.TestList) ... oktest_end_strip (__main__.TestList) ... FAILtest_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_stripassert_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_stripassert_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.006sFAILED (failures=2)
理想情況下,學生們會意識到 .lstrip() 和 .rstrip() 這兩個方法可以滿足他們的需要。但如果他們不這樣做,而是試圖到處使用 .strip() 的話:
source_string = " hello world "@run_testclass TestList(unittest.TestCase):# 這是個贈品:它可以工作!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() # 改完的行assert_that(result,all_of(starts_with("hello"), ends_with("world ")))def test_end_strip(self):result = source_string.strip() # 改完的行assert_that(result,all_of(starts_with(" hello"), ends_with("world")))
test_complete_strip (__main__.TestList) ... oktest_end_strip (__main__.TestList) ... FAILtest_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_stripassert_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_stripassert_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.007sFAILED (failures=2)
他們會得到一個不同的錯誤信息,顯示去除了過多的空白:
source_string = " hello world "@run_testclass TestList(unittest.TestCase):# 這是個贈品:它可以工作!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 lineassert_that(result,all_of(starts_with("hello"), ends_with("world ")))def test_end_strip(self):result = source_string.rstrip() # Fixed this lineassert_that(result,all_of(starts_with(" hello"), ends_with("world")))
test_complete_strip (__main__.TestList) ... oktest_end_strip (__main__.TestList) ... oktest_start_strip (__main__.TestList) ... ok----------------------------------------------------------------------Ran 3 tests in 0.005sOK



































