Hướng dẫn viết unit test python

Các bạn có bao giờ tái cấu trúc code không ? Sau mỗi lần refactor, bạn có bao giờ nín thở khi deploy lại sản phẩm lên PRODUCTION hay không? Làm thế nào để không phải trải qua cảm giác kinh hoàng ấy? Trong quy trình phát triển phần mềm, có một giai đoạn mà các lập trình có thể tự cứu được "sự nghiệp" của mình mà không phải chờ đợi kết quả test của các tester hay báo cáo vận hành sản phẩm trên PRODUCTION, giai đoạn đó là viết unit-test.

Vậy unit-test là gì ? Và áp dụng nó trong các bài toán giải quyết trong Python như thế nào? Mời các bạn theo dõi trong bài viết tiếp theo của tôi.

1. Unit-test là gì ?

Unit là một khái niệm trong hệ thống phần mềm và mỗi unit được định nghĩa là các thành phần độc lập với nhau theo "định cỡ": Function -> Class -> Module -> Package (với Python).
Unit-test là một thuật ngữ trong kiểm thử phần mềm, được định nghĩa là kiểm thử mức đơn vị. Có nghĩa là đặt các bài test vào các thành phần "đơn vị" (unit) của hệ thống phần mềm.

Chúng ta có thể hiểu unit-test như một black-box-testing với tập dữ liệu đầu vào sau khi chạy qua một unit và tập dữ liệu đầu ra phải khớp với một tập dữ liệu đã được tính toán trước.

2. Ai là người viết unit-test?

Thông thường thì các lập trình viên là người sẽ phải chịu trách nhiệm viết unit-test. Vì unit-test là công cụ bảo đảm tính "đúng đắn" của mỗi thành phần sản phẩm mà họ làm ra.

Để có thể viết được các unit-test, các lập trình viên sẽ phải hiểu rõ về yêu cầu cần thực hiện trong các unit.

3. Khi nào thì viết unit-test?

Tùy vào đặc thù của từng dự án hoặc yêu cầu tiến độ của dự án mà lập trình viên sẽ thực hiện viết prototype cho unit-test trước cùng với các unit có thể xuất hiện trong hệ thống. Sau đó, trong lúc lập trình hoặc thậm chí là sau khi sản phẩm chạy ổn định rồi họ mới thực hiện viết unit-test (lúc này đã có các tập hợp dữ liệu input-output hoàn chỉnh).

4. Một số yêu cầu với unit-test.

  • Unit-test phải ngắn gọn, dễ hiểu, dễ đọc, có thể sẽ phải có đầy đủ mô tả cho từng nhóm dữ liệu input/output.
  • Mỗi unit-test cần phát triển riêng biệt, không nên thiết kế output của unit-test này là input của unit-test tiếp theo.
  • Khi đặt tên unit-test cần đặt tên gợi nhớ hoặc theo quy chuẩn của từng nhóm phát triển để tường minh việc unit-test này đang test cho unit nào.
  • Mỗi unit-test chỉ nên thực hiện test cho một unit, nếu các unit có về input/output hoặc code thì chấp nhận việc duplicate các unit-test.

5. Lợi ích của unit-test.

  • Khi có unit-test, các lập trình viên có thể tự tin mà refactor code.
  • Khi thực hiện chạy unit-test, đôi khi lập trình viên sẽ phát hiện ra lỗi tiềm ẩn (lỗi truy vấn đến database chẳng hạn).
  • Nếu làm việc cộng tác trong team, việc đặt unit-test như các rule trong tiến trình CI sẽ ngăn được trường hợp sản phẩm bị nhưng vẫn được triển khai trên PRODUCTION.

Cách sử dụng unit test trong Python.

Với mỗi ngôn ngữ lập trình lại có các công cụ, thư viện khác nhau để thực hiện viết unit-test. Trong Python, có thể sử dụng pytest và unittest để viết các unit-test. Do unittest có độ thông dụng nhiều hơn nên bài viết sau đây của tôi sẽ tập trung vào module unittest trong Python.

1. Giới thiệu về unittest.

Trong lập trình thì cách giới thiệu nhanh nhất cho một module/thư viện chính là ... lập trình dựa trên các đặc tính của module/thư viên đó. Sau đây là một ví dụ nhỏ về unitest. Các bạn hãy tạo một file có tên simple_unittest.py và gõ vào đoạn code như dưới đây.

# test_simple_unittest.py
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('python'.upper(), 'PYTHON')

    def test_isupper(self):
        self.assertTrue('PYTHON'.isupper())
        self.assertFalse('Python'.isupper())

    def test_islower(self):
        self.assertTrue('PYTHON'.islower())
        self.assertFalse('Python'.islower())

    def test_split(self):
        test_string = 'python is a best language'
        self.assertEqual(test_string.split(),
                        ['python', 'is', 'a', 'best', 'language'])
        # check that test_string.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            test_string.split(2)

if __name__ == '__main__':
    unittest.main(verbosity=2)

Từ màn hình terminal, thực hiện chạy file trên, chúng ta thu được kết quả:

> python .\test_simple_unittest.py
test_islower (__main__.TestStringMethods) ... FAIL
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

