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.
It's about writing a test. Use patch.
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)
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.
It is an image to paste from above.
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 patch
edfibonacci (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.
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'>
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'
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.
Here are some tips for patching.
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.
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
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')
It is undecided until the end of the Obon holiday!
Recommended Posts