+1

Tại sao dùng pytest cho dự án Python?

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


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí