Hướng dẫn hoàn chỉnh Python unittest: từ cơ bản đến nâng cao

1. Python unittest là gì?

unittest là một framework kiểm thử đơn vị thuộc thư viện chuẩn của Python và là một công cụ quan trọng để đảm bảo chất lượng mã. Nó cho phép các nhà phát triển kiểm thử riêng lẻ từng phần của mã và giúp phát hiện lỗi sớm. Ngoài ra, trong quá trình phát triển liên tục, nó giúp xác nhận rằng các thay đổi trong mã không làm hỏng các chức năng hiện có。

Tầm quan trọng của kiểm thử đơn vị

Khi mã trở nên phức tạp hơn, việc xác minh liệu các phần khác nhau có phối hợp hoạt động đúng hay không trở nên khó khăn. Bằng cách áp dụng kiểm thử đơn vị, bạn sẽ dễ dàng ngăn chặn các lỗi bất ngờ do những thay đổi nhỏ gây ra và duy trì sự ổn định tổng thể của chương trình。

2. Cách sử dụng cơ bản của unittest

Cơ bản của unittest là tạo một lớp kế thừa unittest.TestCase và định nghĩa các phương thức kiểm thử trong đó. Trong phương thức kiểm thử, sử dụng các phương thức assertion như assertEqual() để so sánh kết quả mong đợi với kết quả thực tế。

Ví dụ kiểm thử cơ bản

Đoạn mã sau là một ví dụ đơn giản để kiểm thử hàm add(a, b)
import unittest

# Mã cần kiểm thử
def add(a, b):
    return a + b

# Lớp kiểm thử
class TestAddFunction(unittest.TestCase):

    def test_add_integers(self):
        result = add(2, 3)
        self.assertEqual(result, 5)

if __name__ == '__main__':
    unittest.main()
Trong đoạn mã này, chúng ta kiểm tra xem hàm add() có hoạt động đúng hay không. Phương thức assertEqual() xác nhận rằng giá trị mong đợi và kết quả thực tế là bằng nhau. Bằng cách này, bạn có thể xác nhận rằng hàm hoạt động đúng trên nhiều trường hợp khác nhau.

Mở rộng kiểm thử

Bạn có thể sử dụng nhiều phương thức kiểm thử để kiểm tra hành vi của hàm với các đầu vào khác nhau. Ví dụ, cũng có thể kiểm thử số dấu phẩy động và phép nối chuỗi.
def test_add_floats(self):
    result = add(2.5, 3.5)
    self.assertAlmostEqual(result, 6.0, places=2)

def test_add_strings(self):
    result = add("Hello, ", "World!")
    self.assertEqual(result, "Hello, World!")
Như vậy, bằng cách kiểm thử hành vi của hàm với các kiểu dữ liệu khác nhau, bạn có thể đảm bảo rằng hàm hoạt động đúng trong nhiều tình huống.

3. Cách sử dụng setUp() và tearDown()

Để tự động thực thi các xử lý cụ thể trước và sau khi kiểm thử, hãy sử dụng các phương thức setUp()tearDown(). Nhờ đó, bạn có thể chuẩn bị những thứ cần thiết trước khi chạy bài kiểm thử và thực hiện dọn dẹp sau khi kiểm thử kết thúc.

Ví dụ về setUp()

setUp() là phương thức luôn được gọi trước khi mỗi phương thức kiểm thử được thực thi, cho phép gom các thao tác khởi tạo dùng chung.
def setUp(self):
    self.temp_value = 42

Ví dụ về tearDown()

tearDown() là phương thức được thực thi sau mỗi phương thức kiểm thử, thực hiện hậu xử lý và giải phóng tài nguyên. Chẳng hạn, có thể dùng để ngắt kết nối cơ sở dữ liệu hoặc xóa các tệp tạm.
def tearDown(self):
    self.temp_value = None
Bằng cách này, bạn có thể giảm tính dư thừa trong mã kiểm thử và duy trì mã sạch hơn.

4. Kiểm thử các phụ thuộc bằng mock

Khi mã cần kiểm thử phụ thuộc vào tài nguyên bên ngoài (cơ sở dữ liệu, API, v.v.), việc thay thế các phần phụ thuộc đó bằng mock giúp cải thiện tốc độ thực thi kiểm thử và cho phép các bài kiểm thử có tính dự đoán. Sử dụng mô-đun unittest.mock của Python sẽ giúp thực hiện điều này một cách đơn giản.

Ví dụ về mock

Trong đoạn mã dưới đây, chúng ta thay thế hàm tốn nhiều thời gian time_consuming_function() bằng mock.
from unittest.mock import patch

class TestAddFunction(unittest.TestCase):

    @patch('my_module.time_consuming_function')
    def test_add_with_mock(self, mock_func):
        mock_func.return_value = 0
        result = add(2, 3)
        self.assertEqual(result, 5)
Trong ví dụ này, chúng ta chạy kiểm thử mà không gọi time_consuming_function bằng cách sử dụng mock. Nhờ đó, thời gian kiểm thử được rút ngắn trong khi vẫn thu được kết quả chính xác.
年収訴求

5. Xử lý ngoại lệ và assertion tùy chỉnh

unittest cho phép bạn kiểm thử cả việc xử lý ngoại lệ. Ví dụ, để xác nhận rằng ngoại lệ phát sinh đúng trong những tình huống nhất định, hãy dùng assertRaises()

Kiểm thử xử lý ngoại lệ

