Sie sind auf Seite 1von 49

PyCon Korea 2019

pytest로 파이썬 코드 테스트하기

최영선
yeongseon.choe@gmail.com
PyCon Korea 2019
pytest
Why we should test code?
• Modify Code with Confidence

• Identify Bugs Early

• Improve System Design

https://alysivji.github.io/testing-101-introduction-to-testing.html#benefits-of-
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

• https://github.com/pallets/flask
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) py.test test_sample.py

• (venv) py.test tests/test_sample.py

• (venv) py.test tests/


Practice
# calculator.py

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
# calculator.py # 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 pytest.raises(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
sample.py
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
calculator.py $ mkdir tests
test_calculator.py $ mv caclculator.py 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.

• @pytest.fixture() decorator

• Purpose

• Setup and Teardown for the tests (e.g, database)

• Test set for the tests


Practice
# calculator.py
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
# calculator.py # test_calculator.py
class Calculator(object): from src.calculator 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
# calculator.py # test_calculator.py
class Calculator(object): from src.calculator import Calculator
"""Calculator class""" def test_add():
def __init__(self): calculator = Calculator()
pass assert calculator.add(1, 2) == 3
assert calculator.add(2, 2) == 4
@staticmethod
def add(a, b): def test_subtract():
return a + b calculator = Calculator()
assert calculator.subtract(5, 1) == 4
@staticmethod assert calculator.subtract(3, 2) == 1
def subtract(a, b):
return a - b def test_multiply():
calculator = Calculator()
@staticmethod assert calculator.multiply(2, 2) == 4
def multiply(a, b): assert calculator.multiply(5, 6) == 30
return a * b
def test_divide():
@staticmethod calculator = Calculator()
def divide(a, b): assert calculator.divide(8, 2) == 4
return a / b assert calculator.divide(9, 3) == 3
# test_calculator.py
from src.calculator import Calculator
def test_add():
calculator = Calculator()
assert calculator.add(1, 2) == 3
assert calculator.add(2, 2) == 4

def test_subtract():
calculator = Calculator()
assert calculator.subtract(5, 1) == 4
assert calculator.subtract(3, 2) == 1

def test_multiply():
calculator = Calculator()
assert calculator.multiply(2, 2) == 4
assert calculator.multiply(5, 6) == 30

def test_divide():
calculator = Calculator()
assert calculator.divide(8, 2) == 4
assert calculator.divide(9, 3) == 3
# test_calculator.py

from src.calculator import Calculator

@pytest.fixture
def calculator():
calculator = Calculator()
return calculator

def test_add(calculator):
assert calculator.add(1, 2) == 3
assert calculator.add(2, 2) == 4

def test_subtract(calculator):
assert calculator.subtract(5, 1) == 4
assert calculator.subtract(3, 2) == 1

def test_multiply(calculator):
assert calculator.multiply(2, 2) == 4
assert calculator.multiply(5, 6) == 30
# confest.py # test_calculator.py

import pytest from src.calculator import Calculator

from src.calculator import @pytest.fixture


Calculator def calculator():
calculator = Calculator()
return calculator
@pytest.fixture
def calculator(): def test_add(calculator):
calculator = Calculator() assert calculator.add(1, 2) == 3
return calculator assert calculator.add(2, 2) == 4

def test_add_fail(calculator):
assert calculator.add(1, 2) != 6
assert calculator.add(2, 2) != 5

def test_multiply(calculator):
assert calculator.multiply(2, 2) == 4
assert calculator.multiply(5, 6) == 30
# test_calculator.py

from src.calculator import Calculator

def test_add(calculator):
assert calculator.add(1, 2) == 3
assert calculator.add(2, 2) == 4

def test_add_fail(calculator):
assert calculator.add(1, 2) != 6
assert calculator.add(2, 2) != 5
PyCon Korea 2019
4. Parameterize
pytest parameterize
• Define multiple sets of arguments and fixtures at the test function or class.

• @pytest.mark.parametrize: parametrizing test functions

• The builtin pytest.mark.parametrize decorator


Practice
# test_calculator.py

from src.calculator import Calculator

def test_add(calculator):
assert calculator.add(1, 2) == 3
assert calculator.add(2, 2) == 4
assert calculator.add(2, 2) == 4

pytest.mark.parametrize(argnames, argvalues)
# test_calculator.py

from src.calculator import Calculator

def test_add(calculator):
assert calculator.add(1, 2) == 3
assert calculator.add(2, 2) == 4
assert calculator.add(9, 2) == 11

@pytest.mark.parametrize(
"a, b, expected",
[(1, 2, 3),
(2, 2, 4),
(2, 7, 11)])
def test_add_fail(calculator, a, b, expected):
assert calculator.add(a, b) != expected
# test_calculator.py

from src.calculator import Calculator

@pytest.mark.parametrize(
"a, b, expected",
[(1, 2, 6),
(2, 2, 5),
(2, 7, 2)])
def test_add_fail(calculator, a, b, expected):
assert calculator.add(a, b) != expected

@pytest.mark.xfail(rason="wrong result")
@pytest.mark.parametrize(
"a, b, expected",
[(1, 2, 6),
(2, 2, 5),
(2, 7, 2)])
def test_add_fail(calculator, a, b, expected):
assert calculator.add(a, b) == expected
• Buildin markers: skip, skipif and fail

