This tutorial will let u know everything about pytest required for automation.
pytest install
Pytest is installed with the following command:
$ pip install pytest
This install the pytest library.
pytest test discovery conventions
If no arguments are specified then test files are searched in locations from testpaths (if configured) or the current directory.
Alternatively, command line arguments can be used in any combination of directories, file names or node ids.
In the selected directories, pytest looks for test_*.py or *_test.py files.
In the selected files, pytest looks for test prefixed test functions outside of class and test prefixed test methods inside Test prefixed test classes (without an __init__() method).
Running pytest
With no arguments, pytest looks at the current working directory (or some other preconfigured directory) and all subdirectories for test files and runs the test code it finds.
$ pytest
Above command with run all test files in the current directory.
//We can run a specific test file by giving its name as an argument.
$ pytest min_max_test.py
//A specific function can be run by providing its name after the :: characters.
$ pytest min_max_test.py::test_min
//Markers can be used to group tests. A marked grouped of tests is then run with pytest -m.
$ pytest -m smoke
//In addition, we can use expressions to run tests that match names of test functions and classes.
$ pytest -k <expression>
//You can run it using the pytest command
$ pytest
================== test session starts =============================
platform darwin — Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.0
rootdir: /…/effective-python-testing-with-pytest
collected 2 items
test_with_pytest.py .F [100%]
======================== FAILURES ==================================
___________________ test_always_fails ______________________________
def test_always_fails():
> assert False
E assert False
test_with_pytest.py:5: AssertionError
============== 1 failed, 1 passed in 0.07s =========================
pytest presents the test results differently than unittest. The report shows:
- The system state, including which versions of Python, pytest, and any plugins you have installed
- The rootdir, or the directory to search under for configuration and tests
- The number of tests the runner discovered
The output then indicates the status of each test using a syntax similar to unittest:
- A dot (.) means that the test passed.
- An F means that the test has failed.
- An E means that the test raised an unexpected exception.
Run a subset of entire test
Sometimes we don’t want to run the entire test suite. Pytest allows us to run specific tests. We can do it in 2 ways
- Grouping of test names by substring matching
- Grouping of tests by markers
We already have test_sample1.py. Create a file test_sample2.py and add the below code into it
def test_file2_method1():
x=5
y=6
assert x+1 == y,"test failed"
assert x == y,"test failed because x=" + str(x) + " y=" + str(y)
def test_file2_method2():
x=5
y=6
assert x+1 == y,"test failed"
So we have currently
- test_sample1.py
- test_file1_method1()
- test_file1_method2()
- test_sample2.py
- test_file2_method1()
- test_file2_method2()
Option 1) Run tests by substring matching
Here to run all the tests having method1 in its name we have to run
py.test -k method1 -v
-k <expression> is used to represent the substring to match
-v increases the verbosity
Option 2) Run tests by markers
Pytest allows us to set various attributes for the test methods using pytest markers, @pytest.mark . To use markers in the test file, we need to import pytest on the test files.
Here we will apply different marker names to test methods and run specific tests based on marker names. We can define the markers on each test names by using
@pytest.mark.<name>.
Running tests in parallel
Usually, a test suite will have multiple test files and hundreds of test methods which will take a considerable amount of time to execute. Pytest allows us to run tests in parallel.
For that we need to first install pytest-xdist by running
pip install pytest-xdist
You can run tests now by
py.test -n 4
-n <num> runs the tests by using multiple workers. In the above command, there will be 4 workers to run the test.
Marks: Categorizing Tests
- In any large test suite, some of the tests will inevitably be slow.
- They might test timeout behavior, for example, or they might exercise a broad area of the code.
- Whatever the reason, it would be nice to avoid running all the slow tests when you’re trying to iterate quickly on a new feature.
- pytest enables you to define categories for your tests and provides options for including or excluding categories when you run your suite.
- You can mark a test with any number of categories.
- Marking tests is useful for categorizing tests by subsystem or dependencies. If some of your tests require access to a database, for example, then you could create a @pytest.mark.database_access mark for them
- If you’d like to run only those tests that require database access, then you can use pytest -m database_access. To run all tests except those that require database access, you can use pytest -m “not database_access”
pytest provides a few marks out of the box:
- skip skips a test unconditionally.
- skipif skips a test if the expression passed to it evaluates to True.
- xfail indicates that a test is expected to fail, so if the test does fail, the overall suite can still result in a passing status.
- parametrize (note the spelling) creates multiple variants of a test with different values as arguments. You’ll learn more about this mark shortly.
Parametrization: Combining Tests
you can parametrize a single test definition, and pytest will create variants of the test for you with the parameters you specify.
Imagine you’ve written a function to tell if a string is a palindrome. An initial set of tests could look like this:
def test_is_palindrome_empty_string():
assert is_palindrome(“”)
def test_is_palindrome_single_character():
assert is_palindrome(“a”)
def test_is_palindrome_mixed_casing():
assert is_palindrome(“Bob”)
def test_is_palindrome_with_spaces():
assert is_palindrome(“Never odd or even”)
def test_is_palindrome_with_punctuation():
assert is_palindrome(“Do geese see God?”)
def test_is_palindrome_not_palindrome():
assert not is_palindrome(“abc”)
def test_is_palindrome_not_quite():
assert not is_palindrome(“abab”)
All of these tests except the last two have the same shape:
def test_is_palindrome_<in some situation>():
assert is_palindrome(“<some string>”)
assert is_palindrome("<some string>")
You can use @pytest.mark.parametrize() to fill in this shape with different values, reducing your test code significantly:
@pytest.mark.parametrize(“palindrome”, [
“”,
“a”,
“Bob”,
“Never odd or even”,
“Do geese see God?”,
])
def test_is_palindrome(palindrome):
assert is_palindrome(palindrome)
@pytest.mark.parametrize(“non_palindrome”, [
“abc”,
“abab”,
])
def test_is_palindrome_not_palindrome(non_palindrome):
assert not is_palindrome(non_palindrome)
The first argument to parametrize() is a comma-delimited string of parameter names. The second argument is a list of either tuples or single values that represent the parameter value(s). You could take your parametrization a step further to combine all your tests into one:
@pytest.mark.parametrize(“maybe_palindrome, expected_result”, [
(“”, True),
(“a”, True),
(“Bob”, True),
(“Never odd or even”, True),
(“Do geese see God?”, True),
(“abc”, False),
(“abab”, False),
])
def test_is_palindrome(maybe_palindrome, expected_result):
assert is_palindrome(maybe_palindrome) == expected_result
Pytest fixtures
- Fixtures are used when we want to run some code before every test method.
- So instead of repeating the same code in every test we define fixtures.
- Usually, fixtures are used to initialize database connections, pass the base , etc
- A method is marked as a fixture by marking with
@pytest.fixture
A test method can use a fixture by mentioning the fixture as an input parameter.
A test method can use a fixture by mentioning the fixture as an input parameter.
Create a new file test_basic_fixture.py with following code
import pytest
@pytest.fixture
def supply_AA_BB_CC():
aa=25
bb =35
cc=45
return [aa,bb,cc]
def test_comparewithAA(supply_AA_BB_CC):
zz=35
assert supply_AA_BB_CC[0]==zz,”aa and zz comparison failed”
def test_comparewithBB(supply_AA_BB_CC):
zz=35
assert supply_AA_BB_CC[1]==zz,”bb and zz comparison failed”
def test_comparewithCC(supply_AA_BB_CC):
zz=35
assert supply_AA_BB_CC[2]==zz,”cc and zz comparison failed”
Here
- We have a fixture named supply_AA_BB_CC. This method will return a list of 3 values.
- We have 3 test methods comparing against each of the values.
Each of the test function has an input argument whose name is matching with an available fixture. Pytest then invokes the corresponding fixture method and the returned values will be stored in the input argument , here the list [25,35,45]. Now the list items are being used in test methods for the comparison.
Now run the test and see the result
py.test test_basic_fixture
test_basic_fixture.py::test_comparewithAA FAILED
test_basic_fixture.py::test_comparewithBB PASSED
test_basic_fixture.py::test_comparewithCC FAILED
============================================== FAILURES ==============================================
_________________________________________ test_comparewithAA _________________________________________
supply_AA_BB_CC = [25, 35, 45]
def test_comparewithAA(supply_AA_BB_CC):
zz=35
> assert supply_AA_BB_CC[0]==zz,”aa and zz comparison failed”
E AssertionError: aa and zz comparison failed
E assert 25 == 35
test_basic_fixture.py:10: AssertionError
_________________________________________ test_comparewithCC _________________________________________
supply_AA_BB_CC = [25, 35, 45]
def test_comparewithCC(supply_AA_BB_CC):
zz=35
> assert supply_AA_BB_CC[2]==zz,”cc and zz comparison failed”
E AssertionError: cc and zz comparison failed
E assert 45 == 35
test_basic_fixture.py:16: AssertionError
================================= 2 failed, 1 passed in 0.05 seconds =================================
conftest.py: sharing fixture functions
If during implementing your tests you realize that you want to use a fixture function from multiple test files you can move it to a conftest.py file. You don’t need to import the fixture you want to use in a test, it automatically gets discovered by pytest. The discovery of fixture functions starts at test classes, then test modules, then conftest.py files and finally builtin and third party plugins.
You can also use the conftest.py file
Scope: sharing fixtures across classes, modules, packages or session
Fixtures requiring network access depend on connectivity and are usually time-expensive to create. Extending the previous example, we can add a scope=”module” parameter to the @pytest.fixture invocation to cause the decorated smtp_connection fixture function to only be invoked once per test module (the default is to invoke once per test function). Multiple test functions in a test module will thus each receive the same smtp_connection fixture instance, thus saving time. Possible values for scope are: function, class, module, package or session.
Fixture scopes
Fixtures are created when first requested by a test, and are destroyed based on their scope:
- function: the default scope, the fixture is destroyed at the end of the test.
- class: the fixture is destroyed during teardown of the last test in the class.
- module: the fixture is destroyed during teardown of the last test in the module.
- package: the fixture is destroyed during teardown of the last test in the package.
- session: the fixture is destroyed at the end of the test session.
Report
Adding the pytest-html plugin to your test project enables you to print pretty HTML reports with one simple command line option.
Reports will look like this:
Command to install plugin for pytest html report is pip install pytest-html