Pytest Tutorial – How To Use pytest For Python Testing

Learn what is pytest, how to install and use Python pytest with examples in this comprehensive pytest tutorial:

A test is a code that checks the validity of the other code. Tests are designed to help in gaining confidence that what you wrote is working. It proves that the code is working as we want and get a safety net for future changes.

=> Visit Here To Learn Python From Scratch

pytest for Python Testing

What Is Pytest

pytest is the framework that makes it easy to write, test, and scale to support complex testing for the applications and libraries. It is the most popular Python package for testing. The basis for a rich ecosystem of testing is plugins and extensions.

The way pytest is designed is as a very extensible system, easy to write plugins and there are a lot of plugins present in the pytest that are used for various purposes. Testing is very important before delivering the code in production.

It is a mature full-featured Python tool that helps to write better programs.

Features Of pytest

  • Does not require API to use.
  • Can be used to run doc tests and unit tests.
  • Gives useful failure information without the use of debuggers.
  • Can be written as a function or method.
  • Has useful plugins.

Advantages Of pytest

  • It is open-source.
  • It can skip tests and automatically detect the tests.
  • Tests are run parallel.
  • Specific tests and subsets of tests can be run from the program.
  • It is easy to start with as it has a very easy syntax.

Many programmers perform automatic testing before the code goes into production.

Python offers three types of testing:

  • Unittest: It is the testing framework that is built in the standard library.
  • Nose: It extends the unittest to make testing easy.
  • pytest: It is the framework that makes it easy to write test cases in Python.

How To Install pytest In Linux

Make a directory with a name suitable for you in which the Python files will take place.

  • Make a directory using the command (mkdir <directory name>).

make_directory

  • Make a virtual environment, in which the installation of specific packages will take place rather than in the whole system.
    • A virtual environment is a way where we can separate different Python environments for different projects.
    • Example: Say we have multiple projects and they all rely on a single package say Django, Flask. Each of these projects may be using a different version of Django or Flask.
    • Now, if we go and upgrade a package in the global size packages, then it breaks into a couple of uses of websites that might not be what we want to do.
    • It would be better if each of these projects had an isolated environment where they had only dependencies and packages they required and the specific versions they needed.
    • That’s what virtual environments do, they allow us to make those different Python environments.
    • Installation of the virtual environment via command line in Linux:
      • `pip install virtualenv`
      • Now, if we run the command `pip list`, it will show the global packages installed globally in the machine with the specific versions.
      • `pip freeze` command shows all the installed packages with their versions in the active environment.
  • To make the virtual environment run the command `virtualenv <environment name> –python=python`
  • Do not forget to activate the virtual env run: `source <environment name>/bin/activate `.

activate the virtual env run

  • After activating the virtual environment, it is time to install pytest in our directory that we made above.
  • Run: `pip install -U pytest` or `pip install pytest` (make sure that the pip version should be the latest).

install_pytest

How To Use pytest Using Python

  • Create a Python file with the name `mathlib.py`.
  • Add the basic Python functions to it as below.

Example 1:

```
def calc_addition(a, b):
    return a + b


def calc_multiply(a, b):
    return a * b
	

def calc_substraction(a, b):
    return a - b
```
  • In the above example, the first function performs the addition of two numbers, the second function performs the multiplication of two numbers and the third function performs the subtraction of two numbers.
  • Now, it’s time to perform automatic testing using pytest.
  • pytest expects the test file name to be in the format: ‘*_test.py’ or ‘test_*.py’
  • Add the following code in that file.
```
import mathlib

def test_calc_addition():
    “””Verify the output of `calc_addition` function”””
    output = mathlib.calc_addition(2,4)
    assert output == 6

def test_calc_substraction():
    “””Verify the output of `calc_substraction` function”””
    output = mathlib.calc_substraction(2, 4)
    assert output == -2
	
def test_calc_multiply():
    “””Verify the output of `calc_multiply` function”””
    output = mathlib.calc_multiply(2,4)
    assert output == 8
```
  • In order to run the test functions, remain in the same directory, and run the `pytest`, `py.test`, `py.test test_func.py` or `pytest test_func.py`.
  • In the output, you will see all that the test cases are passed successfully.

pytest_example_1

  • Use `py.test -v` to see the detailed output of each test case.

use py.test -v

  • Use `py.test -h` if you want any help while running the pytests.

Example 2:

We are going to write a simple program to calculate the area and the perimeter of a rectangle in Python and perform testing using pytest.

Create a file with the name “algo.py” and insert the below.

