Bài 11.2. Python - Kiểm tra một hàm

Để học về kiểm tra, chúng ta cần có mã để kiểm tra. Dưới đây là một hàm đơn giản nhận vào tên và họ, và trả về một tên đầy đủ được định dạng gọn gàng:

# filepath: /f:/blog/python-vy/11/name_function.py
def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = f"{first} {last}"
    return full_name.title()

Hàm get_formatted_name() kết hợp tên và họ với một khoảng trắng ở giữa để hoàn thành tên đầy đủ, sau đó viết hoa và trả về tên đầy đủ. Để kiểm tra rằng get_formatted_name() hoạt động, hãy tạo một chương trình sử dụng hàm này. Chương trình names.py cho phép người dùng nhập tên và họ, và xem tên đầy đủ được định dạng gọn gàng:

# filepath: /f:/blog/python-vy/11/names.py
from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")
while True:
    first = input("\nPlease give me a first name: ")
    if first == 'q':
        break
    last = input("Please give me a last name: ")
    if last == 'q':
        break
    formatted_name = get_formatted_name(first, last)
    print(f"\tNeatly formatted name: {formatted_name}.")

Chương trình này nhập khẩu get_formatted_name() từ name_function.py. Người dùng có thể nhập một loạt tên và họ và xem tên đầy đủ được định dạng gọn gàng được tạo ra:

Enter 'q' at any time to quit.
Please give me a first name: janis
Please give me a last name: joplin
    Neatly formatted name: Janis Joplin.
Please give me a first name: bob
Please give me a last name: dylan
    Neatly formatted name: Bob Dylan.
Please give me a first name: q

Chúng ta có thể thấy rằng các tên được tạo ra ở đây là chính xác. Nhưng giả sử chúng ta muốn sửa đổi get_formatted_name() để nó cũng có thể xử lý tên đệm. Khi chúng ta làm điều này, chúng ta muốn đảm bảo rằng chúng ta không làm hỏng cách hàm xử lý các tên chỉ có tên và họ. Chúng ta có thể kiểm tra mã của mình bằng cách chạy names.py và nhập một tên như Janis Joplin mỗi khi chúng ta sửa đổi get_formatted_name(), nhưng điều đó sẽ trở nên tẻ nhạt. May mắn thay, pytest cung cấp một cách hiệu quả để tự động hóa việc kiểm tra đầu ra của một hàm. Nếu chúng ta tự động hóa việc kiểm tra get_formatted_name(), chúng ta có thể luôn tự tin rằng hàm sẽ hoạt động khi nhận được các loại tên mà chúng ta đã viết các bài kiểm tra cho.

Kiểm tra đơn vị và trường hợp kiểm tra

Có rất nhiều cách tiếp cận để kiểm tra phần mềm. Một trong những loại kiểm tra đơn giản nhất là kiểm tra đơn vị. Kiểm tra đơn vị xác minh rằng một khía cạnh cụ thể của hành vi của một hàm là chính xác. Một trường hợp kiểm tra là một tập hợp các kiểm tra đơn vị cùng nhau chứng minh rằng một hàm hoạt động như mong đợi, trong phạm vi đầy đủ các tình huống mà bạn mong đợi nó xử lý.

Một trường hợp kiểm tra tốt xem xét tất cả các loại đầu vào mà một hàm có thể nhận và bao gồm các bài kiểm tra để đại diện cho mỗi tình huống này. Một trường hợp kiểm tra với phạm vi đầy đủ bao gồm một loạt các kiểm tra đơn vị bao phủ tất cả các cách có thể mà bạn có thể sử dụng một hàm. Đạt được phạm vi đầy đủ trên một dự án lớn có thể là một thách thức. Thường thì đủ tốt để viết các bài kiểm tra cho các hành vi quan trọng của mã của bạn và sau đó nhắm đến phạm vi đầy đủ chỉ khi dự án bắt đầu được sử dụng rộng rãi.

Một bài kiểm tra thành công

Với pytest, viết bài kiểm tra đơn vị đầu tiên của bạn khá đơn giản. Chúng ta sẽ viết một hàm kiểm tra đơn lẻ. Hàm kiểm tra sẽ gọi hàm mà chúng ta đang kiểm tra, và chúng ta sẽ đưa ra một khẳng định về giá trị được trả về. Nếu khẳng định của chúng ta đúng, bài kiểm tra sẽ thành công; nếu khẳng định sai, bài kiểm tra sẽ thất bại.

