Linux, Embedded Systems, Networking, and DevOps Engineering

Pytest và cách sử dụng

Table of contents

  1. Revision history
  2. Introduction
  3. Hoạt động
    1. Example
    2. Giải thích
  4. Sử dụng fixtures
    1. Example
    2. Các biến thể của fixtures
      1. nested fixtures
      2. Sử dụng lại fixtures
      3. Autouse fixtures
      4. Fixture trả về một hàm
      5. Fixture có tham số [HOT]
      6. Chia sẽ fixtures giữa các test khác nhau [HOT]
        1. Phạm vi của shared fixture
    3. Clean up fixtures sau khi chạy test
      1. Dùng yield
    4. Override fixtures
      1. Dùng finalizer
  5. Sử dụng marks
    1. Dùng mark để cung cấp input thay cho fixture
    2. Chỉ chạy test khi mark tồn tại
  6. Các cấu hình cho pytest
  7. Plugin
  8. Tham khảo

Revision history

Revision Date Remark
0.1 Feb-07-2023 Init document
0.2 Feb-08-2023 Add marks
0.3 Feb-08-2023 Add configuration example
0.4 Feb-09-2023 Add plugin

Introduction

  • Pytest là một công cụ test code miễn phí được cộng đồng phát triển.
  • Với các plugins mở rộng, được dùng bởi các hệ thống CI để test tự động.

Hoạt động

User và pytest framework giao tiếp với nhau thông qua các test file, bên dưới là một ví dụ:

Example

  • tạo file test: test_sample.py
# content of demo/test_sample.py
def sumOf(a,b):
    return a + b


def test_answer1():
    assert sumOf(3,6) == 8
def test_answer2():
    assert sumOf(3,1) == 4
  • Chạy test bằng cách chạy lệnh pytest tại thư mục có chứa các test file.
cd demo
pytest

============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.1, pluggy-1.0.0
rootdir: /home/thanhle/build/test
collected 2 items

test_sample.py F.                                                        [100%]

=================================== FAILURES ===================================
_________________________________ test_answer1 _________________________________

    def test_answer1():
>       assert sumOf(3,6) == 8
E       assert 9 == 8
E        +  where 9 = sumOf(3, 6)

test_sample.py:6: AssertionError
=========================== short test summary info ============================
FAILED test_sample.py::test_answer1 - assert 9 == 8
========================= 1 failed, 1 passed in 0.02s ==========================
thanhle@thanhle ~/build/test

Giải thích

  • pytest là một module, khi được gọi nó sẽ tìm ở thư mục hiện tại tất cả các file có dạng test_*.py và thực thi nó(file test_sample.py thõa điều kiện)
  • Bên trong mỗi file test_*.py, pytest tìm các hàm có dạng test*(x) và chạy nó (hàm test_answer1test_answer2 thõa điều kiện)

Sử dụng fixtures

Example

# content of demo/test_sample.py
def sumOf(a,b):
    return a + b

def test_answer(a_val, b_val):
    assert sumOf(a_val, b_val) == a_val + b_val

  • Nếu chạy pytest với nội dung như trên, pytest sẽ không biết lấy a_valb_val từ đâu.

  • fixtures được thiết kế để cung cấp giá trị này cho hàm test, 2 fixtures cung cấp giá trị cho a_valb_val được khai báo như sau

@pytest.fixture
def a_val():
    return 10

@pytest.fixture
def b_val():
    return 5
  • File cuối cùng
import pytest

@pytest.fixture
def a_val():
    return 10
@pytest.fixture
def b_val():
    return 5

def sumOf(a,b):
    return a + b


def test_answer(a_val, b_val):
    assert sumOf(a_val, b_val) == a_val + b_val

Các biến thể của fixtures

nested fixtures

  • biến đầu vào của một fixtures là một fixtures khác
# Example of nested fixtures
import pytest
# Arrange
@pytest.fixture
def first_entry():
    return "a"
# Arrange
@pytest.fixture
def order(first_entry):
    return [first_entry]
def test_string(order):
    # Act
    order.append("b")
    # Assert
    assert order == ["a", "b"]

Sử dụng lại fixtures

  • giá trị a_val đưa vào 2 test case là giá trị ban đầu được hàm @pytest.fixture tạo ra, giúp các test case không bị ảnh hưởng lẫn nhau.
