Python Unittest Module

PythonPythonBeginner
Practice Now

This tutorial is from open-source community. Access the source code

Introduction

In this lab, you will learn how to use Python's built - in unittest module. This module offers a framework for organizing and running tests, which is a crucial part of software development to ensure your code works correctly.

You'll also learn to create and run basic test cases, and test for expected errors and exceptions. The unittest module simplifies the process of creating test suites, running tests, and verifying results. The file teststock.py will be created during this lab.

Creating Your First Unit Test

Python's unittest module is a powerful tool that offers a structured way to organize and execute tests. Before we dive into writing our first unit test, let's understand some key concepts. Test fixtures are methods like setUp and tearDown that help prepare the environment before a test and clean it up afterward. Test cases are individual units of testing, test suites are collections of test cases, and test runners are responsible for executing these tests and presenting the results.

In this first step, we're going to create a basic test file for the Stock class, which is already defined in the stock.py file.

  1. First, let's open the stock.py file. This will help us understand the Stock class we'll be testing. By looking at the code in stock.py, we can see how the class is structured, what attributes it has, and what methods it provides. To view the contents of the stock.py file, run the following command in your terminal:
cat stock.py
  1. Now, it's time to create a new file named teststock.py using your preferred text editor. This file will contain our test cases for the Stock class. Here's the code you need to write in the teststock.py file:
## teststock.py

import unittest
import stock

class TestStock(unittest.TestCase):
    def test_create(self):
        s = stock.Stock('GOOG', 100, 490.1)
        self.assertEqual(s.name, 'GOOG')
        self.assertEqual(s.shares, 100)
        self.assertEqual(s.price, 490.1)

if __name__ == '__main__':
    unittest.main()

Let's break down the key components of this code:

  • import unittest: This line imports the unittest module, which provides the necessary tools and classes for writing and running tests in Python.
  • import stock: This imports the module that contains our Stock class. Without this import, we wouldn't be able to access the Stock class in our test code.
  • class TestStock(unittest.TestCase): We create a new class named TestStock that inherits from unittest.TestCase. This makes our TestStock class a test case class, which can contain multiple test methods.
  • def test_create(self): This is a test method. In the unittest framework, all test methods must start with the prefix test_. This method creates an instance of the Stock class and then uses the assertEqual method to check if the attributes of the Stock instance match the expected values.
  • assertEqual: This is a method provided by the TestCase class. It checks if two values are equal. If they are not equal, the test will fail.
  • unittest.main(): When this script is executed directly, unittest.main() will run all the test methods in the TestStock class and display the results.
  1. After writing the code in the teststock.py file, save it. Then, run the following command in your terminal to execute the test:
python3 teststock.py

You should see output similar to this:

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

OK

The single dot (.) in the output indicates that one test has passed successfully. If a test fails, you'll see an F instead of the dot, along with detailed information about what went wrong in the test. This output helps you quickly identify if your code is working as expected or if there are any issues that need to be fixed.

✨ Check Solution and Practice

Extending Your Test Cases

Now that you've created a basic test case, it's time to expand your testing scope. Adding more tests will help you cover the remaining functionality of the Stock class. This way, you can ensure that all aspects of the class work as expected. We'll modify the TestStock class to include tests for several methods and properties.

  1. Open the teststock.py file. Inside the TestStock class, we're going to add some new test methods. These methods will test different parts of the Stock class. Here's the code you need to add:
def test_create_keyword_args(self):
    s = stock.Stock(name='GOOG', shares=100, price=490.1)
    self.assertEqual(s.name, 'GOOG')
    self.assertEqual(s.shares, 100)
    self.assertEqual(s.price, 490.1)

def test_cost(self):
    s = stock.Stock('GOOG', 100, 490.1)
    self.assertEqual(s.cost, 49010.0)

def test_sell(self):
    s = stock.Stock('GOOG', 100, 490.1)
    s.sell(20)
    self.assertEqual(s.shares, 80)

def test_from_row(self):
    row = ['GOOG', '100', '490.1']
    s = stock.Stock.from_row(row)
    self.assertEqual(s.name, 'GOOG')
    self.assertEqual(s.shares, 100)
    self.assertEqual(s.price, 490.1)

def test_repr(self):
    s = stock.Stock('GOOG', 100, 490.1)
    self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")

def test_eq(self):
    s1 = stock.Stock('GOOG', 100, 490.1)
    s2 = stock.Stock('GOOG', 100, 490.1)
    self.assertEqual(s1, s2)

Let's take a closer look at what each of these tests does:

  • test_create_keyword_args: This test checks if you can create a Stock object using keyword arguments. It verifies that the object's attributes are set correctly.
  • test_cost: This test checks if the cost property of a Stock object returns the correct value, which is calculated as the number of shares multiplied by the price.
  • test_sell: This test checks if the sell() method of a Stock object correctly updates the number of shares after selling some.
  • test_from_row: This test checks if the from_row() class method can create a new Stock instance from a data row.
  • test_repr: This test checks if the __repr__() method of a Stock object returns the expected string representation.
  • test_eq: This test checks if the __eq__() method correctly compares two Stock objects to see if they are equal.
  1. After adding these test methods, save the teststock.py file. Then, run the tests again using the following command in your terminal:
python3 teststock.py

If all the tests pass, you should see output like this:

......
----------------------------------------------------------------------
Ran 7 tests in 0.001s