Dưới đây là bài kiểm tra đầu tiên của hàm get_formatted_name():

# filepath: /f:/blog/python-vy/11/test_name_function.py
from name_function import get_formatted_name

def test_first_last_name():
    """Do names like 'Janis Joplin' work?"""
    formatted_name = get_formatted_name('janis', 'joplin')
    assert formatted_name == 'Janis Joplin'

Trước khi chúng ta chạy bài kiểm tra, hãy xem xét kỹ hơn hàm này. Tên của một tệp kiểm tra rất quan trọng; nó phải bắt đầu bằng test_. Khi chúng ta yêu cầu pytest chạy các bài kiểm tra mà chúng ta đã viết, nó sẽ tìm bất kỳ tệp nào bắt đầu bằng test_, và chạy tất cả các bài kiểm tra mà nó tìm thấy trong tệp đó.

Trong tệp kiểm tra, chúng ta đầu tiên nhập khẩu hàm mà chúng ta muốn kiểm tra: get_formatted_name(). Sau đó, chúng ta định nghĩa một hàm kiểm tra: trong trường hợp này, test_first_last_name(). Đây là một tên hàm dài hơn so với những gì chúng ta đã sử dụng, vì một lý do tốt. Đầu tiên, các hàm kiểm tra cần bắt đầu bằng từ test, theo sau là một dấu gạch dưới. Bất kỳ hàm nào bắt đầu bằng test_ sẽ được pytest phát hiện và sẽ được chạy như một phần của quá trình kiểm tra.

Ngoài ra, tên hàm kiểm tra nên dài hơn và mô tả hơn so với tên hàm thông thường. Bạn sẽ không bao giờ gọi hàm kiểm tra trực tiếp; pytest sẽ tìm hàm và chạy nó cho bạn. Tên hàm kiểm tra nên đủ dài để nếu bạn thấy tên hàm trong một báo cáo kiểm tra, bạn sẽ có một ý tưởng tốt về hành vi nào đang được kiểm tra.

Tiếp theo, chúng ta gọi hàm mà chúng ta đang kiểm tra. Ở đây chúng ta gọi get_formatted_name() với các đối số 'janis' và 'joplin', giống như chúng ta đã sử dụng khi chạy names.py. Chúng ta gán giá trị trả về của hàm này cho formatted_name.

Cuối cùng, chúng ta đưa ra một khẳng định. Một khẳng định là một tuyên bố về một điều kiện. Ở đây chúng ta tuyên bố rằng giá trị của formatted_name nên là 'Janis Joplin'.

Chạy một bài kiểm tra

Nếu bạn chạy tệp test_name_function.py trực tiếp, bạn sẽ không nhận được bất kỳ đầu ra nào vì chúng ta chưa bao giờ gọi hàm kiểm tra. Thay vào đó, chúng ta sẽ để pytest chạy tệp kiểm tra cho chúng ta.

Để làm điều này, mở một cửa sổ terminal và điều hướng đến thư mục chứa tệp kiểm tra. Nếu bạn đang sử dụng VS Code, bạn có thể mở thư mục chứa tệp kiểm tra và sử dụng terminal được nhúng trong cửa sổ trình soạn thảo. Trong cửa sổ terminal, nhập lệnh pytest. Dưới đây là những gì bạn nên thấy:

$ pytest
========================= test session starts =========================
platform darwin -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x
rootdir: /.../python_work/chapter_11
collected 1 item
test_name_function.py . [100%]
========================== 1 passed in 0.00s ==========================

Hãy cố gắng hiểu đầu ra này. Trước hết, chúng ta thấy một số thông tin về hệ thống mà bài kiểm tra đang chạy. Tôi đang kiểm tra điều này trên một hệ thống macOS, vì vậy bạn có thể thấy một số đầu ra khác ở đây. Quan trọng nhất, chúng ta có thể thấy các phiên bản của Python, pytest và các gói khác đang được sử dụng để chạy bài kiểm tra.

