單元測試原則

單元測試原則

單元測試 (unit test) 表示針對一個片段的程式碼給定某個測項 input 來執行此程式片段, 並輸出 output 來比較是否符合預期的結果。

Given-When-Then 原則

此為基本寫一個單元測試的結構:

Given: 表示要測的測向 input
When: 被測的程式碼執行 context
Then: 預期出來的結果

1
2
3
4
5
6
7
8
9
10
11
12
13

def add(a, b):
return a + b


def test_add():
# given
a, b = 2, 3
# when
result = add(a, b)
# then
assert result is 5

程式模組化

程式再撰寫時,必須要把本身的邏輯給拆分(ex: function, module),才比較好導入測試。因此,要做 unit test 程式碼本身必須有一定的模組化。

測試隔離原則

程式邏輯多少會牽扯到外部的相依性呼叫 ex: (DB query, call API) 必須要把程式有牽扯到外部的相依性的邏輯盡可能的去 decouple,如此才比較好導入單元測試。真的無法避免在考慮使用 mock 或 stub 的方式來替代外部環境依賴。

一個功能,一個 Assert

寫單元測試也務必遵守單一職責原則,就是一個測試儘可能專注於一個 test case,如此測試的定義才會清楚,才不會在執行時模糊不清。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# not good
def test_module1():
assert module1.foo1() is "foo1"
assert module1.foo2() is "foo2"
assert module1.foo3() is "foo3"

# prefer
def test_module1_foo1():
assert module1.foo1() is "foo1"

def test_module1_foo2():
assert module1.foo2() is "foo2"

def test_module1_foo3():
assert module1.foo3() is "foo3"

測試獨立原則

單元測試中每個 test case 彼此間都是完全獨立的且不可相依,彼此測試間都有獨立的環境啟動以及結束,此即為 setup 以及 tear down操作。假設有三個測試啟動以及結束都會個別觸發 setup 以及 tear down三次。以下以 pytest 為範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TestClass:
def setup_class(self):
print("setup_class called once for the class")

def teardown_class(self):
print("teardown_class called once for the class")


def setup_method(self):
print("setup_method called for every method")

def teardown_method(self):
print("teardown_method called for every method")


def test_one(self):
print("one")
assert True
print("one after")

def test_two(self):
print("two")
assert False
print("two after")

def test_three(self):
print("three")
assert True
print("three after")

執行此測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ pytest -s test_class.py

setup_class called once for the class
setup_method called for every method
one
one after
teardown_method called for every method
setup_method called for every method
two
teardown_method called for every method
setup_method called for every method
three
three after
teardown_method called for every method
teardown_class called once for the class

reference:

  1. https://martinfowler.com/bliki/UnitTest.html
  2. https://martinfowler.com/bliki/GivenWhenThen.html
  3. https://code-maven.com/slides/python/pytest-class