Unittest - built -in Python tester

онлайн тренажер по питону
Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction to Testing with unittest

Testing is a fundamental part of the software development process, ensuring code reliability and quality. In the Python ecosystem, the unittest module serves as the standard solution for creating and running automated tests.

unittest is a built‑in Python library based on the xUnit architecture, providing developers with a complete set of tools for writing unit tests, verifying business logic, and controlling the behavior of functions and classes. Its main advantage is that it is part of the Python standard library and requires no additional installations.

What Is unittest and Why It Matters

unittest is a framework for writing and executing tests in Python that follows test‑driven development (TDD) principles. It enables the creation of repeatable, isolated, and automated tests that help:

  • Detect bugs early in the development cycle
  • Maintain code stability during refactoring
  • Document expected function behavior
  • Increase confidence in software quality

Getting Started with unittest

Basic Test Structure

To begin using unittest, import the module and create a test class:

import unittest

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(2 + 3, 5)
        
    def test_multiply(self):
        self.assertEqual(4 * 5, 20)

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

Key Structural Principles

Every test class must inherit from unittest.TestCase. Test methods start with the test_ prefix, allowing the framework to automatically discover and run them. Calling unittest.main() executes all discovered tests in the current module.

Test Lifecycle and Fixtures

Setup and Teardown Methods

unittest provides special methods for preparing and cleaning up the test environment:

class TestDatabase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Runs once before all tests in the class"""
        cls.db_connection = create_test_database()
    
    @classmethod
    def tearDownClass(cls):
        """Runs once after all tests in the class"""
        cls.db_connection.close()
    
    def setUp(self):
        """Runs before each test"""
        self.test_data = ["item1", "item2", "item3"]
    
    def tearDown(self):
        """Runs after each test"""
        self.test_data.clear()

Fixture Levels

Fixtures in unittest operate at different scopes:

  • setUp() and tearDown() – per‑test
  • setUpClass() and tearDownClass() – per‑class
  • setUpModule() and tearDownModule() – per‑module

Assertion Methods

Core Assertion Methods

unittest offers a rich set of assertions for various checks:

Method Description Example
assertEqual(a, b) Verifies that a equals b self.assertEqual(2+2, 4)
assertNotEqual(a, b) Verifies that a does not equal b self.assertNotEqual(2+2, 5)
assertTrue(x) Checks that x is truthy self.assertTrue(5 > 3)
assertFalse(x) Checks that x is falsy self.assertFalse(5 < 3)
assertIs(a, b) Verifies object identity self.assertIs(x, None)
assertIsNot(a, b) Verifies objects are not identical self.assertIsNot(x, None)
assertIsNone(x) Checks that x is None self.assertIsNone(result)
assertIsNotNone(x) Checks that x is not None self.assertIsNotNone(result)
assertIn(a, b) Ensures a is contained in b self.assertIn('test', text)
assertNotIn(a, b) Ensures a is not in b self.assertNotIn('bug', text)
assertIsInstance(a, b) Checks that a is an instance of b self.assertIsInstance(result, list)
assertNotIsInstance(a, b) Ensures a is not an instance of b self.assertNotIsInstance(result, str)

Specialized Assertions

Method Description Example
assertAlmostEqual(a, b) Checks approximate equality self.assertAlmostEqual(3.14159, 3.14, places=2)
assertGreater(a, b) Verifies a > b self.assertGreater(10, 5)
assertLess(a, b) Verifies a < b self.assertLess(5, 10)
assertGreaterEqual(a, b) Verifies a >= b self.assertGreaterEqual(10, 10)
assertLessEqual(a, b) Verifies a <= b self.assertLessEqual(5, 10)
assertRegex(text, regex) Checks that text matches a regular expression self.assertRegex(email, r'.*@.*\..*')
assertCountEqual(a, b) Verifies sequences contain the same elements, regardless of order self.assertCountEqual([1,2,3], [3,2,1])

Testing Exceptions

Common Approaches

unittest provides several ways to assert exceptions:

class TestExceptions(unittest.TestCase):
    def test_division_by_zero(self):
        with self.assertRaises(ZeroDivisionError):
            result = 10 / 0
    
    def test_value_error_with_message(self):
        with self.assertRaises(ValueError) as context:
            int("not_a_number")
        self.assertIn("invalid literal", str(context.exception))
    
    def test_custom_exception(self):
        with self.assertRaises(CustomException):
            raise_custom_exception()

Asserting Exception Messages

def test_exception_message(self):
    with self.assertRaisesRegex(ValueError, "must be positive"):
        validate_positive_number(-5)

Working with Mock Objects

Mocking Basics

The unittest.mock module lets you replace real objects and functions with controllable test doubles:

from unittest.mock import Mock, patch, MagicMock

class TestWithMocks(unittest.TestCase):
    def test_function_with_mock(self):
        mock_obj = Mock()
        mock_obj.method.return_value = 42
        
        result = mock_obj.method()
        self.assertEqual(result, 42)
        mock_obj.method.assert_called_once()

Patching Functions and Methods

class TestPatching(unittest.TestCase):
    @patch('requests.get')
    def test_api_call(self, mock_get):
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {'data': 'test'}
        
        response = make_api_call()
        self.assertEqual(response['data'], 'test')
    
    def test_with_context_manager(self):
        with patch('os.path.exists') as mock_exists:
            mock_exists.return_value = True
            result = check_file_exists('/path/to/file')
            self.assertTrue(result)

Running Tests

From the Command Line

# Run all tests in the current directory
python -m unittest discover

# Run a specific module
python -m unittest test_module

# Run a specific class
python -m unittest test_module.TestClass

# Run a specific method
python -m unittest test_module.TestClass.test_method

# Run with verbose output
python -m unittest -v test_module

Programmatic Execution

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

Creating Test Suites

def create_test_suite():
    suite = unittest.TestSuite()
    suite.addTest(TestMath('test_add'))
    suite.addTest(TestMath('test_multiply'))
    return suite

runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(create_test_suite())

Parameterizing Tests

Using subTest

class TestParametrized(unittest.TestCase):
    def test_multiple_values(self):
        test_cases = [
            (2, 3, 5),
            (1, 4, 5),
            (0, 0, 0),
            (-1, 1, 0)
        ]
        
        for a, b, expected in test_cases:
            with self.subTest(a=a, b=b):
                result = add(a, b)
                self.assertEqual(result, expected)

Generating Parameterized Tests Dynamically

def create_parametrized_test(a, b, expected):
    def test_method(self):
        self.assertEqual(add(a, b), expected)
    return test_method

# Dynamically add tests to the class
test_cases = [(2, 3, 5), (1, 4, 5), (0, 0, 0)]
for i, (a, b, expected) in enumerate(test_cases):
    test_method = create_parametrized_test(a, b, expected)
    setattr(TestMath, f'test_add_{i}', test_method)

Skipping and Conditional Test Execution

Decorators for Test Control

import sys

class TestConditional(unittest.TestCase):
    @unittest.skip("Temporarily skipped")
    def test_skip_always(self):
        pass
    
    @unittest.skipIf(sys.version_info < (3, 8), "Requires Python 3.8+")
    def test_skip_if_condition(self):
        pass
    
    @unittest.skipUnless(sys.platform.startswith("win"), "Windows‑only")
    def test_skip_unless_condition(self):
        pass
    
    @unittest.expectedFailure
    def test_expected_failure(self):
        self.assertEqual(1, 2)  # Intentional failure

Integration with IDEs and CI/CD

IDE Support

unittest is supported by all major IDEs:

  • PyCharm: built‑in test runner with GUI
  • VS Code: Python Test Explorer extension
  • Sublime Text: UnitTesting package

GitHub Actions Integration

name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9, '3.10', '3.11']

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    
    - name: Run tests
      run: |
        python -m unittest discover -s tests -p "test_*.py" -v

Practical Examples

Testing a Web API

import unittest
from unittest.mock import patch, Mock
import requests

class TestAPIClient(unittest.TestCase):
    def setUp(self):
        self.base_url = "https://api.example.com"
        self.client = APIClient(self.base_url)
    
    @patch('requests.get')
    def test_get_user_success(self, mock_get):
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {'id': 1, 'name': 'Test User'}
        mock_get.return_value = mock_response
        
        user = self.client.get_user(1)
        
        self.assertEqual(user['id'], 1)
        self.assertEqual(user['name'], 'Test User')
        mock_get.assert_called_once_with(f"{self.base_url}/users/1")
    
    @patch('requests.get')
    def test_get_user_not_found(self, mock_get):
        mock_response = Mock()
        mock_response.status_code = 404
        mock_get.return_value = mock_response
        
        with self.assertRaises(UserNotFoundError):
            self.client.get_user(999)

Testing File Operations

import unittest
import tempfile
import os
from unittest.mock import patch

class TestFileOperations(unittest.TestCase):
    def setUp(self):
        self.temp_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.temp_dir, 'test.txt')
    
    def tearDown(self):
        if os.path.exists(self.test_file):
            os.remove(self.test_file)
        os.rmdir(self.temp_dir)
    
    def test_write_read_file(self):
        content = "Test content"
        write_to_file(self.test_file, content)
        
        self.assertTrue(os.path.exists(self.test_file))
        read_content = read_from_file(self.test_file)
        self.assertEqual(content, read_content)
    
    @patch('builtins.open', side_effect=IOError("Permission denied"))
    def test_file_permission_error(self, mock_open):
        with self.assertRaises(IOError):
            write_to_file(self.test_file, "content")

Best Practices

Test Naming Conventions

class TestUserValidator(unittest.TestCase):
    def test_valid_email_passes_validation(self):
        # Descriptive test name
        pass
    
    def test_invalid_email_raises_validation_error(self):
        pass
    
    def test_empty_email_raises_validation_error(self):
        pass

Organizing Test Code

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calculator = Calculator()
    
    def test_add_positive_numbers(self):
        result = self.calculator.add(2, 3)
        self.assertEqual(result, 5)
    
    def test_add_negative_numbers(self):
        result = self.calculator.add(-2, -3)
        self.assertEqual(result, -5)
    
    def test_add_zero(self):
        result = self.calculator.add(5, 0)
        self.assertEqual(result, 5)

Comparison with Other Testing Frameworks

unittest vs pytest

Feature unittest pytest
Built‑in Part of the standard library Requires separate installation
Syntax Class‑based, more formal Simple function‑based tests
Fixtures setUp/tearDown methods @pytest.fixture decorators
Parameterization Via subTest @pytest.mark.parametrize built‑in
Plugins Limited ecosystem Rich plugin ecosystem
Reporting Basic text output Detailed reports, HTML output

unittest vs nose2

Feature unittest nose2
Test discovery Basic Enhanced
Configuration Programmatic Configuration files
Plugins Limited Plugin system
Performance Standard Optimized

Debugging and Diagnostics

Using Debug Information

class TestDebug(unittest.TestCase):
    def test_with_debug_info(self):
        expected = 10
        actual = complex_calculation()
        
        self.assertEqual(
            actual, 
            expected, 
            f"Expected {expected}, but got {actual}. "
            f"Input parameters: {self.input_params}"
        )
    
    def test_with_detailed_assertions(self):
        result = process_data(self.test_data)
        
        # Verify several aspects of the result
        self.assertIsInstance(result, dict)
        self.assertIn('status', result)
        self.assertEqual(result['status'], 'success')
        self.assertGreater(len(result['data']), 0)

Custom Assertion Methods

class ExtendedTestCase(unittest.TestCase):
    def assertBetween(self, value, min_val, max_val):
        """Asserts that value lies within [min_val, max_val]"""
        if not (min_val <= value <= max_val):
            raise AssertionError(f"{value} is not between {min_val} and {max_val}")
    
    def assertListAlmostEqual(self, list1, list2, places=7):
        """Asserts approximate equality of two numeric lists"""
        self.assertEqual(len(list1), len(list2))
        for a, b in zip(list1, list2):
            self.assertAlmostEqual(a, b, places=places)

Performance and Optimization

Speeding Up Test Execution

class TestPerformance(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # Create expensive resources once
        cls.expensive_resource = create_expensive_resource()
    
    def test_fast_operation(self):
        # Reuse shared resource
        result = self.expensive_resource.quick_operation()
        self.assertIsNotNone(result)
    
    @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS'), "Skipping slow tests")
    def test_slow_operation(self):
        # Slow test that can be omitted in CI
        result = time_consuming_operation()
        self.assertTrue(result)

Parallel Execution

# Use unittest-parallel for concurrent test runs
# pip install unittest-parallel

# Run: python -m unittest_parallel

Frequently Asked Questions

What is unittest and why is it important?

unittest is Python’s built‑in framework for automated testing. It helps ensure code quality and reliability by providing repeatable, isolated tests.

How does unittest differ from pytest?

unittest requires test classes that inherit from TestCase and uses a more formal syntax, whereas pytest lets you write simple functions. unittest is part of the standard library; pytest needs a separate install.

Can unittest test asynchronous code?

Yes. Starting with Python 3.8, unittest supports async tests via unittest.IsolatedAsyncioTestCase. For earlier versions, additional libraries are needed.

How should I organize my test suite?

Create a dedicated tests directory mirroring your source layout. Each test file should start with test_ and contain classes derived from unittest.TestCase.

Does unittest support mock objects?

Absolutely. The unittest.mock module offers a full toolkit for stubs, patching, and interaction verification.

How can I run only specific tests?

Use the command line to target a module, class, or method, e.g., python -m unittest test_module.TestClass.test_method. Decorators also allow selective skipping.

Is parameterization possible in unittest?

While unittest lacks built‑in parametric tests like pytest, you can use subTest() for sub‑tests or dynamically generate test methods.

How does unittest integrate with CI/CD pipelines?

Unittest runs easily from the command line, making it compatible with most CI systems (GitHub Actions, GitLab CI, Jenkins, etc.).

Can I create custom assertion methods?

Yes. Subclass unittest.TestCase and add your own assertion helpers.

How do I test exceptions in unittest?

Use the context manager with self.assertRaises(ExceptionType): or assertRaisesRegex() to also check the exception message.

Conclusion

unittest remains one of the most reliable and versatile testing tools for Python. Its key strengths are built‑in availability, stability, extensive functionality, and seamless integration with a wide range of development environments.

Although newer frameworks like pytest offer more concise syntax and additional features, unittest continues to be an excellent choice for projects that prioritize simplicity of setup and minimal external dependencies.

Mastering unittest gives developers a solid foundation in testing principles and can serve as a stepping stone toward more advanced testing frameworks. Regular use of unittest leads to higher‑quality, more maintainable code.

News