# Example of reusing fixtures
import pytest

@pytest.fixture
def a_val():
    return 10


def test_answer1(a_val):
    tmp = a_val
    print("a_val", a_val)
    a_val = a_val + 1
    assert a_val == tmp + 1
def test_answer2(a_val):
    assert a_val == 11 <<<<< Fail  đây, a_val = 10
  • Nếu một fixtures được gọi trong một test, hoặc hàm con của test, thì pytest chỉ request 1 lần, những lần sau dùng cache

    Autouse fixtures

  • pytest tự động gọi fixture để lấy giá trị mà không cần check có test hoặc fixture nào khác cần tới không.
# Example Autouse fixtures
@pytest.fixture(autouse=True)
def a_val():
    return 10

Fixture trả về một hàm

@pytest.fixture
def make_customer_record():
    def _make_customer_record(name):
        return {"name": name, "orders": []}
    return _make_customer_record
def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")

Fixture có tham số [HOT]

  • Example
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
    smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
    yield smtp_connection
    print(f"finalizing {smtp_connection}")
    smtp_connection.close()
  • Pytest sẽ thực hiện tạo lần lượt các instance smtp_connection ứng với mỗi phần tử của array params. Các test được thực hiện trên lần lượt các instance này.
  • Code của test sẽ không cần thay đổi gì

Chia sẽ fixtures giữa các test khác nhau [HOT]

  • Trong các ví dụ trên, fixture được tạo ta trong mỗi test, chúng là các instance độc lập nhau.
  • Có những fixture cần được tái sử dụng lại trong toàn bộ quá trình test: ví dụ một ssh connection, serial connection, Wi-Fi connection …
  • pytest sử dụng 1 file conftest.py để chứa các fixture được khởi tạo 1 lần và dùng chung cho nhiều test.
# content of conftest.py
import smtplib
import pytest

@pytest.fixture(scope="module") # Phạm vi của fixture
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

file test

# content of test_module.py
def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg

def test_noop(smtp_connection):
    response, msg = smtp_connection.noop()
    assert response == 250
Phạm vi của shared fixture

Fixture được tạo ra và bị hủy dựa trên phạm vi hoạt động của nó

  • function: phạm vi mặc định, fixture bị hũy sau mỗi test.
  • class: bị hủy trong lần test cuối cùng của class.
  • module: bị hủy trong lần test cuối cùng của module.
  • package: bị hủy trong lần test cuối cùng của package.
  • session: bị hủy trong lần test cuối cùng của session.

Clean up fixtures sau khi chạy test

Pytest hỗ trợ việc khởi tạo và clean-up fixtures bằng 2 cách: dùng yield và dùng finalizer

Dùng yield

Xét ví dụ bên dưới

@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    mail_admin.delete_user(user)
  • Trong giai đoạn khởi tạo, pytest sẽ dừng lại ở đoạn yield user tương tự như return user
  • Sau khi chạy xong các test, pytest sẽ chạy đoạn code bên dưới yield cho từng fixture, ta cần thêm phần cleanup ở đó

Info! Thứ tự cleanup fixture sẽ ngược lại với thứ tự khởi tạo fixture

Override fixtures

  • Fixtures được cung cấp bới các pluggin khác nhau, có thể override lại fixtures ở file conftest.py Example
tests/
    conftest.py
    # content of tests/conftest.py
        import pytest
        @pytest.fixture
            def username():
                return 'username'

    test_something.py
        # content of tests/test_something.py
        def test_username(username):
            assert username == 'username'
    subfolder/
        conftest.py
            # content of tests/subfolder/conftest.py
            import pytest
            @pytest.fixture
            def username(username):
                return 'overridden-' + username
        test_something_else.py
            # content of tests/subfolder/test_something_else.py
            def test_username(username):
                assert username == 'overridden-username'
  • fixture username được xây dựng khác nhau ở mỗi level.

Dùng finalizer

  • Cần thêm param request vào cho mỗi fixture muốn dùng cleanup.
  • Cần khai báo hàm cleanup và đăng ký hàm này với object request thông qua api addfinalizer
@pytest.fixture
def receiving_user(mail_admin, request):
    user = mail_admin.create_user()
    def delete_user():
        mail_admin.delete_user(user)
    request.addfinalizer(delete_user)
    return user

