Test

寫測試可以減少改壞以前的東西,加速開發,在原生的 Python 就有提供測試的方法 unittest,然而隨著越來越多的套件支援與其脫鉤,如果是開新專案,可以試著從一開始就使用 pytest

Pytest

該套件支援簡單的測試方法,多個套件支援其測試,例如在寫 Django Channels 時異步測試就推薦使用 pytest 可以支援其異步測試,該套件也支援 pyproject.toml 當作設定檔

Usage

pip install pytest

pytest tests
=================================================== test session starts ===================================================
platform darwin -- Python 3.8.5, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 -- /Users/super/project/prj/venv/bin/python3
cachedir: .pytest_cache
django: settings: settings (from ini)
rootdir: /Users/super/project/prj, configfile: pyproject.toml
plugins: cov-2.11.1, django-4.1.0
collected 26 items

Creating test database for alias 'default'...
tests/test_admin/test_actions.py::TestCreateTransformationAction::test_transformation_action_check_fk_values PASSED
tests/test_models/test_copy.py::TestInnerCopy::test_copy_to_new_diagram_inner_attr_are_copied PASSED
tests/test_models/test_copy.py::TestInnerCopy::test_copy_to_new_diagram_nodes_will_be_copied PASSED
tests/test_models/test_copy.py::TestInnerCopy::test_copy_to_new_diagram_transitions_will_be_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_copy_to_new_diagram_inner_attr_are_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_copy_to_new_diagram_nodes_will_be_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_copy_to_new_diagram_transitions_will_be_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_deep_inner_copy_to_new_diagram_inner_attr_are_copied PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_diagram_will_create_static_diagram PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_diagram_with_inner_diagram PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_node_will_take_permissions PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_node_will_take_updateable_fields PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_transformation_will_take_all_things PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_uuid_pk_mode PASSED
tests/test_models/test_inner_enter.py::TestDeepInnerEnter::test_deep_enter PASSED
tests/test_models/test_inner_enter.py::TestDeepInnerEnter::test_deep_leave PASSED
tests/test_models/test_inner_enter.py::TestDeepInnerEnter::test_leave_inner_deep_will_not_present PASSED
tests/test_models/test_inner_enter.py::TestInnerEnter::test_enter_inner_forward_node_will_check_forward_number PASSED
tests/test_models/test_inner_enter.py::TestInnerEnter::test_enter_node_with_inner_will_create_init_nodes_present PASSED
tests/test_models/test_inner_enter.py::TestInnerEnter::test_enter_rollback_will_create_outer_rollback_node_present PASSED
tests/test_models/test_nodes.py::TestFreeze::test_enter_node_will_change_present_node PASSED
tests/test_models/test_permissions.py::TestFreeze::test_get_permission_pks PASSED
tests/test_utils/test_context_utils.py::TestIterKeys::test_simple_case PASSED
tests/test_utils/test_parser_utils.py::TestIterKeys::test_deep_comparison PASSED
tests/test_utils/test_parser_utils.py::TestIterKeys::test_has_attribute PASSED
tests/test_utils/test_parser_utils.py::TestIterKeys::test_simple_case PASSED
Destroying test database for alias 'default'...

Configuration

# pyproject.toml
[tool.pytest.ini_options]
addopts = "--tb=short -p no:warnings -s -v -ra"
DJANGO_SETTINGS_MODULE = "settings"
# -- recommended but optional:
python_files = [
  "tests.py",
  "test_*.py",
  "*_tests.py",
]

Coverage

跑了測試還不夠,為了要知道哪些程式碼有被測試到,甚至是哪些程式碼被哪些測試程式測試,我們可以使用 coverage 套件來達成,該套件也支援 pyproject.toml

Normal usage

pip install coverage
python -m unittest discover

Integrate with pytest

pip install coverage
coverage run --source=prj -m pytest
coverage report -m
Name                              Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------------------------------------------------------------------------------
prj/__init__.py                       1      0      0      0   100%
prj/admin/__init__.py                28      9      2      0    63%   17-35
prj/admin/actions.py                 64     33     14      1    44%   14->30, 44-58, 61-82, 103-122
prj/admin/base.py                    47     47     14      0     0%   1-95
prj/admin/helpers.py                 18     11      4      0    32%   8, 13, 18-19, 22-32
prj/apps.py                           3      3      0      0     0%   1-5
prj/models.py                       195      7     67      5    95%   24, 45, 65, 121, 129, 239->238, 373->376, 381->390, 422, 458
prj/tests/__init__.py                 0      0      0      0   100%
prj/utils/__init__.py                 0      0      0      0   100%
prj/utils/ast_parser_utils.py        16      0     10      1    96%   27->exit
prj/utils/rule_context_utils.py       7      1      4      2    73%   9
----------------------------------------------------------------------------------------------------------------------------
TOTAL                               379    111    115      9    70%

