Python is an amazing programing language that can be used for almost anything. Once you’ve learned the basics of writing Python code, a great next step to improve the code that you’re writing is to introduce automated tests. Let’s take a look at how you can get started with testing in Python, what automated tests are, the various kinds of tests, testing tools that we can use in Python, and finally, writing a few example tests.

If you haven’t already started programming with Python, you should check out our Introduction to Python Development course.

What are Automated Tests?

Automated tests are bits of code that exercise the code that you’re writing. There are many benefits to having a test suite for your applications, including:

  • Design Feedback: Tests force us to use our code as we’re writing it so we can experience what it’s like to work with our code early and potentially see areas of bad design.
  • Living “documentation”: Tests, if small and focused, can provide a great starting point for a new developer to learn how the parts of a system work.
  • Regression Prevention: For the most part, we won’t be deleting tests as we continue to work on an application. By running all of the tests that we have when we make a change, we can have more confidence that our new changes haven’t broken old code.

The biggest business reason for testing is probably the regression prevention portion because preventing bugs from being introduced is a great thing. From a developer’s point of view, I would consider the other two benefits to be the real reasons to write tests with the regression prevention being a bonus.

Types of Tests

The term “automated tests” is a blanket statement that describes anytime that we write code to exercise and verify something about our code, but there are different categories of automated tests that we should understand. Here are some of the types of tests from the lowest to the highest level:

  • Unit tests: Unit tests should test a single “unit” in isolation. In an object-oriented language like Python, a single unit test would usually test something about a method on an object or a stand-alone function. These tests help us ensure that the smallest pieces work properly before we start combining them to form our application.
  • Integration tests: Integration tests involve testing the points where our units interact with one another. These tests tend to involve more setup and we’ll typically write fewer of these than unit tests.
  • Functional tests (also called acceptance tests): Functional tests actually test the application from the outside. If we were building a web application, this test would start by driving a browser to a route on our running application and then making an assertion about the web page that was returned. Ideally, these tests don’t exercise the application code directly in the test but instead, exercise the external API. Functional tests tend to take longer to run and are less likely to be broken into small segments. We might have an acceptance test that goes through the entire process of selecting an item in our store all the way through making a purchase (using a fake payment gateway).

Now that we have an idea of how tests are classified and why we’d write specific types of tests, let’s take a look at the tools that we can use in Python to actually write and run the tests.

Testing Frameworks

Python is a “batteries included” language with an extremely useful standard library. It might come as no surprise that it also includes not one, but two testing frameworks. For creating a well-structured test suite, we have the unittest module that we can use to write tests. This is the sort of test that we’ll usually write. Python’s other built-in testing framework is a little more unique in that it allows us to run the example code that we write in the docstrings for our functions, methods, and classes. These tests are run using the doctest module.

In addition to the frameworks that are part of the standard library, there are also other testing frameworks that are very popular including nose and pytest (my favorite). Which tool you use is really a matter of preference and what your team is comfortable using.

Let’s get a better grasp of what automated testing looks like in each of these tools by writing some tests.

Writing Your First Tests

The easiest kind of code to test is a pure function that, when given the input “A”, it will always return the output “B”. Let’s write a simple function that squares a given number:

square.py

def square(x):
    pass

Here are the tests in doctest format, unittest, and pytest:

square.py

def square(x):
    """
    square returns the 2nd power of the argument

    >>> square(4)
    16
    """
    pass

# Begin unittest section
import unittest

class TestSquare(unittest.TestCase):
    def test_square_with_positive_integers(self):
        self.assertEqual(square(2), 4)

# Begin pytest section
import pytest

def test_square():
    assert square(2) == 4

Let’s first run our unittest test:

$ pip3.7 install --user pytest
...
$ python3.7 -m unittest square.py
F
======================================================================
FAIL: test_square_with_positive_integers (square.TestSquare)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/cloud_user/square.py", line 15, in test_square_with_positive_integers
    self.assertEqual(square(2), 4)
AssertionError: None != 4

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

Failures displayed by unittest are pretty well written, but let’s compare them to the doctest and pytest output before implementing the function. Here’s the doctest output:

$ python3.7 -m doctest square.py
**********************************************************************
File "/home/cloud_user/square.py", line 5, in square.square
Failed example:
    square(4)
Expected:
    16
Got nothing
**********************************************************************
1 items had failures:
   1 of   1 in square.square
***Test Failed*** 1 failures.

Doctest works by taking the examples that look like a REPL prompt from our docstring and ensuring that, if this code were run in a REPL, it would return the same output.

Running the pytest tests now:

$ pytest square.py
============================= test session starts ==============================
platform linux -- Python 3.7.2, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
rootdir: /home/cloud_user, inifile:
collected 2 items

square.py FF                                                             [100%]

=================================== FAILURES ===================================
________________ TestSquare.test_square_with_positive_integers _________________

self = 

    def test_square_with_positive_integers(self):
>       self.assertEqual(square(2), 4)
E       AssertionError: None != 4

square.py:15: AssertionError
_________________________________ test_square __________________________________

    def test_square():
>       assert square(2) == 4
E       assert None == 4
E        +  where None = square(2)

square.py:21: AssertionError
=========================== 2 failed in 0.03 seconds ===========================

Pytest wraps the unittest module and will run any function/method it finds that starts with test_ so it ran both the unittest test and the pytest function.

Finally, let’s implement the function and then you can run the commands again to see that the tests pass.

square.py

def square(x):
    """
    square returns the 2nd power of the argument

    >>> square(4)
    16
    """
    return x * x

# unit tests omitted

Now if we run our tests again, we should see dots indicating that our tests passed.

$ python3.7 -m unittest square.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
$ python3.7 -m doctest square.py
$ pytest square.py
================================= test session starts ==================================
platform linux -- Python 3.7.2, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
rootdir: /home/cloud_user, inifile:
collected 2 items

square.py ..                                                                     [100%]

=============================== 2 passed in 0.02 seconds ===============================

Digging Deeper into Python

This hopefully gives you a better idea of what automated testing is, why it’s useful, and how you can write some simple tests. This post only scratches the surface of testing, and there’s a lot more that you can learn on the subject. If you are just digging into Python in general, then I would recommend taking a look at our Intro to Python Development course.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *