Skip to main content

Starting with Pytest framework

Pytest is test framework implemented in Python used for API, Database and UI testing.

Running Pytest:

When we run pytest command without any argument, it will look for the "test_*.py" OR "*_test.py" files inside directories and subdirectories and run them. All test functions inside these files must start with "test" in their names. 

Let's create our first file with unit test. We'll create very basic suite to test the addition and subtraction of two numbers to understand the concepts by creating two files as shown below.

Basic strtucture of any test case is :

- Function name has "test" as either prefix or suffix
- Perform some operations
- Verify the result using "assert"

# test_addition.py

def test_add_two_numbers_1():
x, y = 5, 20
s = x + y
assert s == 25

def test_add_two_numbers_2():
x, y = 5, 20
s = x + y
assert s == 20

# test_subtraction.py

def test_subtract_two_numbers_1():
x, y = 25, 20
s = x - y
assert s == 5

def test_subtract_two_numbers_2():
x, y = 25, 20
s = x - y
assert s == 20

In the example above, first test in each file will pass and second test will fail. To run all these tests, just execute the command :-   

pytest -v

-v will use the verbose mode, use -s to print the console log (i..e. if you have used any print statement)

Let's say, we just want to run the test case for addition only, so we can pass the file name to pytest command like

pytest  test_addition.py -v

What if we have additonal tests in this file (not related to addition) OR these test cases are spread in multiple files. In this case, we can use pass the sub-string to the pytest command

pytest -k add -v

Grouping Tests using Markers :

Markers allow to set attributes on test cases. There are some pre-defined markers and you can create your own markers as well to group test cases.

Let's create a new file "test_multiplication.py" to test the multiplication. We will use custom markers to identify and run the multiplication test cases.

# test_multiplication.py

import pytest

@pytest.mark.multiply
def test_multiply_two_numbers_1():
x, y = 5, 20
m = x * y
assert s == 100


Syntax for assigning markers is  @pytest.mark.<marker-name>

Before, we can run these test cases, we need to register the custom markers by creating an .ini file (pytest.ini).

# content of pytest.ini
[pytest]
markers =
multiply: Tests for multiplication


To run the multiplication test cases, use the following command

pytest -m multiply -v

There are some pre-defined marks in Pytest which can be used. As an example, @pytest.mark.skip can be used to Skip any test case from being executed. Another example is @pytest.mark.xfail which will actually run the test case but the pass/fail status is not reported in the reports.

Fixtures :

Fixtures are Python functions which can be called before executing test case. Common use cases are to input data, setting up database connection, user-login into a website and so on. To define a function as fixture, use the decorator like this

@pytest.fixture

Let's modify our file having multiplication tests to add a new test case and fixture, which will provide the input.

The way it works is, for any fixture to be executed before any test, we need to pass the fixture name as an argument of the test case. On running the test cases, fixture will be executed first and the output will be stored in same variable. In our example below, input fixture is executed which will return the values 5, 10 and those values will be stored in input variable.


import pytest

@pytest.fixture
def input():
x = 5
y = 20
return x,y

@pytest.fixture
def setup():
print("Fixture called before test")
yield
print("Fixture called after test")

@pytest.mark.multiply
def test_multiply_two_numbers_1():
x, y = 5, 20
m = x * y
assert m == 100

@pytest.mark.multiply
def test_multiply_two_numbers_2(input):
x, y = input
m = x * y
assert m == 100


Fixtures can be defined in a seperate file called conftest.py and these fixtures could be reused in multiple tests / files without having to import them in test file.

If you want to run a teardown method or a code after a test execution, you can use modify the fixture definition such that anything after yield will be excuted once the test execution is done and the code prior to yield will be called before test case execution.

If you need to pass the fixture to multiple test cases, one way to handle this is by making the pytest functions as Class methods. Then use the usefixtures method by passing the name of fixture. This way all the test cases (class methods) will use the fixture (setup in our case) without having to pass the fixture name explicitly.

@pytest.mark.usefixtures("setup")
class TestCaseDemo:
def test_example1(self):
print("This is test case1")

def test_example2(self):
print("This is test case1")

def test_example2(self):
print("This is test case1")

If you pass the scope=class to fixture, it will only execute the fixture once before running the test cases of the class and teardown method as specified in the yield after all test cases are executed.

You can also use the fixture to pass the data to the test cases. As an example, you can pass the user data to all the test cases using fixtures.

@pytest.fixture
def userdata():
return "John Doe"

@pytest.mark.usefixtures("userdata")
class TestCaseDemo2:
def test_example1(self,userdata):
print("User data is", userdata)

Parameterizing Test Cases :

Parameterizing a test case allows you to run the same test case against multiple inputs. In the example below, we have 4 set of inputs which are passed to the test and the result is compared against the input.

@pytest.mark.parametrize("x, result",[(1,2),(2,4),(3,6),(4,8)])
def test_multiply_two_numbers_2(x, result):
m = x * 2
assert m == result


You can also use the fixtures to run the same test case with different set of input. This is helpful when you want to run the same test case on multiple platform/browsers. In the example below, test case will be executed two times with "data1", "data1b" passed in first run and "data2" in second run.

@pytest.fixture(params=[("data1", "data1b"), "data2"])
def userdata(request):
return request.param

def test_data(userdata):
print(userdata)


Comments