Welcome to the second article in my series on Automated Testing.

In my last article, I spoke at some length about the terminology used around automated testing, and made some arguments about why you should be using automated testing.

In this article, I’m going to be getting on the tools to show you how to actually get started with unit testing. I’m going to cover enough for you to write your first unit test with Pytest, and then I’m going to show you how to use parametization — which is one of my favorite tools to use to write useful unit tests.

This is a very practical tutorial. If you haven’t worked with Pytest before, you’re going to get the most out of it by actually working through the tutorial by writing out the code and running it on your computer.

Photo by Chris Ried on Unsplash

Installing Pytest

The first step of adding unit testing is adding your unit test framework. In Python, I tend to use Pytest instead of the unittest module that's included in Python's standard library - as it provides some useful out-of-the-box features (like parametization), and I find the way that it structures tests to be more intuitive.

Setting up a virtual environment

For this tutorial, we need to work with a virtual environment.

If you already know how to set up and activate a virtual environment, skip this section.

Using a virtual environment is an easy way to separate the projects that you are working on, and the — potentially clashing — pip packages that are required for them.

Creating the environment

If you haven’t already, create a folder for your project

Go into your terminal, and navigate to the folder

Create a virtual environment in a folder called env by running python3 -m venv env in your terminal

A note about Debian and Ubuntu The venv module isn't installed by default in Debian or Ubuntu. You'll need to install it by runnning: sudo apt install python3-venv .

Activating the environment

This is one thing that differs a little bit depending on your OS and shell. This is how you activate in some of the more common shells.

On Linux/Unix/MacOS with sh or compatible: run source env/bin/activate in your shell

run in your shell On Windows with cmd : run source env/scripts/activate.bat in your terminal

run in your terminal On Windows with Powershell: run source env/scripts/activate.ps1 in Powershell

A note about Powershell and permissions One way that Microsoft increased the safety of Powershell over cmd is by limiting what scripts you are able to run. If you haven't done so, you'll need to set a new execution policy. You can find Microsoft's documentation on what this does here. You need to set it to RemoteSigned to run the virtual environment. Do this by starting Powershell in Administrator Mode (by right clicking it in the Start menu, and choosing 'Run as Administrator') and then running the command Set-ExecutionPolicy RemoteSigned .

Installing Pytest with pip

To install Pytest using pip, run the command pip install pytest .

Verify Pytest is installed by running the command python -m pytest in your shell. You should see some output like the following:

We’re running Pytest by python -m pytest intentionally here. Alternatively, you run pytest - but it that way requires some additional work. The basic reason is that just calling pytest changes the paths that Python searches for imports - see here. Basically - I'm taking this route to make it as easy as possible to write some useful tests... and not stir up arguments by adding an __init__.py file to the tests folder.

Writing a test

Pytest, by default, looks for tests recursively in the folder you start it in, by looking for files called test_*.py or *_test.py ( see Pytest Test Discovery]).

Beyond this, where you put your tests is up to you. Some engineers prefer to put it in the same folders as their code. I prefer to have a tests folder which mirrors the structure of folders for the rest of the source. Really - it's a matter of what works for you and your team.

We’re going to write a fairly common style test for our first test: we’re going to test a method in an (albeit simple) class.

Setup folders as follows:

project_folder

models

tests

You need to have a virtual environment set up, with pip set up.

To try this, we need something to test. So, make the file project_folder/models/user.py with the following:

As you can see, it’s part of a class that might exist on a web application. We’re going to be testing the get_display_name method. I've defined the requirements of the get_display_name function in the source there. It's probably a little smaller than what you'll normally test, but it's probably helpful as a learning exercise. Not to mention, we all make mistakes - even with simple code - so it's probably still worthwhile checking that it works.

We’re going to start by testing that when we have a user with a username , real_name and preferred_name , that get_display_name returns the preferred_name .

Put the following in project_folder/tests/test_user.py :