Tiếp theo, chúng ta thấy thư mục nơi bài kiểm tra đang được chạy từ: trong trường hợp của tôi, python_work/chapter_11. Chúng ta có thể thấy rằng pytest đã tìm thấy một bài kiểm tra để chạy, và chúng ta có thể thấy tệp kiểm tra đang được chạy. Dấu chấm đơn sau tên tệp cho chúng ta biết rằng một bài kiểm tra đã thành công, và 100% làm rõ rằng tất cả các bài kiểm tra đã được chạy. Một dự án lớn có thể có hàng trăm hoặc hàng ngàn bài kiểm tra, và các dấu chấm và chỉ báo phần trăm hoàn thành có thể hữu ích trong việc theo dõi tiến trình tổng thể của việc chạy bài kiểm tra.

Dòng cuối cùng cho chúng ta biết rằng một bài kiểm tra đã thành công, và mất chưa đến 0.01 giây để chạy bài kiểm tra.

Đầu ra này cho thấy rằng hàm get_formatted_name() sẽ luôn hoạt động cho các tên có tên và họ, trừ khi chúng ta sửa đổi hàm. Khi chúng ta sửa đổi get_formatted_name(), chúng ta có thể chạy lại bài kiểm tra này. Nếu bài kiểm tra thành công, chúng ta biết rằng hàm sẽ vẫn hoạt động cho các tên như Janis Joplin.

Một bài kiểm tra thất bại

Một bài kiểm tra thất bại trông như thế nào? Hãy sửa đổi get_formatted_name() để nó có thể xử lý tên đệm, nhưng hãy làm điều đó theo cách làm hỏng hàm cho các tên chỉ có tên và họ, như Janis Joplin.

Dưới đây là phiên bản mới của get_formatted_name() yêu cầu một đối số tên đệm:

# filepath: /f:/blog/python-vy/11/name_function.py
def get_formatted_name(first, middle, last):
    """Generate a neatly formatted full name."""
    full_name = f"{first} {middle} {last}"
    return full_name.title()

Phiên bản này nên hoạt động cho những người có tên đệm, nhưng khi chúng ta kiểm tra nó, chúng ta thấy rằng chúng ta đã làm hỏng hàm cho những người chỉ có tên và họ.

Lần này, chạy pytest cho đầu ra sau:

$ pytest
========================= test session starts =========================
--snip--
test_name_function.py F [100%]
============================== FAILURES ===============================
________________________ test_first_last_name _________________________
def test_first_last_name():
    """Do names like 'Janis Joplin' work?"""
> formatted_name = get_formatted_name('janis', 'joplin')
E TypeError: get_formatted_name() missing 1 required positional argument: 'last'
test_name_function.py:5: TypeError
======================= short test summary info =======================
FAILED test_name_function.py::test_first_last_name - TypeError: get_formatted_name() missing 1 required positional argument: 'last'
========================== 1 failed in 0.04s ==========================

Có rất nhiều thông tin ở đây vì có rất nhiều điều bạn có thể cần biết khi một bài kiểm tra thất bại. Mục đầu tiên đáng chú ý trong đầu ra là một chữ F đơn, cho chúng ta biết rằng một bài kiểm tra đã thất bại. Sau đó, chúng ta thấy một phần tập trung vào các lỗi thất bại, vì các bài kiểm tra thất bại thường là điều quan trọng nhất cần tập trung vào trong một lần chạy bài kiểm tra. Tiếp theo, chúng ta thấy rằng test_first_last_name() là hàm kiểm tra đã thất bại. Một dấu ngoặc nhọn chỉ ra dòng mã gây ra bài kiểm tra thất bại. Chữ E trên dòng tiếp theo cho thấy lỗi thực tế gây ra thất bại: một TypeError do thiếu một đối số vị trí bắt buộc, last. Thông tin quan trọng nhất được lặp lại trong một tóm tắt ngắn gọn ở cuối, vì vậy khi bạn đang chạy nhiều bài kiểm tra, bạn có thể nhanh chóng hiểu được bài kiểm tra nào đã thất bại và tại sao.

Phản hồi với một bài kiểm tra thất bại

