Skip to content

Unit Testing!

  • Concept: Manual “black-box” testing (clicking through interfaces to find bugs) is unscalable and prevents rapid deployment. Automated testing relies on code to test code, forming the backbone of Continuous Integration and Continuous Delivery (CI/CD).
  • The Goal: To allow developers to perform large-scale refactoring and deploy to production with mathematical confidence that existing functionality is not broken.
  • The 5 Pillars of a Good Unit Test:
    1. Fast: Must execute in milliseconds.
    2. Isolated: Must not depend on the state, execution order, or output of other tests.
    3. Repeatable: Must return the exact same result every time, regardless of environment.
    4. Self-Checking: Must automatically detect Pass/Fail states (no human review needed).
    5. Timely: Writing the test should only add ~30% overhead to the total coding time.

  • Concept: Python requires a framework to structure and execute tests.
  • unittest (Built-in standard): Follows heavy Object-Oriented patterns (classes, setup/teardown methods). Requires strict naming (test_ prefixes).
  • pytest (The Modern Missing Piece): The current industry standard. It abandons heavy boilerplate classes in favor of simple functions and standard Python assert statements. It is highly recommended to use pytest over unittest in modern applications.

Program (unittest Structure & Debugging Cycle): Here is the logical flow of writing a test, catching a bug, and fixing it using the built-in unittest library.

# --- File: prime.py (The Target Code) ---
def is_prime(number):
# FIXED: Replaced float division (/) with floor division (//)
# FIXED: Added + 1 to the range to ensure upper bound is checked
for i in range(2, (number // 2) + 1):
if number % i == 0:
return False
return True
# --- File: test_prime.py (The Test Suite) ---
import unittest
import prime
class TestPrime(unittest.TestCase):
def setUp(self):
# Executes BEFORE every single test method (e.g., opening a DB connection)
pass
def test_is_prime(self):
# Standard unittest assertion methods
self.assertFalse(prime.is_prime(4))
self.assertTrue(prime.is_prime(13))
def tearDown(self):
# Executes AFTER every single test method (e.g., closing the DB)
pass
if __name__ == "__main__":
unittest.main()

image.png


Mocking External Dependencies (unittest.mock)

Section titled “Mocking External Dependencies (unittest.mock)”
  • Concept: If your code relies on external systems (APIs, databases), your tests will fail if those systems go offline. Mocking intercepts those calls and returns fake, predictable data, ensuring your test remains Isolated and Repeatable.
  • Methods & Syntax:
    • @patch('module.function'): A decorator that replaces the target function with a MagicMock object during the test.
    • mock_obj.return_value: Sets the static value the mock should return.
    • mock_obj.side_effect: (Missing Concept) Used when you need the mock to return a sequence of different values on consecutive calls, or to force the mock to raise an Exception (like simulating an HTTP 500 timeout).

Program:

import requests
from unittest import TestCase
from unittest.mock import patch
# 1. The target function that normally hits the real internet
def get_merge_requests():
response = requests.get('<https://git.epam.com/api/v4/projects/124721/merge_requests>')
return [{"num": item["iid"]} for item in response.json()]
# 2. The Test Suite
class TestMergeRequests(TestCase):
# Intercepts requests.get
@patch('requests.get')
def test_merge_requests(self, mock_get):
# Setup the fake JSON response
mock_get.return_value.json.return_value = [{"iid": 99, "title": "Fix Bug"}]
# Execute the function (It hits the mock, not the real URL)
result = get_merge_requests()
# Verify the parsing logic worked
self.assertEqual(result, [{"num": 99}])

  • Concept: Tools that automatically discover all your tests, run them, and calculate what percentage of your actual source code was executed during the test suite.
  • The Tools: nose (legacy) or pytest (modern), combined with the coverage module.
  • Execution & Setup:
    • Install: $ pip install nose coverage
    • Run All: $ nosetests
    • Run with Coverage: $ nosetests --with-coverage --cover-package=my_app/ --cover-erase
    • Quality Gate (CI/CD): $ nosetests --with-coverage --cover-min-percentage=90 (Fails the CI build if coverage drops below 90%).
    • Visual Report: Append -cover-html to generate an interactive webpage showing exactly which lines of code were missed by the tests.

  • Concept: A CLI tool that standardizes testing. It automatically creates temporary virtualenvs for multiple different Python versions (e.g., 3.8, 3.9, 3.10), installs your application, and runs your test suite in all of them to guarantee cross-version compatibility.
  • Setup Process (The Missing Piece): You must create a tox.ini configuration file in the root of your project.

Program (tox.ini Setup):

# --- File: tox.ini ---
[tox]
# Define the Python versions you want to test against
envlist = py38, py39, py310
[testenv]
# Dependencies required strictly for testing
deps =
nose
coverage
mock
# The commands Tox will execute in every virtual environment
commands =
nosetests --with-coverage --cover-package=my_app/
  • Execution: Simply type $ tox in the terminal. Tox will automatically download the interpreters, build the environments, and run the matrix.