Unit Testing in Python: Quick Tutorial and 4 Best Practices

What Is Unit Testing in Python? 

Unit testing is a software testing method by which individual units of source code are tested to determine if they are fit for use. These units could be sets of one or more program modules, with their associated control data and operating procedures. In simpler terms, it’s a way to test if your code does what you think it does.

Python, a versatile, high-level programming language, offers a built-in module for unit testing known as unittest. This module provides a rich set of tools for constructing and running tests, allowing developers to easily create thorough validation and verification plans for their code. By using unittest or similar testing frameworks like pytest, we can create automated test cases for our software, saving us the time and effort of manually running the same tests over and over again.

Some developers are hesitant to invest time in unit testing, because the value is not obvious. However, the benefits far outweigh the initial investment. Writing and maintaining a good suite of unit tests can help ensure code quality, facilitate code refactoring and maintenance, improve development efficiency, and support agile development and continuous integration.

Importance of Unit Testing in Python 

Improves Code Quality

Unit testing in Python goes beyond verifying code functionality; it actually provides a concrete definition of how a code segment should behave. As Python is a dynamically-typed language, it’s particularly susceptible to runtime errors. Properly designed unit tests can act as an initial line of defense, identifying defects or regressions introduced during ongoing development. When regressions arise, Python’s testing frameworks can promptly flag them, providing an early warning that ensures quality and saves considerable remediation efforts later.

Facilitates Code Refactoring and Maintenance

Python’s dynamic nature, while offering flexibility, can sometimes lead to complex or less readable code structures. As a result, periodic refactoring becomes imperative to maintain a clean codebase. Comprehensive unit tests in Python can offer assurance during refactoring processes, enabling developers to enhance code design or performance without fear of inadvertently altering its intended behavior. Additionally, when adjusting Python codebases, unit tests can demystify existing functionality, ensuring that any modifications don’t introduce inadvertent issues.

Improves Development Efficiency

Automated unit tests in Python can deliver instant feedback post code modifications, streamlining the development workflow. Early detection of defects, especially in Python where issues might only manifest at runtime, is crucial. Automating these tests means developers are freed from the repetitive task of manually executing them, allowing continuous integration tools to take over, running tests after each code commit, and offering immediate insights.

Supports Agile Development and Continuous Integration

In Python development environments that adopt agile methodologies, aiming for frequent, incremental code deliveries, unit tests can be indispensable. They provide timely validations, indicating whether recent changes are primed for release or require further refinement. Moreover, in continuous integration scenarios, where merging new code changes with the broader codebase is a regular occurrence, Python unit tests help ensure that these integrations are smooth and don’t disrupt existing functionalities.

Tutorial: Writing a Unit Test in Python With the unittest Framework 

The unittest unit testing framework, which is part of the Python distribution, was originally inspired by JUnit. It enables test automation, makes it possible to share code to configure tests, and can aggregate tests into collections. Let’s see how to use unittest to create a simple unit test.

Creating Test Classes

The first step in writing a unit test in Python is to create a test class. A test class is essentially a container for your test methods. To create a test class, we define a class that inherits from unittest.TestCase. This base class provides a framework for the test methods, and our test class will contain methods that perform the actual tests.

import unittest

class TestMyFunction(unittest.TestCase):

    pass

In the above code snippet, TestMyFunction is our test class. By convention, test classes should start with the word “Test”. The body of the class is currently empty (pass), but this is where we will add our test methods.

Writing Test Methods

After creating our test class, the next step is to write test methods. Test methods are functions that perform the actual testing. Each test method in a test class should test a specific aspect of the function or method you’re testing.

import unittest

def my_function(x, y):

    return x+y

class TestMyFunction(unittest.TestCase):

    def test_positive_numbers(self):

        result = my_function(1, 2)

        self.assertEqual(result, 3)

In the above code snippet, test_positive_numbers is a test method that checks if my_function correctly adds two positive numbers. The my_function is the function we are testing, and assertEqual is an assertion method that checks if two values are equal.

Note: In this example, the method under test is also in the test class. This is not normally the case. In a real application the method we test is somewhere else, and the test class would have only tests and nothing else.

Running Tests with unittest

Once we’ve written our test class and test methods, the next step is to run the tests. Python’s unittest module includes a command-line interface for running tests. To run a test, we simply need to call unittest.main() in our script.

if __name__ == '__main__':

    unittest.main()

When we run the script, unittest.main() will run all the test methods in our test class. If a test method passes, it means that the function or method we’re testing behaved as expected under the conditions of the test.

Assertions in unittest

Assertions are the heart of any test method. They are the conditions that must be met for a test to pass. The unittest module provides a number of assertion methods that we can use to test our code. 

Some of the most commonly used assertion methods include assertEqual(a, b), which checks if a equals b, assertTrue(x) which checks if x is true, and assertFalse(x) which checks if x is false.

def test_my_function(self):

    self.assertEqual(my_function(1, 2), 3)

    self.assertTrue(my_function(0, 0) == 0)

    self.assertFalse(my_function(-1, -1) == 0)

In the above code snippet, we have three assertions. The first asserts that my_function(1, 2) equals 3, the second asserts that my_function(0, 0) is True, and the third asserts that my_function(-1, -1) is False.

Related content: Read our guide to unit testing framework (coming soon)

Best Practices for Unit Testing in Python 

Here are a few best practices that can make unit testing in Python more effective.

1. Writing Readable Tests

One of the keys to effective unit testing is writing tests that are easy to read. Readable tests are easier to understand, maintain, and debug. To write readable tests, we should use descriptive names for our test methods, and we should structure our tests in a way that clearly shows what we’re testing, what the expected outcome is, and how the test verifies the outcome.

2. Keeping Tests Simple and Focused

Each test should be simple and focused on a single aspect of the function or method being tested. Having focused tests makes it easier to identify the cause of a test failure. If a test fails, we know exactly what aspect of the function or method is not working as expected. To keep tests simple and focused, we should avoid testing multiple aspects of a function or method in a single test.

3. Using Proper Naming Conventions

Proper naming conventions are crucial for readability and maintainability. By convention, test classes should start with the word “Test”, and test methods should start with the word “test”. This makes it easy to identify test classes and methods in our code. Additionally, the names of our test methods should be descriptive of what the test does.

4. Paying Attention to Code Coverage

Code coverage refers to the percentage of your code that is covered by your tests. A high code coverage means that most of your code is tested, which reduces the risk of bugs. To ensure high code coverage, we should strive to write tests for all paths through our code. This includes all possible input values, all branches of conditional statements, and all exceptions that could be raised.

Supercharging Your Test Process with Codefresh

Codefresh has comprehensive support for all different types of testing and is very flexible about how and where you can test your applications. More specifically you run tests:

  • Before compiling the code
  • After compilation
  • During container building
  • Before a deployment
  • After a deployment (smoke tests)

At the same time you can use Codefresh both with mocked services as well as real services. You can launch service containers inside a pipeline and attach required services such as databases, queue, caches in the service that is under test.

Finally Codefresh integrates with the Allure testing framework for producing not only test reports, but also historical trends for your testing results.

This means Codefresh covers all testing needs from all software phases in the most effective way.

Learn more about Codefresh