Bài viết này mình không đi sâu cách sử dụng pytest
mà hướng đến mục tiêu chỉ ra những đặc điểm khiến nó nên được sử dụng hơn bất kỳ các thư viện kiểm thử đơn vị nào.
pytest
là một trong những thư viện unittest được sử dụng phổ biến nhất trong số 1 vài thư viện như unittest
, hypothesis
, nose
, doctest
, tox
,... Qua tìm hiểu mình nhận thấy thư viện này có những điểm nổi trội rõ rệt so với các đối thủ khác, đặt cạnh nó đối thủ đáng gờm nhất, 1 built-in module có sẵn của Python là unittest
, pytest
thể hiện rất nhiều nổi trội. Dưới đây là lý do bạn nên dùng pytest
cho kiểm thử đơn vị:
Ít dài dòng
Dưới đây là đoạn code sử dụng unittest
để kiểm thử:
# test_with_unittest.py from unittest import TestCase class TryTesting(TestCase): def test_always_passes(self): self.assertTrue(True) def test_always_fails(self): self.assertTrue(False)
Chạy code bằng option discover
của unittest
:
(venv) $ python -m unittest discover
F.
======================================================================
FAIL: test_always_fails (test_with_unittest.TryTesting)
----------------------------------------------------------------------
Traceback (most recent call last): File "...\effective-python-testing-with-pytest\test_with_unittest.py", line 10, in test_always_fails self.assertTrue(False)
AssertionError: False is not true ---------------------------------------------------------------------- Ran 2 tests in 0.006s FAILED (failures=1)
Kết quả trả ra đúng kỳ vọng, 1 hàm pass và 1 hàm fail, nhưng hãy nhìn vào cách code:
- Import
TestCase
class từ unittest - Tạo
TryTesting
là subclass củaTestCase
- Viết mỗi phương thức cho mỗi test trong
TryTesting
- Dùng phương thức
self.assert*
củaunittest.TestCase
để kiểm tra điều kiện.
Nếu dùng pytest
thì chỉ cần viết như sau:
# test_with_pytest.py def test_always_passes(): assert True def test_always_fails(): assert False
pytest
chỉ cần bạn thêm tiền tố test_
trước tên hàm cầm test, sử dụng từ khóa assert
có sẵn của Python mà không cần nhớ phương thức của unittest
.
Output đẹp hơn
Chạy code pytest
trên cho ra output như sau (Nhớ đặt tên file bằng tiền tố test_
):
(venv) $ pytest
============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items test_with_pytest.py .F [ 50%]
test_with_unittest.py F. [100%] ================================== FAILURES ===================================
______________________________ test_always_fails ______________________________ def test_always_fails():
> assert False
E assert False test_with_pytest.py:7: AssertionError
________________________ TryTesting.test_always_fails _________________________ self = <test_with_unittest.TryTesting testMethod=test_always_fails> def test_always_fails(self):
> self.assertTrue(False)
E AssertionError: False is not true test_with_unittest.py:10: AssertionError
=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:... ========================= 2 failed, 2 passed in 0.20s =========================
Output này gồm:
- Trạng thái hệ thống, gồm: phiên bản Python, phiên bản
pytest
và các plugin được cài - Đường dẫn
rootdir
hoặc đường dẫnpytest
tìm file config và file để kiểm thử - Số lượng hàm cần kiểm thử mà
pytest
tìm ra
============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items
Output thể hiện kết quả của các bài kiểm thử sử dụng cú pháp giống với unittest
:
- Dấu chấm ( . ): mỗi dấu chấm ứng với 1 bài kiểm thử đã pass
- Chữ F: ứng với 1 bài kiểm thử bị fail
- Chữ E: bài kiểm thử trả ra lỗi/ngoại lệ Trong khi đó, tiến độ kiểm thử được hiển thị ở phía bên phải.
test_with_pytest.py .F [ 50%]
test_with_unittest.py F. [100%]
Với mỗi bài kiểm thử bị fail sẽ có thêm thông tin về hàm gây ra lỗi, được hiển thị trong khối FAILURES, ở ví dụ trên bài kiểm thử fail bởi vì assert False
luôn trả về fail:
================================== FAILURES ===================================
______________________________ test_always_fails ______________________________ def test_always_fails():
> assert False
E assert False test_with_pytest.py:7: AssertionError
________________________ TryTesting.test_always_fails _________________________ self = <test_with_unittest.TryTesting testMethod=test_always_fails> def test_always_fails(self):
> self.assertTrue(False)
E AssertionError: False is not true test_with_unittest.py:10: AssertionError
Cuối cùng là chi tiết hơn về lỗi, hỗ trợ lập trình viên debug:
=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:... ========================= 2 failed, 2 passed in 0.20s =========================
Như vậy, so với unittest
thì pytest
có output nhiều thông tin và dễ đọc hơn.
Ít phải nhớ cách dùng hơn
Với việc tận dụng từ khóa assert
có sẵn, cái mà đã quen thuộc với hầu hết lập trình viên Python khiến việc sử dụng pytest
rất dễ dàng cho người mới.
Nếu bạn chưa quen với assert
thì dưới đây có thêm ví dụ để bạn làm quen:
# test_assert_examples.py def test_uppercase(): assert "loud noises".upper() == "LOUD NOISES" def test_reversed(): assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1] def test_some_primes(): assert 37 in { num for num in range(2, 50) if not any(num % div == 0 for div in range(2, num)) }
Chú ý là tên hàm sẽ thường được đặt dài => khi gặp lỗi chỉ cần nhìn tên hàm là đoán được nơi xảy ra lỗi; trong từng hàm cũng chỉ nên assert
ít chức năng => các hàm cô lập nhau, dễ kiểm thử và phát hiện lỗi.
Dễ quản lý các phụ thuộc
Là decorator fixture
của pytest
. Giả sử bạn có 2 class, 1 để chạy các logic tính toán, 2 để lưu vào cơ sở dữ liệu, và bạn muốn kiểm thử class tính toán sau đó bạn xem kết quả ở cơ sở dữ liệu xem có đúng không => class tính toán phụ thuộc vào class lưu. Trong trường hợp này ta sẽ tạo ra 1 "đối tượng giả" (mock object) làm nhiệm vụ lưu (vì mình không muốn kiểm thử việc lưu), thay vì lưu vào cơ sở dữ liệu thì đối tượng giả này sau khi nhận kết quả tính toán sẽ in ra màn hình thay vì lưu.
unittest
hỗ trợ việc tạo ra các phụ thuộc thông qua .setUp()
và .tearDown()
nhưng nếu hàm kiểm thử mà lớn, nhiều class thì khó đọc code vì theo dõi class nào phụ thuộc class nào.
fixture
của pytest
thì có thể tái sử dụng, giả sử ta kiểm thử thêm chức năng tạo mới người dùng thì ta có thể sử dụng lại đối tượng giả in ra màn hình ở trên thay vì viết lại, khi người dùng đăng ký mới nó sẽ in tên người đó ra màn hình và ta dễ dàng kiểm tra đúng hay không.
Dễ filter các kiểm thử
pytest
cung cấp cách để bạn có thể filter một vài trong số toàn bộ kiểm thử mà bạn viết, vì nhiều khi muốn kiểm thử một service nào đó, bạn chỉ cần chạy một vài thay vì tất cả các kiểm thử để tiết kiệm thời gian mà vẫn đáp ứng yêu cầu.
- Filter bằng tên: Có thể giới hạn để
pytest
chỉ chạy những kiểm thử được đặt tên thỏa mãn yêu cầu truyền vào (sử dụng với tham số đi cùng-k
, viết tắt của keywords) - Đường dẫn nhất định: Mặc định
pytest
sẽ chỉ chạy những kiểm thử được viết trong thư mục chạy kiểm thử và các thư mục con của nó - Phân nhóm các bài kiểm thử: (Sử dụng từ khóa
-m
, viết tắt của marks) Ta có thể phân loại ra các nhóm như nhóm 1 để kiểm thử khi chạy cục bộ, nhóm 2 để chạy khi triển khai thật,...
Tham số hóa hàm kiểm thử
Chính là decorator mark.parametrize
của pytest
. Nó giải quyết vấn đề thường gặp khi viết testcase là hay viết lặp lại 1 đoạn code (ví dụ khi kiểm thử động vật thì phải viết code kiểm thử cho chó, mèo, gà,... với cấu trúc giống nhau.
unittest
có hỗ trợ việc gom nhóm nhiều bài kiểm thử thành một nhưng nó lại không output ra report của từng bài kiểm thử đó ra màn hình => nếu 1 assert
bị fail trong khi tất cả pass, nó sẽ chỉ trả ra duy nhất là hàm đó fail.
Nhiều plugin
Plugin là đoạn mã như --cov=myproject
mà bạn có thể thêm vào câu command line để nó hiển thị các thông tin mà mình cần, ví dụ --cov
(cover) này sẽ thêm thông tin là các testcase bạn viết phủ được bao nhiêu các trường hợp của dự án.
pytest
là thư viện open cho lập trình viên customize theo dự án của mình hoặc tự thêm tính năng mới => các lập trình viên đã phát triển rất nhiều plugin hữu ích cho pytest
.
Chúc các bạn thành công khi ứng dụng pytest
vào dự án của mình!
Tham khảo: https://realpython.com/pytest-python-testing/#what-makes-pytest-so-useful