Bạn làm gì khi một bài kiểm tra thất bại? Giả sử bạn đang kiểm tra các điều kiện đúng, một bài kiểm tra thành công có nghĩa là hàm đang hoạt động đúng và một bài kiểm tra thất bại có nghĩa là có lỗi trong mã mới mà bạn đã viết. Vì vậy, khi một bài kiểm tra thất bại, đừng thay đổi bài kiểm tra. Nếu bạn làm vậy, các bài kiểm tra của bạn có thể thành công, nhưng bất kỳ mã nào gọi hàm của bạn giống như bài kiểm tra sẽ đột nhiên ngừng hoạt động. Thay vào đó, hãy sửa mã gây ra bài kiểm tra thất bại. Kiểm tra các thay đổi bạn vừa thực hiện đối với hàm, và tìm hiểu cách những thay đổi đó làm hỏng hành vi mong muốn.

Trong trường hợp này, get_formatted_name() trước đây chỉ yêu cầu hai tham số: tên và họ. Bây giờ nó yêu cầu tên, tên đệm và họ. Việc thêm tham số tên đệm bắt buộc đã làm hỏng hành vi ban đầu của get_formatted_name(). Lựa chọn tốt nhất ở đây là làm cho tên đệm trở thành tùy chọn. Khi chúng ta làm điều này, bài kiểm tra của chúng ta cho các tên như Janis Joplin nên thành công trở lại, và chúng ta nên có thể chấp nhận tên đệm. Hãy sửa đổi get_formatted_name() để tên đệm là tùy chọn và sau đó chạy lại trường hợp kiểm tra. Nếu nó thành công, chúng ta sẽ tiếp tục đảm bảo rằng hàm xử lý tên đệm đúng cách.

Để làm cho tên đệm là tùy chọn, chúng ta di chuyển tham số middle đến cuối danh sách tham số trong định nghĩa hàm và cho nó một giá trị mặc định rỗng. Chúng ta cũng thêm một kiểm tra if để xây dựng tên đầy đủ đúng cách, tùy thuộc vào việc có cung cấp tên đệm hay không:

# filepath: /f:/blog/python-vy/11/name_function.py
def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name."""
    if middle:
        full_name = f"{first} {middle} {last}"
    else:
        full_name = f"{first} {last}"
    return full_name.title()

Trong phiên bản mới này của get_formatted_name(), tên đệm là tùy chọn. Nếu một tên đệm được truyền vào hàm, tên đầy đủ sẽ chứa tên, tên đệm và họ. Nếu không, tên đầy đủ sẽ chỉ bao gồm tên và họ. Bây giờ hàm nên hoạt động cho cả hai loại tên. Để tìm hiểu xem hàm vẫn hoạt động cho các tên như Janis Joplin, hãy chạy lại bài kiểm tra:

$ pytest
========================= test session starts =========================
--snip--
test_name_function.py . [100%]
========================== 1 passed in 0.00s ==========================

Bài kiểm tra bây giờ thành công. Đây là lý tưởng; nó có nghĩa là hàm hoạt động cho các tên như Janis Joplin một lần nữa, mà không cần chúng ta phải kiểm tra hàm thủ công. Sửa hàm của chúng ta dễ dàng hơn vì bài kiểm tra thất bại đã giúp chúng ta xác định cách mã mới làm hỏng hành vi hiện có.

Thêm các bài kiểm tra mới

Bây giờ chúng ta biết rằng get_formatted_name() hoạt động cho các tên đơn giản một lần nữa, hãy viết một bài kiểm tra thứ hai cho những người bao gồm tên đệm. Chúng ta làm điều này bằng cách thêm một hàm kiểm tra khác vào tệp test_name_function.py:

# filepath: /f:/blog/python-vy/11/test_name_function.py
from name_function import get_formatted_name

def test_first_last_name():
    """Do names like 'Janis Joplin' work?"""
    formatted_name = get_formatted_name('janis', 'joplin')
    assert formatted_name == 'Janis Joplin'

def test_first_last_middle_name():
    """Do names like 'Wolfgang Amadeus Mozart' work?"""
    formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
    assert formatted_name == 'Wolfgang Amadeus Mozart'

Chúng ta đặt tên cho hàm mới này là test_first_last_middle_name(). Tên hàm phải bắt đầu bằng test_ để hàm chạy tự động khi chúng ta chạy pytest. Chúng ta

Comments

There are no comments at the moment.