Intermediate

Testing is an extremely important part of having good reliable python code. However, the challenge is that it can be very tiring to manually test your code again and again, and it’s also very easy to forget things to test. This is where Automated Testing can help.

Automated Testing is where you write code, independent of your main program, to test your code in a repeatable fashion. You can also run the tests automatically each time your code starts or when it gets deployed to production. Let’s learn How to write perform automated testing with pytest in python 3.

What is pytest?

Pytest is a mature full-featured Python testing tool that helps you write better programs. The pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries.

With pytest, you create a separate python script with a series of small functions that test your code where each function (called a unit test) is one test case. When this testing script is executed, you can get a report to see which tests succeeded and failed. It’s as simple as that, but can help improve the quality of your code immensely.

Installing pytest

Installing pytest is as simple as running the following command in your command line, just ensure that python is installed:

pip install -U pytest

Then you can execute the following command to check that it has been installed successfully

pytest --version

Running a simple unit test

Let’s imagine that an application has an adding method, and the developer wants to apply automated unit testing to the method, e.g. ensure that 3+5 = 8.
To achieve this, the developer needs a new function that will test the addition function and its results. This testing function is in charge of assert that another function is working and performing a specific task as expected

# addtion_test.py
def addtion(x,y):
    return x + y

def test_adition():
    assert addition(3,5) == 5

The assert statement checks to see if condition passed to it is true

Note that the file containing this code is named test_addition.py and the testing function is named test_addition(), this is very important to keep in mind due to pytest will execute the unit tests withing files which accomplish the following patterns test_*.py or *_test.py, same rules apply to the testing functions themselves.

In order to trigger the automated testing, pytest has to be executed, just open a terminal at the same path than addition_test.py and writepytest , the result will be the following. It will find the file automatically.

test_addition.py

As can be seen at the above image, the test has been approved with a 100% score, meaning that all our tests, in this case, just one, has been passed. This means that the addition() function is working as intended, but let’s change a little bit that function to see how a failed test looks like.

Assume that somebody has made some changes to the code, but by mistake introduce a little bug, where the addition() function now is multiplying numbers, the code will look something similar to this:

# addtion_test.py
def addtion(x,y):
    return x * y

def test_adition():
    assert addtion(3,5) == 8

Here, the assert statement fails and returns false hence the error. The mistake above is very simple to note in this sample due to it is a very short code, but imagine this change in an extensive commit or even worse, imagine this chance in very manual testing performed by humans with no help of software. This issue is solved with pytest, as you can see at the following image, the pytest command will notice this mistake and alert that something is not working as expected

test_addition.py failed

Here pytest is telling that something has changed and now the application is not working as expected, and actually, it’s explicitly alerting which assert is falling, in this case, the addition() function which is receiving 3 and 5 as parameters should return 8 but it is returning 15 meaning something is wrong. This provides us with the ability to ensure high quality without breaking the functionality.

This little sample shows us the basis of pytest but we can perform complex testing as well, perform many tests in just one execution and much more, pytest combined with tools as Mock is a very powerful automated testing tool.

List of Assert Statements in Python

Above, you saw just the basic assert statement to compare values, but there are other variations you can use – some examples here:

Assert StatementDescriptionExample
Equals Test equality assertEqual(5, 5)
BooleanTest the true or falseassertTrue( 3 > 5 )
is instanceCheck if this is an instance of a given objectassertIsInstance("abc", str)

You can see a full list here

Executing multiple tests

Imagine a bigger application with some files and many functions along with all the application. As it gets bigger it gets harder to test, pytest solves this by running as many tests as needed. Let’s build a sample with two classes and four files, one test file for each class.

We are going to have two classes: MyMath and MyText. MyMath is in charge of handle additions and multiplication and MyText is in charge of concatenating and uppercasing words. Individual functions are created to handle each functionality.

Each class has a test file, which will be in charge of asserting each functionality, the __init__() method is tested as well.

# my_math.py

# Class definition
class MyMath():
    # Addtion funtion, receive two integer parameters and return the resulting value as integer as well
    def addition(self,x:int,y:int) -> int:
        return x + y
 
    # Multiplication funtion, receive two integer parameters and return the resulting value as integer as well 
    def multiplication(self,x:int,y:int) -> int:
        return x * y 

