[note] unittest and pytest
CLI
$ pytest --disable-warnings
$ pytest -s test_xxx.py # 把 print 的內容顯示出來
$ pytest -s -vv -W ignore test_xxx.py
Unit testing framework (unittest)
unittest @ Python Standard Library > Development Tools
內建的 asset keyword
assert <condition>, 'Message if condition is not met'
當 condition 是 false 時,會 raise AssertionError
。
建立 Test Cases
keywords: unittest.TestCase
- 建立一個繼承
unittest.TestCase
的類別 - 欲進行測試的項目(test cases)會定義成該 Class 中的 instance method,並以
test_
作為方法名稱的前綴,例如,def test_square_of_zero(self)
- 需要使用
unittest
提供的assertEqual
而不是內建的assert
- 使用
unittest.main()
可以執行測試
import unittest
class TestSquareFunction(unittest.TestCase):
def test_square_of_zero(self):
self.assertEqual(square(0), 0, 'Expected square(0) to return 0')
def test_square_of_large_number(self):
self.assertEqual(square(1000), 1000000, 'Expected square(1000) to return 1000000')
def test_square_of_negative_number(self):
self.assertEqual(square(-10), 100, 'Expected square(-10) to return 100')
# 執行測試
unittest.main()
常用的 assertions
TestCase:所有可用的方法和 assertions @ 官方文件
assertEqual(a, b)
:檢查是不是相同,等同於a == b
assertTrue(a, b)
:等同於bool(xxx) is True
assertIs(a, b)
:檢查是不是同一個物件,等同於a is b
assertIsNone(x)
:等同於x is None
assertIn(member, container, msg=None)
:等同於member in container
assertLess(a, b)
:等同於a < b
assertAlmostEqual(a, b)
:等同於round(a - b) == 0
assertRaises(exception, callable, *args, **kwds)
exception
是預期要看到的 Error 類別,例如TypeError
、CustomError
callable
帶入的是要測試的 function*args
和**kwds
則是會帶入callable
的參數
assertWarns(warning, callable, *args, **kwds)
使用 subTest 達到 Test Parameterization
keywords: self.subTest(<test_case>)
透過 subTest
的使用,每一個 loop 中的 assertion 都會被視為獨立的 test case,如此可以把測試資料用參數方式帶入 test case 中,測試多種不同的可能情況:
- 在迴圈中使用使用
with self.subTest
import unittest
import music
class MusicSystemTests(unittest.TestCase):
def test_song_license(self):
daily_songs = music.get_daily_playlist()
licensed_songs = music.get_licensed_songs()
for song in daily_songs:
# `self.subTest()` 的參數中可以放入每次執行的測試資料,如此當有錯誤產生時,能比較清 楚知道是哪個測試資料導致 test failed
with self.subTest(song=song):
self.assertIn(song, licensed_songs)
unittest.main()
不使用 self.subTest()
也能執行測試,差別在於使用 subTest
的話,迴圈中的每個測資都會被視為獨立的 test case,所以如果其中一個 failed,還是會繼續執行剩下的測試資料;但如果沒有用 subTest
的話,只要一個資料 failed,這個測試就會被終止。
Test Fixtures
keywords: setUp
、tearDown
、setUpClass
、tearDownClass
在 Test Class 中,下列這個 instance method 會在特定時間點被執行:
setUp
:每個 test case 執行前會先執行這個 instance method,類似 Vitest 中的beforeEach
tearDown
:每個 test case 執行後會接著執行這個 instance method,類似 Vitest 中的afterEach
setUpClass
:在所有 test cases 被執行前,會先執行這個 class method,類似 Vitest 中的beforeAll
tearDownClass
:在所有 test cases 被執行後,會接著執行這個 class method,類似 Vitest 中的afterAll
import unittest
class DatabaseTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("Setting up the test database connection...")
# Code to initialize a database connection
cls.db_connection = "Database connection established"
@classmethod
def tearDownClass(cls):
print("Closing the test database connection...")
# Code to close the database connection
cls.db_connection = None
def setUp(self):
print("Setting up for a test...")
# Code to initialize test data or reset state
self.test_data = {"user": "test_user", "password": "test_password"}
def tearDown(self):
print("Cleaning up after a test...")
# Code to clean up test data or reset state
self.test_data = None
def test_insert_data(self):
print("Testing data insertion...")
# Insert data using self.db_connection and self.test_data
self.assertEqual(self.test_data["user"], "test_user")
def test_delete_data(self):
print("Testing data deletion...")
# Delete data using self.db_connection
self.assertIsNotNone(self.db_connection)
unittest.main()
MagicMock
有兩種使用 MagicMock 的兩種方式
方式一:使用 decorator 後,在 test function 的第一個參數就可以拿到 MagicMock
class TestExample(unittest.TestCase):
@patch("package.path.foo")
def test_foo_bar(self, mock_foo):
# 這裡的 mock_foo 會來自 MagicMock,可以對它進行一些 assertion
# ...
# mock bar 這個 method 的 return value
mock_foo.bar.return_value = "return_value"
# assert bar 這個 method 會被執行一次,且這個 method 被呼叫到的參數會是 arguments_to_be_called
mock_foo.bar.assert_called_one_with("arguments_to_be_called")
方式二:自己定義 MagicMock 後傳入 decorator
class TestExample(unittest.TestCase):
mock_foo = MagicMock()
@patch("package.path.foo", mock_foo)
def test_foo_bar(self):
# 這裡的 mock_foo 會來自 MagicMock,可以對它進行一些 assertion
# ...
# mock mock_foo 裡的 bar 這個 method 的 return value
mock_foo.bar.return_value = "return_value"
# assert mock_foo 裡的 bar 這個 method 會被執行一次,且這個 method 被呼叫到的參數會是 arguments_to_be_called
mock_foo.bar.assert_called_one_with("arguments_to_be_called")
如果一個 method 有 多個地方需要 Mock
可以針對一個 method 使用多個 @patch
decorator,但是 method 中拿到 Mock 的參數順序是反過來的,這個需要特別留意
class TestExample(unittest.TestCase):
@patch("package.path.foo")
@patch("package.path.bar")
def test_foo_bar(self, mock_bar, mock_foo):
pass
# ...
略過某些測試(Skipping tests)
要略過某些測試可以使用:
-
使用
unittest.TestCase
提供的skipTest
這個 instance methodself.skipTest(<reason>)
-
@unittest
的 skip decorator
-
@unittest.skip(<reason>)
-
@unittest.skipUnless(<condition>, <reason>)
-
@unittest.skipIf(<condition>, <reason>)
import unittest
import power_management
class PowerManagementTests(unittest.TestCase):
@unittest.skipIf(power_management.is_low_power_mode(), 'Test skipped in low power mode')
def test_battery_charging(self):
battery_status = power_management.get_battery_status()
self.assertEqual(battery_status, "charging")
@unittest.skipUnless(power_management.is_connected_to_ac(), 'Test requires AC power')
def test_high_performance_mode(self):
performance_mode = power_management.get_performance_mode()
self.assertEqual(performance_mode, "high")
def test_battery_temperature(self):
if power_management.is_low_power_mode():
self.skipTest('Test skipped in low power mode')
battery_temp = power_management.get_battery_temperature()
self.assertLess(battery_temp, 45)
def test_cpu_performance(self):
if not power_management.is_connected_to_ac():
self.skipTest('Test requires AC power')
cpu_performance = power_management.get_cpu_performance()
self.assertGreater(cpu_performance, 80)
unittest.main()
除了 SkipTest 之外,如果我們預期某個測試一定會失敗(可能是還沒修好的 bug),這時候比較適合的使用 @unittest.expectedFailure
而不是把這個 test skip 掉。
Decorators
@pytest.mark.skip # 略過這個 test case
fixture
@pytest.fixture
可以用在幫每個 test 做 setup、提供資料、cleanup 等等(類似 Vitest 中的 beforeEach
、afterEach
),需用到的時候再把定義好的 fixture 當成參數傳入。
例如,使用 @pytest.fixture
可以不用重複建立物件實例,而是可以初始化一次就好:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
@pytest.fixture
def default_student():
return Student('John', 20)
# 可以把 fixture 帶入後,就可以使用
def test_student_initialization(default_student):
assert default_student.name == 'John'
assert default_student.age == 20
參考資料
- Unit Testing @ CodeCademy > Learn Intermediate Python 3