Warning! finalizer được gọi lúc khỏi tạo fixture và nếu có lỗi gì trong quá trình thêm này thì test vẫn tiếp tục chạy.

Sử dụng marks

  • Fixture đã cung cấp khả năng khởi tạo resource cho test rất thành công. Tiếp theo mark cung cấp các tùy biến cho các test function bên trong mỗi test file. Sau đây là các ví dụ sử dụng mark để tùy biến cho test function.

Dùng mark để cung cấp input thay cho fixture

  • Sử dụng built-in mark parametrize để cung cấp input cho hàm test.
    # content of test_expectation.py
    import pytest
    @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
    def test_eval(test_input, expected):
      assert eval(test_input) == expected
    
  • Khai báo toàn cục
import pytest
pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected
    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected

  • Matrix
import pytest
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
# Các cặp input được tạo ra: x=0/y=2, x=1/y=2, x=0/y=3, và x=1/y=3
def test_foo(x, y):
    pass

Chỉ chạy test khi mark tồn tại

  • Thay vì luôn luôn chạy một test, có thể dùng mark để chỉ chạy một test khi mark name tồn tại.
import pytest

@pytest.fixture
def a_val():
    return 10

def test_answer1(a_val):
    tmp = a_val
    print("a_val", a_val)
    a_val = a_val + 1
    assert a_val == tmp + 12

@pytest.mark.thanhle
def test_answer2(a_val):
    assert a_val == 10

  • Trong ví dụ trên, pytest sẽ kiểm tra mark name thanhle có tồn tại hay không trước khi chạy test_answer2.
  • Vì mark thanhle là một custom mark nên cần khai báo. Cần tạo một file pytest.ini tại thư mục cần chạy test, và khai báo custom mark như sau
[pytest]
markers =
  thanhle: test mark

Các cấu hình cho pytest

  • Các thiết lập cho pytest được ghi ở file pytest.ini
  • Nếu muốn thay đổi các option trong file này thì có thể overide nó bằng cách dùng option -o/--override-ini
pytest -o console_output_style=classic -o cache_dir=/tmp/mycache

# content of pytest.ini
[pytest]

# Test file to scan
python_files =
    test_*.py
    check_*.py
    example_*.py

# App options
addopts =
  -s
  --embedded-services esp,idf
  --tb short
  --skip-check-coredump y
  --maxfail=2 -rf # exit after 2 failures, report fail info
console_output_style =
    classic: classic pytest output.
    progress: like classic pytest output, but with a progress indicator
    count: like progress, but shows progress as the number of tests completed instead of a percent
# ignore DeprecationWarning
filterwarnings =
  ignore::DeprecationWarning:matplotlib.*:
  ignore::DeprecationWarning:google.protobuf.*:
  ignore::_pytest.warning_types.PytestExperimentalApiWarning

# log related

log_cli = True # Enable log display during test run (also known as “live logging”). The default is False.
log_cli_level = INFO
log_cli_format = %(asctime)s %(levelname)s %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S
log_file = logs/pytest-logs.txt

# junit related
# xunit1 produces old style output, compatible with the xunit 1.0 format
# xunit2 produces xunit 2.0 style output, which should be more compatible with latest Jenkins versions

junit_family = xunit1

## log all to `system-out` when case fail
# log: write only logging captured output.
# system-out: write captured stdout contents.
# system-err: write captured stderr contents.
# out-err: write both captured stdout and stderr contents.
# all: write captured logging, stdout and stderr contents.
# no (the default): no captured output is written.

# Write captured log messages to JUnit report: one of no|log|system-out|system-err|out-err|all
junit_logging = log
# Capture log information for passing tests to JUnit report
junit_log_passing_tests = True


Plugin

  • Pytest đã cung cấp một frame work hỗ trợ việc test, tạo các resource cho việc test. Các dự án khác nhau sẽ có các nhu cầu test khác nhau, ví dụ test cho web app, mobile app, embedded, backend, frontend …
  • Plugin giúp tạo công cụ chuyên biệt cho từng loại ứng dụng. Một vài ví dụ
Tên Chức năng
pytest-django write tests for django apps, using pytest integration.
pytest-cov coverage reporting, compatible with distributed testing
pytest-embedded A pytest plugin that has multiple services available for various functionalities. Designed for embedded testing.
Info! Xem thêm bài viết về pytest-embedded

Tham khảo