```
import pytest

def area_of_rectangle(width, height):
area = width*height
return area

def perimeter_of_rectangle(width, height):
perimeter = 2 * (width + height)
return perimeter
```

Create a file with the name “test_algo.py” in the same directory.

```
import algo

def test_area():
output = algo.area_of_rectangle(2,5)
assert output == 10

def test_perimeter():
output = algo.perimeter_of_rectangle(2,5)
assert output == 14

```

How to Use pytest for Python Testing

pytest Fixtures

  • When we run any test case, we need to set up a resource (Resources which needs to be set up before the test starts and cleaned once done) for example, ” connecting to the database before the starting of the test case and disconnecting when it’s done”.
  • Launch the URL and maximize the window before starting and close the window once done.
  • Opening data files for reading\writing and closing the files.

Thus, there can be scenarios that we need generally for connecting the data source or anything before executing the test case.

Fixtures are the functions that will run before and after each test function to which it is applied. They are very important as they help us to set up resources and tear them down before and after the test cases start. All fixtures are written in the `conftest.py` file.

Now, let us understand this with the help of an example.

Example:

In this example, we are using fixtures to provide the input to the Python program.

Create three files named “conftest.py”(is used to give the output to the Python program), “testrough1.py” and “testrough2.py” (both the files contain the Python functions to perform the mathematical operations and get the input from the conftest.py)

In the “conftest.py” file insert the following:

```
import pytest

@pytest.fixture
def input_total( ):
    total = 100
    return total
```

In the “testrough1.py” file insert

```
import pytest

def test_total_divisible_by_5(input_total):
    assert input_total % 5 == 0


def test_total_divisible_by_10(input_total):
    assert input_total % 10 == 0


def test_total_divisible_by_20(input_total):
    assert input_total % 20 == 0


def test_total_divisible_by_9(input_total):
    assert input_total % 9 == 0

```

In the “testrough2.py” file insert

```
import pytest

def test_total_divisible_by_6(input_total):
    assert input_total % 6 == 0


def test_total_divisible_by_15(input_total):
    assert input_total % 15 == 0


def test_total_divisible_by_9(input_total):
    assert input_total % 9  == 0
```

pytest fixtures

In the output, we got an assertion error because 100 is not divisible by 9. To correct it, replace 9 with 20.

```
def test_total_divisible_by_20(input_total):
    assert input_total % 20  == 0
```

Where To Add Python Fixtures

Fixtures are used instead of class xUnit style setup and teardown methods in which a particular part of code is executed for each test case.

The major reasons to use the Python Fixtures are :

  • They are implemented in a modular manner. They do not have any learning curve.
  • Fixtures have scope and lifetime. As same as normal functions, the default scope of the fixture is the function scope and the other scopes are – module, class, and session/packages.
  • They are reusable and are used for simple unit testing and complex testing.
  • They act as vaccine and test functions that are used by the fixture consumers in the fixture objects.

When To Avoid pytest Fixtures

Fixtures are good for extracting the objects that we are using in multiple test cases. But it is not necessary that we need fixtures every time. Even when our program needs a little bit of variation in the data.

Scope Of pytest Fixtures

The scope of pytest Fixtures indicates how many times a fixture function is invoked.

pytest fixture scopes are:

  • Function: It is the default value of Python fixture scope. The fixture which has a function scope is executed only once in each session.
  • Module: The fixture function which has a scope as a module is created once per module.
  • Class: We can create a fixture function once per class object.

Assertions In pytest

Assertions are the way of telling your program to test a certain condition and trigger an error if the condition is false. For that, we use the `assert` keyword.

Let us see the basic syntax of Assertions in Python:

```
assert <condition>, <option -message>
```

Example 1:

Let’s consider that there is a program which takes the age of a person.

```
def get_age(age):
    print (“Ok your age is:”, age)

get_age(20)
```

The output will be “Ok your age is 20”.

Now, let us take a case in which we incidentally give the age in negatives like `get_age(-10)`

The output will be “Ok your age is -10”.

Which is quite weird! This is not what we want in our program, In that case, we will use assertions.

```
def get_age(age):	
    assert age > 0, “Age cannot be less than zero.”
    print (“Ok your age is:”, age)

get_age(-1)
```

Now, comes the Assertion Error.

assertion error

Example 2:

In the given example we are performing basic addition of two numbers where `x` can be any number.

```
def func(x):
    return x +3

def test_func():
    assert func(4) == 8
```

Assertion error is thrown