You may not have seen the GIVEN, WHEN, THEN layout used in this unit test. It’s borrowed from Dan North’s writings on Behavior-driven development. They can be a useful way of writing user stories/specifications for work, but they can also be helpful in explaining what piece of behavior a unit test is testing. After all — almost every test is going to consist of setting up the test ( GIVEN), running the piece of code you’re testing ( WHEN) and verifying that it did what you expected it to do ( THEN).

You’ll see this is essentially a very ordinary — and indeed short — function. We create the object, we run the function, and then we do an assert to verify that the function returned what we expect it to. The assert statement might be new to you. It essentially means to do a logical test, and raise an exception if it fails. You can include a more helpful error message after a comma like I did there. I recommend you do so - when, in a years time you write code that breaks this unit test, you know why. You will usually use at least one assert in a test - but you don't need to.

Note that you didn’t need to add any decorators. By having the test in a .py file with a name starting with test , and a function with a name starting with test , Pytest knew to run it.

Now, lets run the test. Call python -m pytest from your shell in project_folder . You should see something like:

The test successfully ran! If you’d like, run python -m pytest -v and you'll see a PASSED or FAILED result next to each individual test instead of a summary.

Making a test fail

Now — let’s break it again, for learnings sake. Change User.get_display_name in models/user.py to be the following:

The enthusiastic coder may have already noted that this will always return the username — which, we know will fail our test. So — let’s run it again with python -m pytest . You'll see something like:

Unit tests will usually fail because of an assert that failed, or an exception that was thrown during the test. As you can see here, you'll see that a test failed, and a stack trace as to how.

If your project uses Python’s logging library, any log messages that are a higher level than WARNING will be shown with any failing tests.

Parametrizing Tests

Before we start, we’re going to need to fix User.get_display_name (since we intentionally broke the model in the last test). Change User.get_display_name in models/user.py back to the following:

Our first test only tested one specific part of the specification — namely that preferred_name would be returned if User has a real_name , preferred_name and username . This obviously would not pick up some bugs. For instance, if the function wrongly always returned preferred_name (like we forced it to always return username when we intentionally broke it) then that unit test would pass.

The obvious solution is that we need to duplicate the test a bunch of times with all of the potential combinations. So — with over-application of our ‘Copy’ and ‘Paste’ functions, we’ll end up with four different copies of our test function, that could test the entire spec. That’s not too ludicrous, but there is a better way.

After all — if you want to test your string manipulation function with the entire Big List of Naughty Strings you probably don’t want to make a few hundred copies of the same function.

Lots of testing frameworks — including Pytest — support a feature called ‘parametrization’. This means that can get data from an iterator and run a test with that data

Yes, you can use generators — as you can use any iterators. It’s outside the scope of this article to discuss exactly how. The official documentation for Pytest has a pretty in-depth look at how you can use parametrization that is worth a read.

So — here is our new version of tests/test_user.py that uses parametrization to test all combinations of input.

The magic part is the @pytest.mark.parametrize . It marks the test for Pytest as one that uses parametrization. The first parameter ( "username,real_name,preferred_name,expected" ) is a list of the parameters on the test function to fill in. The second parameter is the actual data source.

The data source in this case is TEST_DISPLAY_NAME_DATA . It's a list of tuples. Each item in the list is one of the test cases. Each item in those tuples is one of the parameters to fill in.

It’s quite common to have a field like expected , because often you're testing for specific output for each input value. Sometimes you may not need it - for instance, if you're testing that none of your test cases causes the code you're testing to raise an exception.

We now run these tests the same way:

As you can see, it now ran the four different tests. If one of those failed, you would see which test case failed so you can work to making it pass.

What’s next?

At the point, you should have enough tools to start adding some unit tests to your own projects, and start making them more reliable. Pick one of your python projects, and add a unit test or two — you’ll gain far more out of using this and thinking through the process than by just reading articles.

But, I recognize that it’s not always easy to add unit testing to a legacy project. In my next article, I’m going to work through some strategies for adding unit tests to legacy projects generally, and some tools that Pytest provides that can help you do so.