Ví dụ sau xác nhận rằng ZeroDivisionError sẽ phát sinh。
def test_divide_by_zero(self):
    with self.assertRaises(ZeroDivisionError):
        divide(1, 0)
Đoạn mã này kiểm thử rằng khi gọi divide(1, 0) thì sẽ phát sinh ZeroDivisionError

Tạo assertion tùy chỉnh

Trong các trường hợp mà các assertion tiêu chuẩn không đáp ứng được, bạn có thể tạo phương thức assertion tùy chỉnh của riêng mình。
def assertIsPositive(self, value):
    self.assertTrue(value > 0, f'{value} is not positive')
Việc sử dụng assertion tùy chỉnh cho phép bạn xử lý các kịch bản kiểm thử cụ thể hơn。

6. Tính năng phát hiện kiểm thử của unittest

unittest có tính năng phát hiện kiểm thử cho phép tự động tìm và chạy tất cả các tệp kiểm thử trong dự án. Tính năng này đặc biệt hữu ích đối với các dự án lớn.

Cách sử dụng tính năng phát hiện kiểm thử

Để chạy tính năng phát hiện kiểm thử, hãy dùng lệnh sau.
python -m unittest discover
Điều này sẽ chạy tất cả các tệp test_*.py trong thư mục được chỉ định. Nếu muốn chỉ định tệp hoặc thư mục, hãy dùng các tùy chọn như sau.
python -m unittest discover -s tests -p "test_*.py"
Nhờ tính năng này, bạn không cần chỉ định từng tệp kiểm thử theo cách thủ công và có thể quản lý kiểm thử hiệu quả ngay cả với các dự án lớn.

7. Mẹo nâng cao hiệu năng khi dùng unittest

Khi tốc độ thực thi kiểm thử chậm, hiệu quả phát triển sẽ giảm. Ở đây, chúng tôi giới thiệu một vài mẹo để cải thiện hiệu năng của các bài kiểm thử dùng unittest

Tối ưu hóa I/O tệp

Các bài kiểm thử cần đọc/ghi tệp có thể tăng tốc bằng cách xử lý trong bộ nhớ. Bằng cách sử dụng StringIO để tạo một đối tượng hoạt động như một tệp ngay trên bộ nhớ, bạn có thể tránh I/O đĩa。
from io import StringIO

class TestFileOperations(unittest.TestCase):

    def test_write_to_memory(self):
        output = StringIO()
        output.write('Hello, World!')
        self.assertEqual(output.getvalue(), 'Hello, World!')
Với cách này, ngay cả các bài kiểm thử cần truy cập tệp cũng có thể cải thiện đáng kể tốc độ。

Sử dụng mock

Để giảm thiểu việc truy cập tài nguyên bên ngoài, bạn có thể dùng mock để tăng tốc kiểm thử. Nhờ đó, có thể tránh độ trễ từ mạng, cơ sở dữ liệu, v.v. và rút ngắn thời gian chạy kiểm thử. Trong ví dụ sau, các lời gọi API được thay thế bằng mock。
from unittest.mock import MagicMock

class TestApiCall(unittest.TestCase):

    def test_api_response(self):
        mock_api = MagicMock(return_value={'status': 'success'})
        response = mock_api()
        self.assertEqual(response['status'], 'success')
Bằng cách này, kiểm thử chức năng mà không phụ thuộc vào tài nguyên bên ngoài sẽ giúp bạn xây dựng môi trường kiểm thử nhanh hơn và ổn định hơn。

8. Tổng kết và các bước tiếp theo

Trong bài viết này, chúng tôi đã trình bày toàn diện từ những kiến thức cơ bản về kiểm thử đơn vị bằng unittest của Python, cách sử dụng setup và teardown, kiểm thử các phụ thuộc bằng mock, cho đến các kỹ thuật giúp cải thiện hiệu năng kiểm thử.

Tóm tắt các điểm chính

  • Cách sử dụng cơ bản: Kế thừa unittest.TestCase và tận dụng các phương thức assertion để tạo bài kiểm thử.
  • setUp() / tearDown(): Gom và quản lý các xử lý chung trước và sau mỗi bài kiểm thử để cải thiện khả năng tái sử dụng và tính dễ đọc của mã.
  • Sử dụng mock: Có thể kiểm thử chức năng mà không phụ thuộc vào tài nguyên bên ngoài, nhờ đó hiệu quả kiểm thử được cải thiện đáng kể.
  • Test discovery: Tính năng hữu ích giúp đơn giản hóa việc quản lý kiểm thử trong các dự án lớn.
  • Các kỹ thuật cải thiện hiệu năng: Nhờ xử lý trong bộ nhớ và sử dụng mock, có thể rút ngắn thời gian chạy kiểm thử.

Các bước tiếp theo

Sau khi nắm vững những điều cơ bản của unittest, hãy thử các phương pháp kiểm thử nâng cao hơn. Chẳng hạn, bạn có thể tăng cường chiến lược kiểm thử cho toàn bộ dự án bằng kiểm thử tham số hóa (cho phép kiểm thử nhiều dữ liệu đầu vào cùng lúc) và dùng công cụ coverage để kiểm tra phạm vi kiểm thử của mã. Ngoài ra, cũng nên cân nhắc các framework kiểm thử khác như pytest để mở rộng lựa chọn phù hợp với nhu cầu. Kiểm thử là một yếu tố quan trọng trong phát triển. Hãy chủ động áp dụng kiểm thử để phát hiện lỗi sớm và duy trì chất lượng mã.