======================================================================
FAIL: test_islower (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
File ".\test_simple_unittest.py", line 14, in test_islower
self.assertTrue('PYTHON'.islower())
AssertionError: False is not true

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=1)

Bây giờ chúng ta cùng nhau phân tích một chút về ví dụ trên.

import unittest --> Có nghĩa là module unittest là module đã được cài đặt cùng với gói cài đặt của Python.

class TestStringMethods(unittest.TestCase): --> module unittest cung cấp một class unittest.TestCase để các class khác thực hiện kế thừa.

def test_upper(self):, def test_isupper(self):, def test_split(self): --> các function đều bắt đầu bằng test_

unittest.main(verbosity=2) --> Để khởi chạy các test case trong một module, cần đặt gọi đến unittest.main() của module đó. unittest.main() thường đặt ở cuối cùng của module (file code).

assertEqual, assertTrue, assertFalse, assertRaises --> Các function dùng để so sánh và raise lên các message thông báo cho kết quả test chính chính xác hay không.

Trong trường kết quả test không chính xác, sẽ hiển thị ra bài test không pass được bằng cách trỏ đến dòng code và nguyên nhân gây ra lỗi.
Như ví dụ trên, lỗi nằm ở dòng só 14 khi cố tình đặt self.assertTrue('PYTHON'.islower()), và messagelỗi cũng chỉ ra đúng dòng lỗi là dòng 14, và nôi dung lỗi cũng rõ ràng AssertionError: False is not true

2. Một số function trong unit-test thường dùng.

2.1. Các function trong unit-test trả về True/False

assertEqual(value1, value2)
--> Trả về True: Nếu giá trị value1 == value2
--> Trả về False: nếu value1 != value2

assertTrue(value)
--> Trả về True: Nếu giá trị value == True
--> Trả về False: nếu value1 == False

assertFalse(value)
--> Trả về True: Nếu giá trị value == False
--> Trả về False: nếu value1 == True


with self.assertRaises(TypeException):
--expressions--

Trả về True: Nếu trong các expressions phát sinh ra lỗi TypeException
Trả về False: Nếu trong expressions không phát sinh ra lỗi

2.2. Các function khác
Method Checks that
assertNotEqual(a, b) a != b
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b)
assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertRegex(s, r) r.search(s)
assertNotRegex(s, r) not r.search(s)
assertCountEqual(a, b) a and b have the same elements in the same number, regardless of their order

2.3. Các function so sánh các kiểu dữ liệu khác nhau.

Method Used to compare
assertMultiLineEqual(a, b) strings
assertSequenceEqual(a, b) sequences
assertListEqual(a, b) lists
assertTupleEqual(a, b) tuples
assertSetEqual(a, b) sets or frozensets
assertDictEqual(a, b) dicts

3. Cách chạy unittest.

Ở ví dụ phía trên, ngoài cách gọi trực tiếp vào module/file để thực thi, chúng ta có thể gọi unittest từng đơn vị như sau: 

Test cả module:

>python -m unittest test_simple_unittest

======================================================================

FAIL: test_islower (test_simple_unittest.TestStringMethods)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "E:\code_learn\Projects\20201206_python-unitest\python-unittest\source_code\test_simple_unittest.py", line 14in test_islower

    self.assertTrue('PYTHON'.islower())

AssertionErrorFalse is not true

----------------------------------------------------------------------

Ran 4 tests in 0.001s

FAILED (failures=1)

Test từng class/function trong module

python -m unittest test_simple_unittest.TestStringMethods

======================================================================

FAIL: test_islower (test_simple_unittest.TestStringMethods)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "E:\code_learn\Projects\20201206_python-unitest\python-unittest\source_code\test_simple_unittest.py", line 14in test_islower

    self.assertTrue('PYTHON'.islower())

AssertionErrorFalse is not true

----------------------------------------------------------------------

Ran 4 tests in 0.001s

FAILED (failures=1)

Test từng function trong module

python -m unittest test_simple_unittest.TestStringMethods.test_isupper

----------------------------------------------------------------------

Ran 1 test in 0.000s

OK

python -m unittest test_simple_unittest.TestStringMethods.test_islower

======================================================================

FAIL: test_islower (test_simple_unittest.TestStringMethods)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "E:\code_learn\Projects\20201206_python-unitest\python-unittest\source_code\test_simple_unittest.py", line 14in test_islower

    self.assertTrue('PYTHON'.islower())

AssertionErrorFalse is not true

----------------------------------------------------------------------

Ran 1 test in 0.000s

FAILED (failures=1)

4. Một số ví dụ về unittest

Ví dụ 1: Viết chương trình tìm nghiệm của phương trình bậc 1: aX + b = 0

 Viết unit test cho chương trình trên

Giải thuật:

Nếu a == 0 và b == 0 -> phương trình vô số nghiệm, trả về ALL

Nếu a == 0 và b != 0 -> Phương trình vô nghiệm, trả về NONE

Kết quả: X = -b / a 

Sau khi giải bài toàn và lưu vào file first_equation.py

