2018年1月16日 星期二

[ Python 文章收集 ] Pytest - Create Tests via Parametrization

Source From Here 
User Case 
Imagine you want to write a test for a particular function, but for multiple input values. Writing a for-loop is a bad idea as the test will fail as soon as it hits the first AssertionError. Subsequent input values will not be tested and you have no idea which part of your code is actually broken. At the same time you want to stick to DRY and not implement the same unittest.Testcase method over and over again with slightly different input values. 

Keep in mind why we write unit tests: 
We want to know when we break stuff, but also at the same time get as many hints as possible on why the error occurs!

Pytest provides various ways of creating individual test items. There are parametrized fixtures and mark.parametrize (and hooks). 

Marker 
Using this built-in marker @pytest.mark.parametrize you do not need to implement any fixtures. Instead you define your scenarios in a decorator and the only thing you really need to look out for is to match the number of positional test arguments with your iterable. 
- test1.py 
  1. import pytest  
  2.   
  3. @pytest.mark.parametrize(  
  4.     'number, word', [  
  5.         (1'1'),  
  6.         (3'Fizz'),  
  7.         (5'Buzz'),  
  8.         (10'Buzz'),  
  9.         (15'FizzBuzz'),  
  10.         (16'16')  
  11.     ]  
  12. )  
  13. def test_fizzbuzz(number, word):  
  14.     assert fizzbuzz(number) == word  
Fixture 
To parametrize a fixture you need pass an iterable to the params keyword argument. The built-in fixture request knows about the current parameter and if you don't want to do anything fancy, you can pass it right to the test via the returnstatement. 
- test2.py 
  1. import pytest  
  2.   
  3. @pytest.fixture(params=[('1+1'2), ('1*3'3), ('1+2*3'7)])  
  4. def calc(request):  
  5.     return request.param  
  6.   
  7. def test_calc(calc):  
  8.     print("{}".format(str(calc)))  
  9.     assert eval(calc[0]) == calc[1]  
Then you can test it this way: 
# pytest -s test2.py
...
=== 3 passed in 0.01 seconds


Example Implementation 
Sometimes you may find yourself struggling to chose which is the best way to parametrize your tests. At the end of the day it really depends on what you want to test. But... Good news! Pytest lets you combine both methods to get the most out of both worlds. 

Some Classes in a Module 
Imagine this Python module (foobar.py) which contains a few class definitions with a bit of logic: 
  1. # -*- coding: utf-8 -*-  
  2.   
  3. FOSS_LICENSES = ['Apache 2.0''MIT''GPL''BSD']  
  4.   
  5. PYTHON_PKGS = ['pytest''requests''django''cookiecutter']  
  6.   
  7.   
  8. class Package:  
  9.     def __init__(self, name, license):  
  10.         self.name = name  
  11.         self.license = license  
  12.   
  13.     @property  
  14.     def is_open_source(self):  
  15.         return self.license in FOSS_LICENSES  
  16.   
  17.   
  18. class Person:  
  19.     def __init__(self, name, gender):  
  20.         self.name = name  
  21.         self.gender = gender  
  22.         self._skills = ['eating''sleeping']  
  23.   
  24.     def learn(self, skill):  
  25.         self._skills.append(skill)  
  26.   
  27.     @property  
  28.     def looks_like_a_programmer(self):  
  29.         return any(  
  30.             package in self._skills  
  31.             for package in PYTHON_PKGS  
  32.         )  
  33.   
  34.   
  35. class Woman(Person):  
  36.     def __init__(self, name):  
  37.         super().__init__(name, 'female')  
  38.   
  39.   
  40. class Man(Person):  
  41.     def __init__(self, name):  
  42.         super().__init__(name, 'male')  
Tests in a Separate Module 
With only two few lines of pytest code, we can create loads of different scenarios that we would like to test. By re-using parametrized fixtures and applying the aforementioned markers to your tests, you can focus on the actual test implementation, as opposed to writing the same boilerplate code for each of the methods that you would have to write with unittest.Testcase
- test3.py 
  1. # -*- coding: utf-8 -*-  
  2.   
  3. import operator  
  4. import pytest  
  5.   
  6. from foobar import Package, Woman, Man  
  7.   
  8. PACKAGES = [  
  9.     Package('requests''Apache 2.0'),  
  10.     Package('django''BSD'),  
  11.     Package('pytest''MIT'),  
  12. ]  
  13.   
  14.   
  15. @pytest.fixture(params=PACKAGES, ids=operator.attrgetter('name'))  
  16. def python_package(request):  
  17.     return request.param  
  18.   
  19.   
  20. @pytest.mark.parametrize('person', [  
  21.     Woman('Audrey'), Woman('Brianna'),  
  22.     Man('Daniel'), Woman('Ola'), Man('Kenneth')  
  23. ])  
  24. def test_become_a_programmer(person, python_package):  
  25.     person.learn(python_package.name)  
  26.     assert person.looks_like_a_programmer  
  27.   
  28.   
  29. def test_is_open_source(python_package):  
  30.     assert python_package.is_open_source  
Then you can test it this way: 
# pytest -v -s test3.py
...
plugins: celery-4.1.0
collected 18 items

test3.py::test_become_a_programmer[requests-person0] PASSED
test3.py::test_become_a_programmer[requests-person1] PASSED
test3.py::test_become_a_programmer[requests-person2] PASSED
test3.py::test_become_a_programmer[requests-person3] PASSED
test3.py::test_become_a_programmer[requests-person4] PASSED
test3.py::test_become_a_programmer[django-person0] PASSED
test3.py::test_become_a_programmer[django-person1] PASSED
test3.py::test_become_a_programmer[django-person2] PASSED
test3.py::test_become_a_programmer[django-person3] PASSED
test3.py::test_become_a_programmer[django-person4] PASSED
test3.py::test_become_a_programmer[pytest-person0] PASSED
test3.py::test_become_a_programmer[pytest-person1] PASSED
test3.py::test_become_a_programmer[pytest-person2] PASSED
test3.py::test_become_a_programmer[pytest-person3] PASSED
test3.py::test_become_a_programmer[pytest-person4] PASSED
test3.py::test_is_open_source[requests] PASSED
test3.py::test_is_open_source[django] PASSED
test3.py::test_is_open_source[pytest] PASSED

========================================== 18 passed in 0.03 seconds

Going the extra mile and setting up ids for your test scenarios greatly increases the comprehensibilty of your test report. In this case we would like to display the name of each Package rather than the fixture name with a numbered suffix such as python_package2.

沒有留言:

張貼留言

[ Py DS ] Ch1 - IPython: Beyond Normal Python

Source From  Here   Keyboard Shortcuts in the IPython Shell   If you spend any amount of time on the computer, you’ve probably found a u...