Configuration

# pyproject.toml
[tool.coverage.report]
exclude_lines = [
  "pragma: no cover",
  "def __repr__",
  "if __name__ == .__main__.:",
  "nocov",
  "if TYPE_CHECKING:",
]

[tool.coverage.run]
# Activating branch coverage is super important
branch = true
omit = [
  "*/migrations/*"
]
source = "prj"

pytest-cov

為了讓 pytest 執行時不要那麼麻煩改用 coverage,我們使用該套件去讓 pytest 直接整合 coverage 以後寫測試就只要下 pytest --cov=prj 就好了

Usage

pip install pytest-cov
pytest --cov=prj
=================================================== test session starts ===================================================
platform darwin -- Python 3.8.5, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 -- /Users/super/project/prj/venv/bin/python3
cachedir: .pytest_cache
django: settings: settings (from ini)
rootdir: /Users/super/project/prj, configfile: pyproject.toml
plugins: cov-2.11.1, django-4.1.0
collected 26 items


Creating test database for alias 'default'...
tests/test_admin/test_actions.py::TestCreateTransformationAction::test_transformation_action_check_fk_values PASSED
tests/test_models/test_copy.py::TestInnerCopy::test_copy_to_new_diagram_inner_attr_are_copied PASSED
tests/test_models/test_copy.py::TestInnerCopy::test_copy_to_new_diagram_nodes_will_be_copied PASSED
tests/test_models/test_copy.py::TestInnerCopy::test_copy_to_new_diagram_transitions_will_be_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_copy_to_new_diagram_inner_attr_are_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_copy_to_new_diagram_nodes_will_be_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_copy_to_new_diagram_transitions_will_be_copied PASSED
tests/test_models/test_copy.py::TestDeepCopy::test_deep_inner_copy_to_new_diagram_inner_attr_are_copied PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_diagram_will_create_static_diagram PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_diagram_with_inner_diagram PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_node_will_take_permissions PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_node_will_take_updateable_fields PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_transformation_will_take_all_things PASSED
tests/test_models/test_freeze.py::TestFreeze::test_freeze_uuid_pk_mode PASSED
tests/test_models/test_inner_enter.py::TestDeepInnerEnter::test_deep_enter PASSED
tests/test_models/test_inner_enter.py::TestDeepInnerEnter::test_deep_leave PASSED
tests/test_models/test_inner_enter.py::TestDeepInnerEnter::test_leave_inner_deep_will_not_present PASSED
tests/test_models/test_inner_enter.py::TestInnerEnter::test_enter_inner_forward_node_will_check_forward_number PASSED
tests/test_models/test_inner_enter.py::TestInnerEnter::test_enter_node_with_inner_will_create_init_nodes_present PASSED
tests/test_models/test_inner_enter.py::TestInnerEnter::test_enter_rollback_will_create_outer_rollback_node_present PASSED
tests/test_models/test_nodes.py::TestFreeze::test_enter_node_will_change_present_node PASSED
tests/test_models/test_permissions.py::TestFreeze::test_get_permission_pks PASSED
tests/test_utils/test_context_utils.py::TestIterKeys::test_simple_case PASSED
tests/test_utils/test_parser_utils.py::TestIterKeys::test_deep_comparison PASSED
tests/test_utils/test_parser_utils.py::TestIterKeys::test_has_attribute PASSED
tests/test_utils/test_parser_utils.py::TestIterKeys::test_simple_case PASSED
Destroying test database for alias 'default'...


---------- coverage: platform darwin, python 3.8.5-final-0 -----------
Name                              Stmts   Miss Branch BrPart  Cover
----------------------------------------------------------------------
prj/__init__.py                       1      0      0      0   100%
prj/admin/__init__.py                28      9      2      0    63%
prj/admin/actions.py                 64     33     14      1    44%
prj/admin/base.py                    47     47     14      0     0%
prj/admin/helpers.py                 18     11      4      0    32%
prj/apps.py                           3      3      0      0     0%
prj/models.py                       195      7     67      5    95%
prj/tests/__init__.py                 0      0      0      0   100%
prj/utils/__init__.py                 0      0      0      0   100%
prj/utils/ast_parser_utils.py        16      0     10      1    96%
prj/utils/rule_context_utils.py       7      1      4      2    73%
----------------------------------------------------------------------
TOTAL                               379    111    115      9    70%

Configuration

# pyproject.toml
[tool.pytest.ini_options]
# 多加一個參數即可
addopts = "--tb=short -p no:warnings -s -v -ra --cov=prj"
DJANGO_SETTINGS_MODULE = "settings"
# -- recommended but optional:
python_files = [
  "tests.py",
  "test_*.py",
  "*_tests.py",
]