# first_equation.py
def find_x(a,b):
    if a:
        return -b/a
    elif b:
        return "NONE"
    else:
        return "ALL"

def find_x_2(a,b):
    return -b/a


if __name__ == "__main__":
    print(find_x(10,10))​

Thực hiện viết unittest của function trên.

# test_first_equation.py

import unittest
import first_equation

class TestFirst(unittest.TestCase):

    def test_find_x(self):
        args = (10, 10)
        self.assertEqual(first_equation.find_x(*args), -1)
        args = (0, 0)
        self.assertEqual(first_equation.find_x(*args), "ALL")
        args = (0, 10)
        self.assertEqual(first_equation.find_x(*args), "NONE")

if __name__ == '__main__':
    unittest.main(verbosity=2)

Thực hiện chạy thử, ta thu được kết quả:
> python -m unittest test_first_equation.TestFirst
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Ví dụ 2: Cho 1 chuỗi cho trước, thực hiện tách chuỗi theo các khoảng trắng và trả về một list của các tuple dạng [(số thứ tự, giá trị),…]

Input:
test_str = "Python is a best language”

Output:

[(1, 'Python'), (2, 'is'), (3, 'a'), (4, 'best'), (5, 'language')]


Code bài toán:

# list_value.py
data = "Python is a best language"

def create_value(data_str):
    input_data = data_str.split()
    result = []
    for index, element in enumerate(input_data):
        result.append((index + 1, element))
    return result

if __name__ == "__main__":
    print(create_value(data))

Viết unit-test cho bài toán:

# test_list_value.py
import unittest
import list_value

class TestStringMethods(unittest.TestCase):

    def test_create_tuple(self):
        test_str = list_value.data
        data_list = test_str.split()
        len_expected = len(data_list)
        result_list = list_value.create_value(test_str)
        self.assertIsInstance(result_list, list, "Dữ liệu trả về không đúng dạng list")
        for item in result_list:
            self.assertIsInstance(item, tuple, "Dữ liệu trả về không đúng dạng tuple")
        self.assertEqual(len(result_list), len_expected, "Số lượng phần tử không đúng")
        self.assertEqual(result_list[0][0], 1, 'Index phần tử đầu tiên không đúng')
        self.assertEqual(
            result_list[-1][0],
            len_expected,
            'Index phần tử cuối cùng không đúng'
        )


if __name__ == '__main__':
    unittest.main(verbosity=2)

Kết quả thực hiện:
>python -m unittest test_list_value.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Ví dụ 3: Xử lý chuỗi palindrome
Chuỗi palindrome có dạng: Chuỗi lớn hơn 1, Không phân biệt chữ hoa, thường, viết xuôi hay ngược đều thu được kết quả như nhau.
Hãy viết chương trình kiểm tra một chuỗi có phải là chuỗi palindrome không ?

Dữ liệu mẫu:

Input: test_str = ”Civic”
Output: True

Input: test_str = ”Noon”
Output: True


Input: test_str = ” Python”
Output: False

Code của bài toán:

# palindrome.py

def check_palindrome(text):
    if len(text) <= 1:
        return False
    text = text.strip().lower().replace(' ', '')
    return text == text[::-1]


if __name__ == "__main__":
    text = "Noon"
    print(check_palindrome(text))

Unitest của bài toán:

# test_palindrome.py
import unittest
import palindrome


class TestExercise(unittest.TestCase):
    MESSAGE_FMT = 'Kết quả mong muốn là `{0}`, nhận được `{1}`: `{2}`'

    def _test_all(self, func, cases):
        for input_, expect in cases:
            output = func(input_)
            msg = self.MESSAGE_FMT.format(expect, output, input_)
            self.assertEqual(output, expect, msg)


class TestPalindrome(TestExercise):

    def test_check_palindrome(self):
        cases = [('ana', True),
                 ('Civic', True),
                 ('Python', False),
                 ('', False),
                 ('P', False),
                 ('Able was I ere I saw Elba', True)]
        self._test_all(palindrome.check_palindrome, cases)


if __name__ == '__main__':
    unittest.main(verbosity=2)

Thực hiện test bài test:

> python -m unittest test_palindrome
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Tiếp theo, các bạn hãy sửa lại các function thực hiện xử lý bài toán để ra kết quả... sai. 

Sau đó thực hiện chạy các test case, chắc chắn kết quả sẽ bị FAIL --> Đó là lý do tại sao chúng ta cần phải viết unitest. Kết quả fail có nghĩa là chúng ta sẽ phải xem lại phần code chương trình, có thể đã có chỗ nào đó đã bị thay đổi để đưa so với ban đầu khiến cho chương trình hoạt động không được chuẩn xác.

Kết

Trên đây là một số vấn đề cơ bản khi thực hiện viết unittest cho lập trình Python. Hẹn gặp lại các bạn vào một bài viết chuyên sâu hơn, phân tích kỹ hơn các chiến thuật viết unittest và unittest-mock.
Source code chương trình được lưu trên github tại link: https://github.com/quangvinh2986/python-unittest.
Cảm ơn các bạn đã theo dõi bài viết.

Tài liệu tham khảo: https://docs.python.org/3/library/unittest.html.