In the output, we are getting the assertion error because 8 is the wrong result as 5 + 3 = 8 and the test case is failed.

Correct program:

```
def func(x):
    return x +3


def test_func():
    assert func(4) == 7
```

correct program - assertion

Basically, this is the way to debug the code, it is easier to find the errors.

Parametrization In pytest

Parametrization is used to combine the multiple test cases into one test case. With parameterized testing, we can test functions and classes with different multiple sets of arguments.

In parametrize, we use `@pytest.mark.parametrize()` to perform parameterization in the Python code.

Example 1:

In this example, we are calculating the square of a number using the parametrization.

Create two files `parametrize/mathlib.py` and `parametrize/test_mathlib.py`

In `parametrize/mathlib.py` insert the following code that will return the square of a number.

```
def cal_square(num):
    return num * num
```

Save the file and open the second file` parametrize/test_mathlib.py`

In the test files, we write the test cases to test the Python code. Let’s use the Python test cases to test the code.

Insert the following:

```
import mathlib

# Test case 1
def test_cal_square_1( ):
    result = mathlib.cal_square(5)
    assert == 25


# Test case 2
def test_cal_square_2( ):
    result = mathlib.cal_square(6)
    assert == 36


# Test case 3
def test_cal_square_3( ):
    result = mathlib.cal_square(7)
    assert == 49


# Test case 4
def test_cal_square_4( ):
    result = mathlib.cal_square(8)
    assert == 64
```

There will be a number of test cases to test the code which is quite weird. The code for the test cases is the same except for the input. To get rid of such things, we will perform parameterization.

Replace the above test cases with the below:

```
import pytest
import mathlib


@pytest.mark.parametrize(“test_input”, “expected_output”, [ (5, 25), (6, 36), (7, 49) ] )
    def test_cal_square(test_input, expected_output):
        result = mathlib.cal_square(test_input)
        assert result == expected_output
```

The test case will pass in both manners, just parametrization is used to avoid the repetition of code and get rid of the lines of code.

parametrization

Example 2:

In this example, we are performing multiplication of numbers and comparing the output(`result`). If the calculation is equal to the result then, the test case will be passed otherwise not.

```
import pytest


@pytest.mark.parametrize(“num”, “result”, [(1, 11), (2, 22), (3, 34), (4, 44), (5, 55)]
def test_calculation(num, result):
    assert 11*num == result
```

In the output, it will throw the error because in the (3, 34) case we are expecting (3, 33). The assertion in the Python code will help to debug the errors in the code.

parametrization error

The correct program is:

```
@pytest.mark.parametrize(“num”, “result”, [(1, 11), (2,22), (3,33), (4,44), (5,55)]
def test_calculation(num, result):
    assert 11*num == result
```

parametrization example

Decorators In pytest

Decorators allow us to wrap the functions in another function. It avoids code duplication and cluttering the main logic of function with additional functionality (i.e. time in our example).

The problem that we face generally in our programs is code repetition/duplication. Let’s understand this concept with an example.

Create a file `decorators.py` and insert the following code to print the time taken by the function to calculate the square of a number.

