I want to write in Python! (3) Utilize the mock

Hello. This is leo1109. This time as well, following the previous, we will talk about testing.

The code used in the article has been uploaded on GitHub.

The story to introduce this time

It's about writing a test. Use patch.

Create a module for testing

We have improved the previous script and added the following features.

--Get Fibonacci number --Get the sum of consecutive values sequence --Get the difference between the Fibonacci number and the sum of the continuous sequences --Get the value of the current time (UTC), which is the remainder of the input value.

In addition, the acquisition of the Fibonacci number and the acquisition of the sum are changed to the implementation as defined.


# python 3.5.2

import time
from functools import wraps


def required_natural_number(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        k = args[0]
        if isinstance(k, bool):
            raise TypeError
        if not isinstance(k, int) or k < 1:
            raise TypeError
        return func(*args, **kwargs)
    return wrapper


def _f(k):
    if k == 1 or k == 2:
        return 1
    else:
        return _f(k-1) + _f(k-2)


def _sum(k):
    if k == 1:
        return 1
    else:
        return k + _sum(k-1)


@required_natural_number
def fibonacci(k):
    return _f(k)


@required_natural_number
def sum_all(k):
    return _sum(k)


@required_natural_number
def delta_of_sum_all_and_fibonacci(k):
    x = sum_all(k) - fibonacci(k)
    return x if x >= 0 else -x


@required_natural_number
def surplus_time_by(k):
    return int(time.time() % k)

Write a test immediately

Add a test for sum_all_minus_fibonacci (x) to get the difference between the sum and the Fibonacci number. I chose the test case values appropriately.

my_math_2_test.py


# python 3.5.2

from unittest.mock import patch

import pytest

import my_math


class TestFibonacci:
    def test(self):
        assert my_math.fibonacci(1) == 1
        assert my_math.fibonacci(5) == 5
        assert my_math.fibonacci(10) == 55
        assert my_math.fibonacci(20) == 6765
        assert my_math.fibonacci(30) == 832040
        assert my_math.fibonacci(35) == 9227465


class TestSumAll:
    def test(self):
        assert my_math.sum_all(1) == 1
        assert my_math.sum_all(5) == 15
        assert my_math.sum_all(10) == 55
        assert my_math.sum_all(20) == 210
        assert my_math.sum_all(30) == 465
        assert my_math.sum_all(35) == 630


class TestDeltaOfSumAllAndFibonacci:

    def test(self):
        assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
        assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
        assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
        assert my_math.delta_of_sum_all_and_fibonacci(20) == -1 * (210 - 6765)
        assert my_math.delta_of_sum_all_and_fibonacci(30) == -1 * (465 - 832040)
        assert my_math.delta_of_sum_all_and_fibonacci(35) == -1 * (630 - 9227465)

Let's run the test.


$ pytest -v  my_math_2_test.py
================================================================== test session starts ==================================================================
collected 3 items

my_math_2_test.py::TestFibonacci::test PASSED
my_math_2_test.py::TestSumAll::test PASSED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test PASSED

=============================================================== 3 passed in 21.01 seconds ===============================================================

It took 20 seconds. It seems that it takes about 10 seconds each to test the Fibonacci number and the difference.

The cause is that the method to get the Fibonacci number was recursively rewritten. (Since the purpose of this article is a test, I will omit the explanation about the calculation time, but the implementation using recursion may take a very large amount of calculation time.)

However, this test actually involves unnecessary processing other than implementation problems. By fixing that point, you can speed up the test.

Patch the function

It is an image to paste from above.

Patch slow functions

The reason for the slow test is the method to get the Fibonacci number, so I would like to avoid executing it as much as possible. Looking at the test content, you can see that the fibonacci (x) test and the delta_of_sum_all_and_fibonacci (x) both call fibonacci (x) with the same arguments. ... this is a bit of a waste. Since fibonacci (x) has been tested on its own, I don't want to run it when testingdelta_of_sum_all_and_fibonacci (x), if possible. (Because it takes time) This time, let's use patch and try not to executefibonacci (x).

After skipping the existing test, I added a test that patchedfibonacci (x).

class TestDeltaOfSumAllAndFibonacci:
    @pytest.mark.skip
    def test_slow(self):
        assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
        assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
        assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
        assert my_math.delta_of_sum_all_and_fibonacci(20) \
            == -1 * (210 - 6765)
        assert my_math.delta_of_sum_all_and_fibonacci(30) \
            == -1 * (465 - 832040)
        assert my_math.delta_of_sum_all_and_fibonacci(35) \
            == -1 * (630 - 9227465)

    def test_patch(self):
        with patch('my_math.fibonacci') as mock_fib:
            mock_fib.return_value = 1
            assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
            assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
            assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
            assert my_math.delta_of_sum_all_and_fibonacci(20) \
                == -1 * (210 - 6765)
            assert my_math.delta_of_sum_all_and_fibonacci(30) \
                == -1 * (465 - 832040)
            assert my_math.delta_of_sum_all_and_fibonacci(35) \
                == -1 * (630 - 9227465)

patch uses the with statement and decorators to rewrite the method execution content of the specified module. As a confirmation of operation, fibonacci (x) always returns 1. Let's run it.


$ pytest -v my_math_2_test.py::TestDeltaOfSumAllAndFibonacci
================================================================== test session starts ==================================================================
collected 2 items

my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_slow SKIPPED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_patch FAILED

======================================================================= FAILURES ========================================================================
_______________________________________________________ TestDeltaOfSumAllAndFibonacci.test_patch ________________________________________________________


self = <my_math_2_test.TestDeltaOfSumAllAndFibonacci object at 0x103abcf28>

    def test_patch(self):
        with patch('my_math.fibonacci') as mock_fib:
            mock_fib.return_value = 1
            assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
>           assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
E           assert 14 == (15 - 5)
E            +  where 14 = <function delta_of_sum_all_and_fibonacci at 0x103ac0840>(5)
E            +    where <function delta_of_sum_all_and_fibonacci at 0x103ac0840> = my_math.delta_of_sum_all_and_fibonacci

my_math_2_test.py:45: AssertionError
========================================================== 1 failed, 1 skipped in 0.21 seconds ==========================================================

The test now fails. In the test case of delta_of_sum_all_and_fibonacci (5),fibonacci (5), which should get 5 originally, now returns 1, so the test fails as14 == (15 --5). I will.

Now that we've confirmed that the patch is working as expected, we'll improve the test to return the correct value.

    def test_patch(self):
        with patch('my_math.fibonacci') as mock_fib:
            mock_fib.return_value = 1
            assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
            mock_fib.return_value = 5
            assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
            mock_fib.return_value = 55
            assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
            mock_fib.return_value = 6765
            assert my_math.delta_of_sum_all_and_fibonacci(20) \
                == -1 * (210 - 6765)
            mock_fib.return_value = 832040
            assert my_math.delta_of_sum_all_and_fibonacci(30) \
                == -1 * (465 - 832040)
            mock_fib.return_value = 9227465
            assert my_math.delta_of_sum_all_and_fibonacci(35) \
                == -1 * (630 - 9227465)

Try again.

$ pytest -v my_math_2_test.py::TestDeltaOfSumAllAndFibonacci
================================================================== test session starts ==================================================================
collected 2 items

my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_slow SKIPPED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_patch PASSED

========================================================== 1 passed, 1 skipped in 0.05 seconds ==========================================================

The test was successful. The execution time is 0.05 (sec), which is considerably faster than before. Of course, if you execute all, it will take as much time as the first fibonacci (x) to be executed, but even so, it was 20 seconds-> 10 seconds, which was a 50% reduction.

Patch a function whose return value is uncertain

In the previous section, by mocking a slow function. We have succeeded in streamlining the test. By the way, surplus_time_by (k) is a function that gets the unix time of the current time (UTC) and returns the remainder, but since the current time changes every time it is executed, set a fixed value as before. I can't do the test I wrote.

For example, the following test will succeed if the value of time.time () is a multiple of 5, but not otherwise.


   assert my_math.surplus_time_by(5) == 5

In these cases, consider patching time.time (). Because time is Python's standard library, so assuming it's already well tested, it's part of the modulo calculation.

Now, to patch the time.time () used in my_math, write:

class TestSurplusTimeBy:
    def test(self):
        with patch('my_math.time') as mock_time:
            mock_time.time.return_value = 1000
            assert my_math.surplus_time_by(3) == 1
            assert my_math.surplus_time_by(5) == 0
            mock_time.time.return_value = 1001
            assert my_math.surplus_time_by(3) == 2
            assert my_math.surplus_time_by(5) == 1

It is also possible to patch up to time.time as shown below.

    def test2(self):
        with patch('my_math.time.time') as mock_time:
            mock_time.return_value = 1000
            assert my_math.surplus_time_by(3) == 1
            assert my_math.surplus_time_by(5) == 0
            mock_time.return_value = 1001
            assert my_math.surplus_time_by(3) == 2
            assert my_math.surplus_time_by(5) == 1

The difference above is in the extent to which it is mocked. Looking at the execution results below, you can see that when time is patched, the time module itself is rewritten as a mock object. So, for example, if you are using something other than time () under time, you need to explicitly specify time.time or patch all the methods you are using.

# time.patch time
>>> with patch('my_math.time.time') as m:
...   print(my_math.time)
...   print(my_math.time.time)
...
<module 'time' (built-in)>
<MagicMock name='time' id='4329989904'>

#patch time
>>> with patch('my_math.time') as m:
...   print(my_math.time)
...   print(my_math.time.time)
...
<MagicMock name='time' id='4330034680'>
<MagicMock name='time.time' id='4330019136'>

About the character string specified in patch

In this example, the module time is imported in my_math, so write it as my_math.time. Therefore, if another module my_module that uses my_math appears, you need to write my_module.my_math.time.

When patching an instance method of a class, you must even specify the instance.

my_class.py


# Python 3.5.2


class MyClass:
    def __init__(self, prefix='My'):
        self._prefix = prefix

    def my_method(self, x):
        return '{} {}'.format(self._prefix, x)

my_main.py


# Python 3.5.2

from my_class import MyClass


def main(name):
    c = MyClass()
    return c.my_method(name)


if __name__ == '__main__':
    print(main('Python'))

After running the above, you will see My Python. Now, if you want to patch MyClass.my_method, you can write:

my_main_test.py


# python 3.5.2

from unittest.mock import patch

import my_main


class TestFunction:
    def test(self):
        assert my_main.function('Python') == 'My Python'

    def test_patch_return_value(self):
        with patch('my_class.MyClass.my_method') as mock:
            mock.return_value = 'Hi! Perl'
            assert my_main.function('Python') == 'Hi! Perl'

    def test_patch_side_effect(self):
        with patch('my_class.MyClass.my_method') as mock:
            mock.side_effect = lambda x: 'OLA! {}'.format(x)
            assert my_main.function('Python') == 'OLA! Python'

Patch methods for external access

Patches are also useful when testing modules that are accessed externally. Using the requests module, I created a method that returns the status code when the specified URL is GET. Consider testing this.


# Python 3.5.2

import requests


def get_status_code(url):
    r = requests.get(url)

    return r.status_code

I will write a test.

# python 3.5.2

import pytest

import my_http


class TestGetStatusCode:
    @pytest.fixture
    def url(self):
        return 'http://example.com'


    def test(self, url):
        assert my_http.get_status_code(url) == 200

This test passes without any problems. But what about when you're offline? I'll omit the detailed stack trace, but the test will fail.


...
>           raise ConnectionError(e, request=request)
E           requests.exceptions.ConnectionError: HTTPConnectionPool(host='example.com', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x1039a4cf8>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known',))

../../../.pyenv/versions/3.5.2/lib/python3.5/site-packages/requests/adapters.py:504: ConnectionError                                               =============================================================== 1 failed in 1.06 seconds ================================================================

To make this test pass offline, patch request.get. Since the return value is an object, you need to explicitly specify MagicMock () in return_value.


# python 3.5.2

from unittest.mock import MagicMock, patch

import pytest

import my_http


class TestGetStatusCode:
    @pytest.fixture
    def url(self):
        return 'http://example.com'

    @pytest.mark.skip
    def test_online(self, url):
        assert my_http.get_status_code(url) == 200

    def test_offline(self, url):
        with patch('my_http.requests') as mock_requests:
            mock_response = MagicMock(status_code=200)
            mock_requests.get.return_value = mock_response

            assert my_http.get_status_code(url) == 200

The above test will pass even offline. If you specify the Status_code of MagicMock as 400, 500, etc., you can add an error case test while keeping the URL the same. It can be used when mocking DB access in an environment without a database or token acquisition from an external service.

Patch Tips

Here are some tips for patching.

If you want to patch multiple methods

In Python3 series, ʻExitStack ()` is convenient.


>>> with ExitStack() as stack:
...   x = stack.enter_context(patch('my_math.fibonacci'))
...   y = stack.enter_context(patch('my_math.sum_all'))
...   x.return_value = 100
...   y.return_value = 200
...   z = my_math.delta_of_sum_all_and_fibonacci(99999)
...   print(z)
...
100

Since the return values of both functions have been rewritten, my_math.delta_of_sum_all_and_fibonacci (99999) returns the difference between 200 and 100.

I want to return an exception

When using a mock, you may want to return an exception. In that case, use mock.side_effect.


>>> with patch('my_math.fibonacci') as m:
...   m.side_effect = ValueError
...   my_math.fibonacci(1)
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File "/Users/sho/.pyenv/versions/3.5.2/lib/python3.5/unittest/mock.py", line 917, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/Users/sho/.pyenv/versions/3.5.2/lib/python3.5/unittest/mock.py", line 973, in _mock_call
    raise effect
ValueError

Check if the mock has been executed

Since the call object is stored in the mock object, it is convenient to check it. You don't need to write ʻassert because methods starting with ʻassert_ will raise an exception if the condition is not met. The arguments when the mock is executed are stored as tuples in mock.call_args. See also the official documentation for more details.

Only typical ones will be introduced.

--mock.called Whether it was executed (bool) --mock.call_count Number of executions --Whether it was executed with the mock.assert_called_with () argument


   def test_called(self):
        with patch('my_class.MyClass.my_method') as mock:
            my_main.function('Python')

        assert mock.called
        assert mock.call_count == 1
        mock.assert_called_with('Python')

next time

It is undecided until the end of the Obon holiday!

Recommended Posts

I want to write in Python! (3) Utilize the mock
I want to display the progress in Python!
I want to write in Python! (1) Code format check
I want to write in Python! (2) Let's write a test
I want to use the R dataset in python
I didn't want to write the AWS key in the program
I wrote the code to write the code of Brainf * ck in python
I want to do Dunnett's test in Python
I want to create a window in Python
The trick to write flatten concisely in python
I want to write to a file with Python
I want to batch convert the result of "string" .split () in Python
I want to explain the abstract class (ABCmeta) of Python in detail.
I tried to graph the packages installed in Python
I want to easily implement a timeout in python
Even in JavaScript, I want to see Python `range ()`!
I want to randomly sample a file in Python
I want to inherit to the back with python dataclass
I want to work with a robot in python.
I want to do something in Python when I finish
I want to manipulate strings in Kotlin like Python!
[Python] I want to know the variables in the function when an error occurs!
I want to use Python in the environment of pyenv + pipenv on Windows 10
I want to get the file name, line number, and function name in Python 3.4
In the python command python points to python3.8
I wrote the queue in Python
I want to debug with Python
I wrote the stack in Python
I want to initialize if the value is empty (python)
maya Python I want to fix the baked animation again.
I want to do something like sort uniq in Python
[Python] I want to use the -h option with argparse
I tried to implement the mail sending function in Python
I want to know the features of Python and pip
I want to make the Dictionary type in the List unique
I want to align the significant figures in the Numpy array
I want to be able to run Python in VS Code
I want to make input () a nice complement in python
I just want to find the 95% confidence interval for the difference in population ratios in Python
I want to write a triple loop and conditional branch in one line in python
I want to pin Spyder to the taskbar
I tried to implement PLSA in Python
I want to output to the console coolly
I want to know the weather with LINE bot feat.Heroku + Python
I tried to implement permutation in Python
I want to print in a comprehension
[Linux] I want to know the date when the user logged in
I want to handle the rhyme part1
Write the test in a python docstring
I want to solve APG4b with Python (only 4.01 and 4.04 in Chapter 4)
The 15th offline real-time how to write reference problem in Python
I want to handle the rhyme part3
I want to output the beginning of the next month with Python
I want to use jar from python
I want to build a Python environment
I want to run the Python GUI when starting Raspberry Pi
I want to analyze logs with Python
LINEbot development, I want to check the operation in the local environment
I want to play with aws with python
[Python / AWS Lambda layers] I want to reuse only module in AWS Lambda Layers
I tried to implement ADALINE in Python