Pytest: A Testing Framework for Python Code
How can you check that your code changes actually achieve what they’re meant to?
Ensuring your code has integrity is actually quite difficult to ensure, especially at scale. Usually you’ll work in a large team with different people working on different parts of the system. Everyone is tinkering about with something and if you’re using an agile methodology, you’ll be committing code multiple times a day. So how can you keep track that all your changes are backwards compatible? How can you keep track that your code changes maintain at least the same functionality as the code you’re removing (without the bad bits)?
You can test code in a number of ways. A lot of it tends to be sensibility testing and coming up with situations or extreme cases in which the code will definitely fail, and then by narrowing the scope.
It’s a long process, but this is so important because some code holds a lot of responsibility: at times, faulty code can bring down the company.
That’s not a joke, check these stories:
- GETCO lost $400m in Trading caused by Computer Error (and was rescued through acquisition by KCG)
- Y2K Bug that reportedly cost the industry upwards of $300bn to resolve (that’s billion with a b!).
- AT&T Goes does for 9 hours and 75 million calls go unanswered — what caused it? A software update
These stories broke headlines and broke companies but just think about it as a customer as well: would you use an application that was super buggy? No, I wouldn’t either.
The Python community has appreciated testing for a while and pretty much all developers should know how to test their code. In what follows, we’ll be discussing the library pytest
.
So what is pytest
?
Pytest
has been built over a number of years and it’s been so popular for the following reasons:
- easy and simple syntax
- run specific tests or a subset of tests in parallel
- built-in automatic detection of tests and the ability to skip tests
- encourages test parametrisation and gives useful information on failure
- encompasses minimal boilerplate
- makes testing easy by providing special routines and extensibility (many of plugins, hooks etc. are available)
- open-source i.e. allows contribution from the larger community
These are but a few points that make pytest
easy to use. Note that testing also forms an integral part of your continuous integration and continuous development process, but this will be covered elsewhere.
Getting Started with PyTest
To install pytest
, open up your command line and run the following command:
pip install pytest
You can also check whether the version is installed correctly with:
$ pytest — version
Your First Test
Before getting started, create a folder with name “Sample Test” and make a python file called testing_sample.py.
The assert
statements are used to ascertain a true or false status in a method or test expectation.
Now in what follows, we’ll produce your first test in just 4 lines of code:
# testing_sample.py
def func(y):
return y + 1
def test_answer():
assert func(2) == 4
On execution of the above test function with:
$ pytest testing_sample.py
It would return a failure report because func(2)
is not equal to 4 (i.e. 3!= 4
). Additionally, it provides you with a solution for test failure. However, if you reset the code to: func(2)==3
, this would then pass as it isTrue
. Make sure to correct this before progressing.
Note: with standard test discovery rules, you can store multiple tests of your files in your current directory and its subdirectories and pytest will run through them all
Assert Exception
To determine if a piece of code causes an exception, you can create a test with an assert with the raise
helper as shown:
# testing_sysexit.py
import pytest
def p():
raise SystemExit(1)
def test_mytest():
with pytest.raises(SystemExit):
p()
This test would result in an exception being thrown but as it’s expected (and controlled using the raise
keyword), the test would pass and continue as on.
Multiple Tests Class Grouping
Now if you want to run multiple tests, you can choose to group them as a class as such:
# testing_class.py
class TestClass:
def test_one(self):
y = “this”
assert “h” in y
def test_two(self):
y = “hello”
assert hasattr(y, “check”)
Pytest
discovers every test in the class bearing prefixes like “testing” as used. we can now run the code with:
$ pytest testing_class.py
You can discover that the first test passed while the second failed to give the reasons for the failure to gain a proper understanding.
FIXTURES
pytest
fixtures are decorator functions, which essentially run before you run a function. A pytest
fixture is implemented in the same manner as a decorator function as follows:
@pytest.fixture
def abc(input):
...
The implementation in pytest
offers dramatic improvements over the classic xUnit
style of setup/teardown functions because primarily, fixtures have explicit names and are activated by declaring their use from test functions. Fixtures are also modular, so each fixture name triggers a fixture function which can itself use other fixtures. Finally, fixture management scales from a simple unit test to more complex parametrised fixtures. With high-grade production code in a big organisation, the ability to configure and re-use fixtures is imperative.
In more layman terms, fixtures are generally used to set the data for a test by e.g. setting a database connections or connecting to a URL to test: generally, setting some sort of input data. So instead of running the same code for every test, we can attach fixture function to the tests and it will run and return the data to the test before executing each test.
Let’s look at a ream example. Create a file test_div.py and add the below code to it
# test_div.py
import pytest
@pytest.fixture
def input_value():
input = 39
return input
def test_divisible_by_3(input_value):
assert input_value % 3 == 0
def test_divisible_by_6(input_value):
assert input_value % 6 == 0
When running this file, we will get a pass in the first test but a fail in the second test as 39 is not divisible by 6.
Now the approach comes with its own limitation. A fixture function defined inside a file can only be used within that file only (as it’s in that scope). To make a fixture available to multiple test files, we have to define the fixture function in a file called conftest.py
.
The coding community feels strongly about testing because it really does save a lot of time and hassle when you’re making changes as part of a wider web of code. We’ve all been in that case where you make a small change and suddenly everything breaks and there’s no clear reason why. Testing ensures that we stay on-top of all problems and can isolate/fix them quickly.
I’d recommend it as a practitioner because testing allows you to build a solid foundation to any new framework. You can at times test to death, but, it’s better than not testing at all. After all, you want to ensure that your code has the highest degree of integrity as possible — not only for when you merge it, but for there after.
Thanks for reading again!! Let me know if you have any questions and I’ll be happy to help.
Keep up to date with my latest work here!