In the same path, a testing file named test_math.py

# test_math.py
from my_math import MyMath
class TestMath():

    # test the __init__() method
    def test_constructor(self):
        # initialize a MyMath object
        mm = MyMath()
        # Test if the mm object is a instance of MyMath class
        assert isinstance(mm,MyMath)

    def test_addition(self):
        # initialize a MyMath object
        mm = MyMath()
        # assert addition's funtion result
        assert mm.addition(3,5) == 8

    def test_multiplication(self) -> int:
        # initialize a MyMath object
        mm = MyMath()
        # assert multiplication's funtion result
        assert mm.multiplication(3,5) == 15 

Same process with MyText class, one file to hold the functionality and other in charge of testing every single method

# my_text.py
class MyText():
    def __init__(self):
        pass
    # Concatenate two words, with a space between them
    def concatenate(self,word1:str,word2:str) -> str:
        return f'{word1} {word2}'
    # Returns the uppercase of the received string 
    def uppercase(self,word1:str) -> str:
        return word1.upper() 

Now create a new file, in the same path, remember that file name and function names must accomplish the following patterns test_*.py or *_test.py

# test_text.py
from my_text import MyText
class TestText():
    # test the __init__() method
    def test_constructor(self):
        # initialize a MyText object
        mt = MyText()
        # Test if the mt object is a MyText class instance 
        assert isinstance(mt,MyText)

    def test_concatenate(self):
        # initialize a MyText object
        mt = MyText()
        # assert concatenate funtion result
        assert mt.concatenate("hello","world") == 'hello world'

    def test_uppercase(self) -> int:
        # initialize a MyText object
        mt = MyText()
        # assert uppercase funtion result
        assert mt.uppercase('hello') == 'HELLO' 

Running a pytest the command will assert all files and functions that accomplish the pattern mentioned, meaning it will execute three functions within each file, six functions in total as shown in the following image

testing multiple files

As can be seen, it says that six items were collected, meaning that six functions were tested, and then it displays which files were tested as well ant the respective percentage. If you want to collect more information a flag -v can be attached to the command, having the following result.

Verbose option

The -v flag provide us with detailed information of each function that were tested and the state of the assert with the respective percentage. If you just want to test a specific file just need to write the name of the file after the pytest command.

Testing test_text.py file

Parametrized test

In the above examples, the test cases were hardcoded for individual numbers. How can you test a range of inputs rather than a single set? By using the pytest.mark.parametrize helper you can easily set parametrization of arguments for a test function, This way different scenarios can be tested at once. Here is a sample of the pytest.mark.parametrize decorator

# test_math.py
import pytest
from my_math import MyMath
class TestMath():

    # test the __init__() method
    def test_constructor(self):
        # initialize a MyMath object
        mm = MyMath()
        assert isinstance(mm,MyMath)

    @pytest.mark.parametrize("num1,num2,expected", [(3,5, 8), (4,2, 6), (-4,-1, -5)])
    def test_addition(self,num1,num2, expected):
        # initialize a MyMath object
        mm = MyMath()
        # assert addition's funtion result
        assert mm.addition(num1,num2) == expected

    @pytest.mark.parametrize("num1,num2,expected", [(3,5, 15), (4,2, 8), (-4,-1, 4)])
    def test_multiplication(self,num1,num2,expected) -> int:
        # initialize a MyMath object
        mm = MyMath()
        # assert multiplication's funtion result
        assert mm.multiplication(num1,num2) == expected 
pytest 5
pytest 5

In the above example, we use a decorator @pytest.mark.parametrize to specify inputs for the parameters that are provided. For example, in the first for the three variables “num1, num2, expected”, the values (3, 5, 15) are included meaning that the test function is executed with num1 =3, num2=5 and expected=15. This is an easy way to run multiple scenarios quite easily and retain readable code.

As shown above, the execution has been done more than once on the parameterized functions, three different scenarios have been tested, this way the testing can ensure a better assurance.

Conclusions

Will all the tools provided until here, you are able to perform thousands of testing, bring incredibly high quality to your application and letting. This way you will be able to test your application functionality and be sure that anything breaks with the changes performed.

Get Notified Automatically

Error SendFox Connection: 403 Forbidden

403 Forbidden