PyCon Korea 2019
pytest로 파이썬 코드 테스트하기
최영선
[Link]@[Link]
PyCon Korea 2019
pytest
Why we should test code?
• Modify Code with Confidence
• Identify Bugs Early
• Improve System Design
[Link]
testing
What is pytest?
• python test framework.
• Advantages of pytest
• Easy to use
• Can run a specific test or subset of tests
• Skip tests
• Can run tests in parallel
What is pytest?
• Examples
• [Link]
PyCon Korea 2019
1. Getting Started
Getting Started
• Requirements
• Python 3
• virtualenv
• Installation
$ virtualenv –p python3.7 venv
$ source virtualenv/bin/activate
$ (venv)
• An example of simple test
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert int(3) == 5
• Execution
$ pytest test_sample.py
How to run test cases?
$ pytest -h
usage: pytest [options] [file_or_dir] [file_or_dir] [...]
positional arguments:
file_or_dir
• (venv) [Link] test_sample.py
• (venv) [Link] tests/test_sample.py
• (venv) [Link] tests/
Practice
# [Link]
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
return x / y
# [Link] # test_calculator.py
def add(x, y): import pytest
return x + y from calculator import add, divide
def subtract(x, y):
def test_add():
return x - y assert add(1, 2) == 3
assert not add (2, 2) == 3
def multiply(x, y): def test_divide():
return x * y with [Link](ZeroDivisionError):
divide(1, 0)
def divide(x, y):
return x / y
PyCon Korea 2019
2. Project Structure
Structure of the Repository
project/
sample/
__init__.py
[Link]
tests/
test_sample.py
• Run test
$ cd project
$ python –m pytest tests/test_sample.py
Practice
$ cd pytest_tutorial $ cd pytest_tutorial
$ ls $ mkdir src
[Link] $ mkdir tests
test_calculator.py $ mv [Link] src
$ mv test_calculator.py tests
$ pytest
# edit code
$ python –m pytest tests
PyCon Korea 2019
3. Fixture
Test Fixture
Four phases of a test:
1. Set-up
2. Exercise, interacting with the system under test
3. Verify, determining whether the expected outcome has been
obtained
4. Tear down, to return to the original state
• A software test fixture sets up the system for the testing process by
providing it with all the necessary code to initialize it, thereby satisfying
whatever preconditions there may be.
• Frequently fixtures are created by handling setUp() and tearDown() events of
the unit testing framework.
pytest fixtures
• Fixtures are run pytest before the actual test functions.
• @[Link]() decorator
• Purpose
• Setup and Teardown for the tests (e.g, database)
• Test set for the tests
Practice
# [Link]
class Calculator(object):
"""Calculator class"""
def __init__(self):
pass
@staticmethod
def add(a, b):
return a + b
@staticmethod
def subtract(a, b):
return a - b
@staticmethod
def multiply(a, b):
return a * b
@staticmethod
def divide(a, b):
return a / b
# [Link] # test_calculator.py
class Calculator(object): from [Link] import add
"""Calculator class"""
def __init__(self): def test_add():
pass assert add(1, 2) == 3
@staticmethod
def add(a, b):
return a + b
@staticmethod
def subtract(a, b):
return a - b
@staticmethod
def multiply(a, b):
return a * b
@staticmethod
def divide(a, b):
return a / b
# [Link] # test_calculator.py
class Calculator(object): from [Link] import Calculator
"""Calculator class""" def test_add():
def __init__(self): calculator = Calculator()
pass assert [Link](1, 2) == 3
assert [Link](2, 2) == 4
@staticmethod
def add(a, b): def test_subtract():
return a + b calculator = Calculator()
assert [Link](5, 1) == 4
@staticmethod assert [Link](3, 2) == 1
def subtract(a, b):
return a - b def test_multiply():
calculator = Calculator()
@staticmethod assert [Link](2, 2) == 4
def multiply(a, b): assert [Link](5, 6) == 30
return a * b
def test_divide():
@staticmethod calculator = Calculator()
def divide(a, b): assert [Link](8, 2) == 4
return a / b assert [Link](9, 3) == 3
# test_calculator.py
from [Link] import Calculator
def test_add():
calculator = Calculator()
assert [Link](1, 2) == 3
assert [Link](2, 2) == 4
def test_subtract():
calculator = Calculator()
assert [Link](5, 1) == 4
assert [Link](3, 2) == 1
def test_multiply():
calculator = Calculator()
assert [Link](2, 2) == 4
assert [Link](5, 6) == 30
def test_divide():
calculator = Calculator()
assert [Link](8, 2) == 4
assert [Link](9, 3) == 3
# test_calculator.py
from [Link] import Calculator
@[Link]
def calculator():
calculator = Calculator()
return calculator
def test_add(calculator):
assert [Link](1, 2) == 3
assert [Link](2, 2) == 4
def test_subtract(calculator):
assert [Link](5, 1) == 4
assert [Link](3, 2) == 1
def test_multiply(calculator):
assert [Link](2, 2) == 4
assert [Link](5, 6) == 30
# [Link] # test_calculator.py
import pytest from [Link] import Calculator
from [Link] import @[Link]
Calculator def calculator():
calculator = Calculator()
return calculator
@[Link]
def calculator(): def test_add(calculator):
calculator = Calculator() assert [Link](1, 2) == 3
return calculator assert [Link](2, 2) == 4
def test_add_fail(calculator):
assert [Link](1, 2) != 6
assert [Link](2, 2) != 5
def test_multiply(calculator):
assert [Link](2, 2) == 4
assert [Link](5, 6) == 30
# test_calculator.py
from [Link] import Calculator
def test_add(calculator):
assert [Link](1, 2) == 3
assert [Link](2, 2) == 4
def test_add_fail(calculator):
assert [Link](1, 2) != 6
assert [Link](2, 2) != 5
PyCon Korea 2019
4. Parameterize
pytest parameterize
• Define multiple sets of arguments and fixtures at the test function or class.
• @[Link]: parametrizing test functions
• The builtin [Link] decorator
Practice
# test_calculator.py
from [Link] import Calculator
def test_add(calculator):
assert [Link](1, 2) == 3
assert [Link](2, 2) == 4
assert [Link](2, 2) == 4
[Link](argnames, argvalues)
# test_calculator.py
from [Link] import Calculator
def test_add(calculator):
assert [Link](1, 2) == 3
assert [Link](2, 2) == 4
assert [Link](9, 2) == 11
@[Link](
"a, b, expected",
[(1, 2, 3),
(2, 2, 4),
(2, 7, 11)])
def test_add_fail(calculator, a, b, expected):
assert [Link](a, b) != expected
# test_calculator.py
from [Link] import Calculator
@[Link](
"a, b, expected",
[(1, 2, 6),
(2, 2, 5),
(2, 7, 2)])
def test_add_fail(calculator, a, b, expected):
assert [Link](a, b) != expected
@[Link](rason="wrong result")
@[Link](
"a, b, expected",
[(1, 2, 6),
(2, 2, 5),
(2, 7, 2)])
def test_add_fail(calculator, a, b, expected):
assert [Link](a, b) == expected
• Buildin markers: skip, skipif and fail
• skip: enable you to skip tests you don’t want to run
• @[Link](reason=‘something’)
• @[Link](condition, reason=‘something’)
• xfail: we are telling pytest to run a test function, but we expect it to fail.
• @[Link]
# test_calculator.py
from [Link] import Calculator
@[Link](rason="wrong result")
@[Link](
"a, b, expected",
[(1, 2, 6),
(2, 2, 5),
(2, 7, 2)])
def test_add_fail(calculator, a, b, expected):
assert [Link](a, b) == expected
@[Link](
"a, b, expected",
[[Link](1, 2, 6, marks=[Link]),
[Link](2, 2, 5, marks=[Link]),
[Link](2, 7, 2, marks=[Link])])
def test_add_fail(calculator, a, b, expected):
assert [Link](a, b) == expected
# test_calculator.py
from [Link] import Calculator
@[Link](
"a, b, expected",
[(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
[Link](1, 2, 6, marks=[Link]),
[Link](2, 2, 5, marks=[Link]),
[Link](2, 7, 2, marks=[Link])])
def test_add(calculator, a, b, expected):
assert [Link](a, b) == expected
# test_calculator.py
from [Link] import Calculator
add_test_data = [
(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
[Link](1, 2, 6, marks=[Link]),
[Link](2, 2, 5, marks=[Link]),
[Link](2, 7, 2, marks=[Link])
]
@[Link](
"a, b, expected", add_test_data)
def test_add(calculator, a, b, expected):
assert [Link](a, b) == expected
• ids: optional parameter to parameterize()
• @[Link]() decorator
• [Link](<value>, id=“something”)
# test_calculator.py
from [Link] import Calculator
add_test_data = [
(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
[Link](1, 2, 6, marks=[Link]),
[Link](2, 2, 5, marks=[Link]),
[Link](2, 7, 2, marks=[Link])]
@[Link](
"a, b, expected", add_test_data, ids=[
"1 add 2 is 3",
"2 add 2 is 4",
"2 add 7 is 9",
"1 add 2 is not 6",
"2 add 2 is not 5",
"2 add 7 is not 2"] )
def test_add(calculator, a, b, expected):
assert [Link](a, b) == expected
# test_calculator.py
from [Link] import Calculator
add_test_data = [
(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
[Link](1, 2, 6, marks=[Link]),
[Link](2, 2, 5, marks=[Link]),
[Link](2, 7, 2, marks=[Link])]
@[Link](
"a, b, expected", add_test_data, ids=[
"1 add 2 is 3",
"2 add 2 is 4",
"2 add 7 is 9",
"1 add 2 is not 6",
"2 add 2 is not 5",
"2 add 7 is not 2"] )
def test_add(calculator, a, b, expected):
assert [Link](a, b) == expected
# test_calculator.py
from [Link] import Calculator
subtract_test_data = [
[Link](5, 1, 4, id="5 subtract 1 is 4"),
[Link](3, 2, 1, id="3 subtract 2 is 1"),
[Link](10, 2, 8, id="10 subtract 2 is 8"),
[Link](5, 1, 6, marks=[Link], id="5 subtract 1 is 6"),
[Link](3, 2, 2, marks=[Link], id="3 subtract 2 is 2"),
[Link](10, 2, 1, marks=[Link], id="10 subtract 2 is 1")
]
@[Link](
"a, b, expected", subtract_test_data
)
def test_subtract(calculator, a, b, expected):
assert [Link](a, b) == expected
PyCon Korea 2019
5. Mock
pytest mock
• Thirty-party pytest plugin
• pytest-mock
• Swapping out part of the system to isolate bits of code
• Mock objects: test doubles, spies, fakes, or stubs..
• [Link]
• [Link]
Practice
• Installation
$ (venv) pip install pytest-mock
def test_add_with_mocker1(mocker, calculator):
"""Test functionality of add."""
[Link](calculator, 'add', return_value=5)
assert [Link](1, 2) is 5
assert [Link](2, 2) is 5
def test_add_with_mocker2(mocker, calculator):
"""Test functionality of add."""
[Link](calculator, 'add', side_effect=[1, 2])
assert [Link](1, 2) is 1
assert [Link](2, 2) is 2
def test_add_with_mocker3(mocker, calculator):
"""Test functionality of add."""
[Link](calculator, 'add', side_effect=ZeroDivisionError())
with [Link](ZeroDivisionError):
[Link](1, 2)
# [Link]
import logging
[Link](level=[Link])
class Calculator():
"""Calculator class"""
def __init__(self):
[Link] = [Link](self.__class__.__name__)
def add(self, a, b):
[Link](
"add {a} to {b} is {result}".format(
a=a, b=b, result=a + b
))
return a + b
@[Link](
"a, b, expected", add_test_data
)
# mocker: The mocker fixture is provided by the pytest-mock plugin
def test_add_spy_logger(mocker, calculator, a, b, expected):
spy_info = [Link]([Link], "info")
assert [Link](a, b) == expected
assert spy_info.called
assert spy_info.call_count == 1
calls = [[Link]("add {a} to {b} is {expected}".format(
a=a, b=b, expected=expected
))]
assert spy_info.call_args_list == calls
References
• [Link]
• [Link]
• [Link]
test-directory-structure
• [Link]
• [Link]