- vừa được xem lúc

Kiểm thử đơn vị cho dự án Python: hãy dùng pytest!

0 0 9

Người đăng: Vũ Văn Định

Theo Viblo Asia

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:

  1. Import TestCase class từ unittest
  2. Tạo TryTesting là subclass của TestCase
  3. Viết mỗi phương thức cho mỗi test trong TryTesting
  4. Dùng phương thức self.assert* của unittest.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:

  1. 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
  2. Đường dẫn rootdir hoặc đường dẫn pytest tìm file config và file để kiểm thử
  3. 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().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

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tìm hiểu về Jest Mocks Test phía frontend

Giới thiệu. Chắc hẳn không ai phủ nhận rằng UnitTest là 1 phần quan trọng trong giai đoạn phát triển phần mềm, đảm bảo cho code được coverage tránh các bug không mong muốn.

0 0 24

- vừa được xem lúc

Sử dụng mock trong Django

Để không làm mất thời gian thì mình xin bắt đầu luôn. Để test được thì đầu tiên chúng ta phải có 1 project. Cách để tạo 1 project như thế nào thì các bạn có thể tham khảo ở đây nhé. mysite/.

0 0 21

- vừa được xem lúc

UnitTest trong Laravel, một số ví dụ về UnitTest

1. Giới thiệu UnitTest. . Cùng với việc viết code thì việc đảm bảo để những dòng code viết ra chạy đúng cũng rất quan trọng.

0 0 437

- vừa được xem lúc

[Investigation] Test double? Mock? Stub? Spy? Fake?

Lời mở đầu. Một vấn đề mà khá nhiều lập trình viên mắc phải đó là sử dụng những thứ mà mình chưa hiểu rõ về nó.

0 0 14

- vừa được xem lúc

02. NUnit là gì? Viết TestCase cơ bản đầu tiên với NUnit - (Phần 1)

NUnit là gì. Framework này bao gồm một số phương thức khẳng định giá trị giúp đảm bảo rằng code cần được test hoạt động đúng với mong muốn.

0 0 4

- vừa được xem lúc

Viết Unit Test dễ dàng và hiệu quả với Copilot

1. Giới thiệu.

0 0 2