```
import time 


def calc_square(num):
    start = time.time()
    result = []
    for num in num:
        result.append(num*num)
            end = time.time()
            print(“calc_square took: ” + str((end-start)*1000 + “mil sec)


def calc_cude(num):
    start = time.time()
    result = []
    for num in num:
    result.append(num*num*num)
    end = time.time()
    print(“calc_cube took: ” + str((end-start)*1000 + “mil sec)


array = range(1,100000)
out_square = cal_square(array)

In the above function, we are printing the time taken by the function to get executed. In every function, we are writing the same lines of code to print the time taken which is not looking good.

```
start = time.time()
end = time.time()
print(“calc_cube took: ” + str((end-start)*1000 + “mil sec)
```

The above code is code duplication.

The second problem is that there is a logic in the program which is calculating the square and we are cluttering the logic with the timing code. It thereby makes the code less readable.

To avoid these problems we use decorators as shown below.

```
import time

# Functions are the first class objects in Python. 
# What it means is that they can be treated just like other variables and you can pass them as
# arguments to another function  or even return them as a return value.

def time_it (func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name___ + “took ” + str((end - start) * 1000 + “mil sec”)
        return result
     return wrapper


@time_it
def calc_square(num):
    start = time.time()
    result = []
    for num in num:
    result.append(num*num)
    end = time.time()
    print(“calc_square took: ” + str((end - start) * 1000 + “mil sec)


@time_it
def calc_cude(num):
    start = time.time()
    result = []
    for num in num:
        result.append(num*num*num)
        end = time.time()
        print(“calc_cube took: ” + str((end-start)*1000 + “mil sec)

array = range(1,100000)
out_square = cal_square(array)
```

Decorators in pytest

The output will show the time taken by the `cacl_square` function as 11.3081932068 mil seconds.

Stop The Testing Process

  • Run `pytest -x` which is used to stop after the first failure.
  • Run `pytest –maxfail = 2` which is used to stop after the two failures. Where you can change the maxfail number with any digit you want.

Run Specific Tests

  • Run all tests in a module
    • pytest test_module.py
  • Run all tests in a directory
    • pytest <directory_name>/
  • Run a specific test from file
    • pytest test_file.py::test_func_name

Frequently Asked Questions

Q #1) How do I run a specific test in pytest?

Answer: We can run the specific test from the test file as

 `pytest <test_file.py>::<test_function_name>`

Q #2) Should I use pytest or Unittest?

Answer: Unittest is the testing framework that is built in the standard library. You don’t need to install it separately, it comes with the system and is used to test the internals of the core of Python. It has a long history which is a good solid tool.

But presenting a united ideal for reasons, the biggest reason is `assert`. Assert is the way in which we do testing in Python. But if we are using unittest for testing then, we have to use `assertEqual`, `assertNotEqual`, `assertTrue`, `assertFalse`, `assertls`, `assertlsNot` and so on.

Unittest is not as magical as pytest. pytest is fast and reliable.

Q #3) What is Autouse in pytest?

Answer: Fixture with `autouse=True` will be initiated first than the other fixtures of the same scope.

In the given example, we see that in the `onion` function we define the `autouse = True` which means it will be initiated first among the others.

```
import pytest

vegetables = [] 

@pytest.fixture
Def cauliflower(potato):
	vegetables.append(“cauliflower”)

@pytest.fixture
Def potato():
	vegetables.append(“potato”)

@pytest.fixture(autouse=True)
Def onion():
	vegetables.append(“onion”)

def test_vegetables_order(cauliflower, onion):
    assert vegetables == [“onion”, “potato”, “cauliflower”]
```

Q #4) How many exit codes are there in pytest?

Answer:

There are six exit codes

Exit code 0: Success, all tests are passed
Exit code 1: Some tests were failed
Exit code 2: User interrupted the test execution
Exit code 3: Internal error occurred
Exit code 4: Error in pytest command for triggering tests
Exit code 5: No test were found

Q #5) Can we use TestNG with Python?

Answer: No you cannot use TestNG directly in Python. One can do Python Unittest, pytest, and Nose frameworks.

Q #6) What is the pytest session?

Answer: Fixtures with `scope=session` are of high priority i.e. it will trigger only once at the start, no matter where it is declared in the program.

Example:

In this example, the fixture function goes through all the collected tests and looks if their test class defines a `ping_me` method and calls it. Test classes may now define a `ping_me` method that will be called ahead of running any tests.

We are creating two files i.e. `conftest.py`, `testrought1.py`

In the `conftest.py` insert the following:

```

import pytest

@pytest.fixture(scope=”session”, autouse=True)
def ping_me(request):
	print(“Hi! Ping me”)
	seen = {None}
	session=request.node
	for item in session.items:
		png=item.getparent(pytest.class)
		if png not in seen:
			if hasattr(png.obj, “ping me”):
				png.obj.ping_me()
                                               seen.add(png)	
```

<strong>In `testrough1.py` insert the following:</strong>

```
class TestHi:
	@classmethod
	def ping_me(png):
		print(“ping_me called!”)
	def testmethod_1(self):
		print(“testmethod_1 called”)
	def testmethod_1(self):
		print(“testmethod_1 called”)

```

Run this command to see the output:

`pytest -q -s testrough1.py`

pytest session

Conclusion

In a nutshell, we covered the below in this tutorial:

  • Installation of Virtual Python Environment: `pip install virtualenv`
  • Installation of pytest: `pip install pytest`
  • Fixtures: Fixtures are the functions that will run before and after each test function to which it is applied.
  • Assertions: Assertions are the way of telling your program to test a certain condition and trigger an error if the condition is false.
  • Parametrization: Parametrization is used to combine the multiple test cases into one test case.
  • Decorators: Decorators allow you to wrap the functions in another function.
  • Plugins: This way allows us to create global constants that are configured at the time of compilation.

=> Check Here To See A-Z Of Python Training Tutorials