Scott's Blog

学则不固, 知则不惑

0%

Test in Python

Automated testing is a hot topic especially in CI/CD, there are many tools, today let's talk about 'pytest'.

Pytest features:

- Test code more readable
- Support assert statements
- Compare with unitest, Pytest Updated more frequently
- Have a consistent style across all Python project (like Django and Flask)

Automated tests should be:

- Fast
- Isolated/independent
- Deterministic/repeatable

Mocking

Before we explain mocking in Python, you need understand what is **args and **kwargs. Both keywords are special syntax in Python that allows a function to accept a variable number of keyword arguments - **args, which you can use it(args) as a list inside a function - **kwargs which you can use it(kwargs) as a dict inside a function

1
2
3
4
5
6
7
8
9
10
11
def args_func(*args):
for arg in args:
print(arg)

args_func([1, 2, 3], [4, 5, 6])

def kwargs_func(**kwargs):
for k, v in kwargs.items():
print(k, v)
kwargs_func(name="scott", age=27)

Here is two mocking examples that replace a attribute and a method in runtime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Mock a attribute
def test_my_function_mock_attr(monkeypatch):
class MyClass:
my_attr = 42

def mock_my_attr():
return 99

monkeypatch.setattr(MyClass, "my_attr", mock_my_attr)

assert MyClass().my_attr == 99

# Mock a function
def test_my_function_mock_method(monkeypatch):
class MyClass:
def my_method(self):
return 42

def mock_my_method():
return 99

monkeypatch.setattr(MyClass, "my_method", mock_my_method)

assert MyClass().my_attr == 99

pytest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from unittest import mock

import requests
from requests import Response


def get_my_ip():
response = requests.get(
'http://ipinfo.io/json'
)
return response.json()['ip']


def test_get_my_ip(monkeypatch):
my_ip = '123.123.123.123'
# creates a mock object with the same properties and methods
# as the object passed as a parameter
response = mock.create_autospec(Response)
response.json.return_value = {'ip': my_ip}

# setattr method allows you to dynamically replace an attribute
# or method of an object with a new value or method
monkeypatch.setattr(
requests,
'get',
lambda *args, **kwargs: response
)

assert get_my_ip() == my_ip

Code Coverage

1
2
3
4
5
6
Code coverage is a metric which tells you the ratio between the executed lines code(during test) and total lines code.

there is a plugin for this: `pytest-cov`, once installed, to run tests with coverage reporting, add the `--cov` option:

```shell
python -m pytest --cov=.

In the output:

  • Stmts, number of lines of code
  • Miss, number of lines that weren't excuted by the test
  • Cover, Coverage percentage
    1
    2
    3

    # Muatation Testing

    Muatation means the tools will iterates through each line of you code, making small changes to test effectiveness or robustness.

For example, following code:

1
2
3
4
if x > y:
z = 50
else:
z = 100

mutation tool may change the operotor from > to >= like so:

1
2
3
4
if x >= y:
z = 50
else:
z = 100

Mutation testing tools for Python are not as mature as some of the others out there.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# Hypothesis

Hypothesis testing generates a wide-range of random data that's dependent on previous tests runs.


```python
def increment(num: int) -> int:
return num + 1

# You need many test data, example here.
# if you don't write enought code, then you test coverage isn't enought
import pytest
@pytest.mark.parametrize(
'number, result',
[
(-2, -1),
(0, 1),
(3, 4),
(101234, 101235),
]
)
def test_increment(number, result):
assert increment(number) == result


# with hypothesis tools, they can generate data for you to test
from hypothesis import given
import hypothesis.strategies as st


@given(st.integers())
def test_add_one(num):
assert increment(num) == num - 1

Type Checking

Tests are code, You do need maintain and refactor them, but remember to keep you tests short, simple and straight.

Don't Over test your code.

Runtime (or dynamic) type checkers, like Typeguard and pydantic, can help to minimize the number of tests. Let's take a look at an example of this with pydantic.

you need to install it first: # !pip install pydantic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User:

def __init__(self, email: str):
self.email = email


user = User(email='john@doe.com')
user.email

from pydantic import BaseModel, EmailStr


class User(BaseModel):
email: EmailStr


# will be validated by pydantic before every new User instance is created
user = User(email='john@doe.com')