• skip: enable you to skip tests you don’t want to run

• @pytest.mark.skip(reason=‘something’)

• @pytest.mark.skipif(condition, reason=‘something’)

• xfail: we are telling pytest to run a test function, but we expect it to fail.

• @pytest.mark.fail
# test_calculator.py

from src.calculator import Calculator

@pytest.mark.xfail(rason="wrong result")
@pytest.mark.parametrize(
"a, b, expected",
[(1, 2, 6),
(2, 2, 5),
(2, 7, 2)])
def test_add_fail(calculator, a, b, expected):
assert calculator.add(a, b) == expected

@pytest.mark.parametrize(
"a, b, expected",
[pytest.param(1, 2, 6, marks=pytest.mark.xfail),
pytest.param(2, 2, 5, marks=pytest.mark.xfail),
pytest.param(2, 7, 2, marks=pytest.mark.xfail)])
def test_add_fail(calculator, a, b, expected):
assert calculator.add(a, b) == expected
# test_calculator.py

from src.calculator import Calculator

@pytest.mark.parametrize(
"a, b, expected",
[(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
pytest.param(1, 2, 6, marks=pytest.mark.xfail),
pytest.param(2, 2, 5, marks=pytest.mark.xfail),
pytest.param(2, 7, 2, marks=pytest.mark.xfail)])
def test_add(calculator, a, b, expected):
assert calculator.add(a, b) == expected
# test_calculator.py

from src.calculator import Calculator

add_test_data = [
(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
pytest.param(1, 2, 6, marks=pytest.mark.xfail),
pytest.param(2, 2, 5, marks=pytest.mark.xfail),
pytest.param(2, 7, 2, marks=pytest.mark.xfail)
]

@pytest.mark.parametrize(
"a, b, expected", add_test_data)
def test_add(calculator, a, b, expected):
assert calculator.add(a, b) == expected
• ids: optional parameter to parameterize()

• @pytest.mark.parameterize() decorator

• pytest.param(<value>, id=“something”)
# test_calculator.py

from src.calculator import Calculator


add_test_data = [
(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
pytest.param(1, 2, 6, marks=pytest.mark.xfail),
pytest.param(2, 2, 5, marks=pytest.mark.xfail),
pytest.param(2, 7, 2, marks=pytest.mark.xfail)]

@pytest.mark.parametrize(
"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 calculator.add(a, b) == expected
# test_calculator.py

from src.calculator import Calculator


add_test_data = [
(1, 2, 3),
(2, 2, 4),
(2, 7, 9),
pytest.param(1, 2, 6, marks=pytest.mark.xfail),
pytest.param(2, 2, 5, marks=pytest.mark.xfail),
pytest.param(2, 7, 2, marks=pytest.mark.xfail)]

@pytest.mark.parametrize(
"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 calculator.add(a, b) == expected
# test_calculator.py

from src.calculator import Calculator

subtract_test_data = [
pytest.param(5, 1, 4, id="5 subtract 1 is 4"),
pytest.param(3, 2, 1, id="3 subtract 2 is 1"),
pytest.param(10, 2, 8, id="10 subtract 2 is 8"),
pytest.param(5, 1, 6, marks=pytest.mark.xfail, id="5 subtract 1 is 6"),
pytest.param(3, 2, 2, marks=pytest.mark.xfail, id="3 subtract 2 is 2"),
pytest.param(10, 2, 1, marks=pytest.mark.xfail, id="10 subtract 2 is 1")
]

@pytest.mark.parametrize(
"a, b, expected", subtract_test_data
)
def test_subtract(calculator, a, b, expected):
assert calculator.subtract(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..

• mock.patch

• mock.patch.object
Practice
• Installation
$ (venv) pip install pytest-mock
def test_add_with_mocker1(mocker, calculator):
"""Test functionality of add."""
mocker.patch.object(calculator, 'add', return_value=5)
assert calculator.add(1, 2) is 5
assert calculator.add(2, 2) is 5

def test_add_with_mocker2(mocker, calculator):


"""Test functionality of add."""
mocker.patch.object(calculator, 'add', side_effect=[1, 2])
assert calculator.add(1, 2) is 1
assert calculator.add(2, 2) is 2

def test_add_with_mocker3(mocker, calculator):


"""Test functionality of add."""
mocker.patch.object(calculator, 'add', side_effect=ZeroDivisionError())
with pytest.raises(ZeroDivisionError):
calculator.add(1, 2)
# calculator.py
import logging

logging.basicConfig(level=logging.INFO)

class Calculator():
"""Calculator class"""

def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)

def add(self, a, b):


self.logger.info(
"add {a} to {b} is {result}".format(
a=a, b=b, result=a + b
))
return a + b
@pytest.mark.parametrize(
"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 = mocker.spy(calculator.logger, "info")
assert calculator.add(a, b) == expected
assert spy_info.called
assert spy_info.call_count == 1

calls = [mocker.call("add {a} to {b} is {expected}".format(


a=a, b=b, expected=expected
))]
assert spy_info.call_args_list == calls
References
• https://docs.pytest.org/en/latest/

• https://docs.python-guide.org/writing/structure/

• https://stackoverflow.com/questions/1896918/running-unittest-with-typical-
test-directory-structure

• https://www.slideshare.net/soasme/pytest

• https://github.com/pytest-dev/pytest-mock/

Das könnte Ihnen auch gefallen