OK

The seven dots in the output represent each test. Each dot indicates that a test has passed successfully. So, if you see seven dots, it means all seven tests have passed.

✨ Check Solution and Practice

Testing for Exceptions

Testing is a crucial part of software development, and one important aspect of it is to ensure that your code can handle error conditions properly. In Python, the unittest module provides a convenient way to test if specific exceptions are raised as expected.

  1. Open the teststock.py file. We're going to add some test methods that are designed to check for exceptions. These tests will help us make sure that our code behaves correctly when it encounters invalid input.
def test_shares_type(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(TypeError):
        s.shares = '50'

def test_shares_value(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(ValueError):
        s.shares = -50

def test_price_type(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(TypeError):
        s.price = '490.1'

def test_price_value(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(ValueError):
        s.price = -490.1

def test_attribute_error(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(AttributeError):
        s.share = 100  ## 'share' is incorrect, should be 'shares'

Now, let's understand how these exception tests work.

  • The with self.assertRaises(ExceptionType): statement creates a context manager. This context manager checks if the code inside the with block raises the specified exception.
  • If the expected exception is raised within the with block, the test passes. This means that our code is correctly detecting the invalid input and raising the appropriate error.
  • If no exception is raised or a different exception is raised, the test fails. This indicates that our code might not be handling the invalid input as expected.

These tests are designed to verify the following scenarios:

  • Setting the shares attribute to a string should raise a TypeError because shares should be a number.
  • Setting the shares attribute to a negative number should raise a ValueError since the number of shares cannot be negative.
  • Setting the price attribute to a string should raise a TypeError because price should be a number.
  • Setting the price attribute to a negative number should raise a ValueError as the price cannot be negative.
  • Attempting to set a non-existent attribute share (note the missing 's') should raise an AttributeError because the correct attribute name is shares.
  1. After adding these test methods, save the teststock.py file. Then, run all the tests using the following command in your terminal:
python3 teststock.py

If everything is working correctly, you should see output indicating that all 12 tests have passed. The output will look like this:

............
----------------------------------------------------------------------
Ran 12 tests in 0.002s

OK

The twelve dots represent all the tests you've written so far. There were 7 tests from the previous step, and we've just added 5 new ones. This output shows that your code is handling exceptions as expected, which is a great sign of a well-tested program.

✨ Check Solution and Practice

Running Selected Tests and Using Test Discovery

The unittest module in Python is a powerful tool that allows you to test your code effectively. It provides several ways to run specific tests or automatically discover and run all tests in your project. This is very useful because it helps you focus on specific parts of your code during testing or quickly check the entire project's test suite.

Running Specific Tests

Sometimes, you may want to run only specific test methods or test classes instead of the entire test suite. You can achieve this by using the pattern option with the unittest module. This gives you more control over which tests are executed, which can be handy when you're debugging a particular part of your code.

  1. To run just the tests related to creating a Stock object:
python3 -m unittest teststock.TestStock.test_create

In this command, python3 -m unittest tells Python to run the unittest module. teststock is the name of the test file, TestStock is the name of the test class, and test_create is the specific test method we want to run. By running this command, you can quickly check if the code related to creating a Stock object is working as expected.

  1. To run all tests in the TestStock class:
python3 -m unittest teststock.TestStock

Here, we omit the specific test method name. So, this command will execute all the test methods within the TestStock class in the teststock file. This is useful when you want to check the overall functionality of the Stock object's test cases.

Using Test Discovery

The unittest module can automatically discover and run all test files in your project. This saves you the trouble of manually specifying each test file to run, especially in larger projects with many test files.

  1. Rename the current file to follow the test discovery naming pattern:
mv teststock.py test_stock.py

The test discovery mechanism in unittest looks for files that follow the naming pattern test_*.py. By renaming the file to test_stock.py, we make it easier for the unittest module to find and run the tests in this file.

  1. Run the test discovery:
python3 -m unittest discover

This command tells the unittest module to automatically discover and run all test files that match the pattern test_*.py in the current directory. It will search through the directory and execute all the test cases found in the matching files.

  1. You can also specify a directory to search for tests:
python3 -m unittest discover -s . -p "test_*.py"

Where:

  • -s . specifies the directory to start discovery (current directory in this case). The dot (.) represents the current directory. You can change this to another directory path if you want to search for tests in a different location.
  • -p "test_*.py" is the pattern to match test files. This ensures that only files with names starting with test_ and having the .py extension are considered as test files.

You should see all 12 tests run and pass, just like before.

  1. Rename the file back to the original name for consistency with the lab:
mv test_stock.py teststock.py

After running the test discovery, we rename the file back to its original name to keep the lab environment consistent.

By using test discovery, you can easily run all tests in a project without having to specify each test file individually. This makes the testing process more efficient and less error-prone.

✨ Check Solution and Practice

Summary

In this lab, you have learned how to use Python's unittest module to create and run automated tests. You created a basic test case by extending the unittest.TestCase class, wrote tests to verify the normal functioning of a class's methods and properties, and created tests to check for appropriate exceptions in error conditions. You also learned how to run specific tests and use test discovery.

Unit testing is a fundamental skill in software development, ensuring code reliability and correctness. Writing thorough tests helps catch bugs early and gives confidence in your code's behavior. As you develop Python applications, consider adopting a test-driven development (TDD) approach, writing tests before implementing functionality for more robust and maintainable code.