Unit Testing!
The Automated Testing Paradigm & CI/CD
Section titled “The Automated Testing Paradigm & CI/CD”- 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:
- Fast: Must execute in milliseconds.
- Isolated: Must not depend on the state, execution order, or output of other tests.
- Repeatable: Must return the exact same result every time, regardless of environment.
- Self-Checking: Must automatically detect Pass/Fail states (no human review needed).
- Timely: Writing the test should only add ~30% overhead to the total coding time.
Testing Frameworks (unittest vs. pytest)
Section titled “Testing Frameworks (unittest vs. pytest)”- 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 Pythonassertstatements. It is highly recommended to usepytestoverunittestin 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 unittestimport 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()
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 requestsfrom unittest import TestCasefrom unittest.mock import patch
# 1. The target function that normally hits the real internetdef 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 Suiteclass 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}])Test Runners and Code Coverage
Section titled “Test Runners and Code Coverage”- 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) orpytest(modern), combined with thecoveragemodule. - 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-htmlto generate an interactive webpage showing exactly which lines of code were missed by the tests.
- Install:
Multi-Environment Matrix Testing (tox)
Section titled “Multi-Environment Matrix Testing (tox)”- Concept: A CLI tool that standardizes testing. It automatically creates temporary
virtualenvsfor 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.iniconfiguration file in the root of your project.
Program (tox.ini Setup):
# --- File: tox.ini ---[tox]# Define the Python versions you want to test againstenvlist = py38, py39, py310
[testenv]# Dependencies required strictly for testingdeps = nose coverage mock
# The commands Tox will execute in every virtual environmentcommands = nosetests --with-coverage --cover-package=my_app/- Execution: Simply type
$ toxin the terminal. Tox will automatically download the interpreters, build the environments, and run the matrix.