Testing
Automated testing with unittest and pytest, fixtures, mocking external dependencies with unittest.mock, integration testing, API testing, and code coverage.
How do you know your code actually works — not just today, but after you change something six months from now? Manually running your program and checking the output by eye doesn't scale. Automated testing lets you write code that checks your code, catching bugs before they reach real users.
1. What is Testing?
What is it?
Testing means writing code that automatically verifies your program behaves correctly — checking that functions return the expected results, and catching bugs early.
Definition: Software testing is the process of verifying that a program behaves as expected, using automated checks rather than manual inspection.
Why do we use it?
- Catch bugs early — before they reach real users.
- Confidence when changing code — if you modify a function and all tests still pass, you can be reasonably confident you didn't break anything.
- Documentation — tests show, through real examples, exactly how a function is supposed to behave.
- Required in professional environments — nearly every real software job expects you to write tests.
Types of Testing
| Type | What it Checks |
|---|---|
| Unit Testing | A single, small piece of code (usually one function) in isolation |
| Integration Testing | Multiple pieces working together correctly |
| API Testing | An API's endpoints behave correctly (right status codes, right data) |
2. Unit Testing with unittest
What is it?
unittest is Python's built-in testing framework, included with every Python installation — no separate install needed.
Simple Example
File: `calculator.py`
pythondef add(a, b): return a + b def divide(a, b): if b == 0: raise ValueError("Cannot divide by zero") return a / b
File: `test_calculator.py`
pythonimport unittest from calculator import add, divide class TestCalculator(unittest.TestCase): def test_add(self): self.assertEqual(add(2, 3), 5) self.assertEqual(add(-1, 1), 0) def test_divide(self): self.assertEqual(divide(10, 2), 5) def test_divide_by_zero(self): with self.assertRaises(ValueError): divide(10, 0) if __name__ == "__main__": unittest.main()
Run it:
bashpython -m unittest test_calculator.py
Output:
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OKExplanation of the Code
- Every test class inherits from
unittest.TestCase. - Every test method must start with `test_` — this is how
unittestrecognizes which methods are actual tests to run. self.assertEqual(actual, expected)checks that two values match; if they don't, the test fails and reports the mismatch.self.assertRaises(ValueError)(used withwith) checks that the code inside the block correctly raises the expected exception.
Common unittest Assertions
| Method | Checks |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | x is True |
assertFalse(x) | x is False |
assertIsNone(x) | x is None |
assertRaises(Error) | Code raises the given exception |
assertIn(a, b) | a is inside b |
Common Mistakes
- Forgetting test method names must start with
test_— otherwiseunittestsilently ignores them. - Testing too much in one test method — each test should ideally check one specific behavior, making failures easier to pinpoint.
Important Points
unittestis built into Python — no installation required.- Test files are usually named
test_<module_name>.pyby convention.
Practice
- Write
unittesttests for a functionis_even(n)— testing both an even and an odd input.
3. Testing with pytest
What is it?
pytest is a hugely popular third-party testing framework — simpler and more concise than unittest, and the standard choice in most professional Python projects today.
bashpip install pytest
Simple Example
File: `test_calculator.py`
pythonfrom calculator import add, divide import pytest def test_add(): assert add(2, 3) == 5 assert add(-1, 1) == 0 def test_divide(): assert divide(10, 2) == 5 def test_divide_by_zero(): with pytest.raises(ValueError): divide(10, 0)
Run it:
bashpytest
Output:
===================== 3 passed in 0.02s =====================Explanation of the Code
pytestuses plainassertstatements — no specialself.assertEqual()methods needed, which is one reason it's considered more readable.pytestautomatically discovers any file namedtest_*.py(or*_test.py) and any function starting withtest_, without needing a class at all.pytest.raises()works just likeunittest'sassertRaises, but as a simpler standalone function.
Comparison Table — unittest vs pytest
unittest | pytest | |
|---|---|---|
| Installation | Built-in | Requires pip install pytest |
| Syntax | self.assertEqual(a, b) | Plain assert a == b |
| Structure | Requires a TestCase class | Plain functions work fine |
| Fixtures | setUp()/tearDown() methods | @pytest.fixture decorator (more flexible) |
| Popularity | Standard, widely known | More popular in modern real-world projects |
Important Points
pytestis generally preferred in real-world professional Python development for its simplicity and powerful features.- Both frameworks can run tests written for
unittest-style classes —pytestis largely backward-compatible.
4. Fixtures
What is it?
A fixture is reusable setup code that prepares something needed by multiple tests — like a sample object, a test database connection, or test data — avoiding repetition across test functions.
Simple Example — pytest Fixture
pythonimport pytest @pytest.fixture def sample_numbers(): return [10, 20, 30, 40, 50] def test_sum(sample_numbers): assert sum(sample_numbers) == 150 def test_length(sample_numbers): assert len(sample_numbers) == 5
Explanation of the Code
@pytest.fixturemarkssample_numbers()as a fixture — a function that provides reusable test data.- Any test function that includes
sample_numbersas a parameter automatically receives the fixture's return value —pytesthandles the connection behind the scenes. - Both
test_sumandtest_lengthreuse the exact same setup, without repeating the list definition in each test.
setUp() in unittest (Equivalent Concept)
pythonclass TestCalculator(unittest.TestCase): def setUp(self): self.numbers = [10, 20, 30, 40, 50] def test_sum(self): self.assertEqual(sum(self.numbers), 150)
Explanation: setUp() runs automatically before every single test method in the class, providing a fresh setup each time.
Important Points
- Fixtures avoid repeating setup code across many tests.
pytestfixtures are more flexible and reusable across multiple test files thanunittest'ssetUp().
5. Mocking
What is it?
Mocking replaces a real, often unpredictable or slow component (like an API call, a database, or the current time) with a fake, controlled stand-in — so tests can run quickly, reliably, and without needing real external services.
Why do we use it?
Imagine testing a function that calls a weather API. You don't want your tests to depend on the internet being available, the API being online, or the weather actually being sunny that day. Mocking replaces the real API call with a fake, predictable response.
Simple Example
pythonfrom unittest.mock import patch def get_weather_description(weather_api_call): data = weather_api_call() return f"The weather is {data['condition']}" def test_get_weather_description(): with patch("__main__.weather_api_call") as mock_api: mock_api.return_value = {"condition": "sunny"} result = get_weather_description(mock_api) assert result == "The weather is sunny"
Explanation of the Code
patch()temporarily replaces the real function/object with a "mock" — a fake stand-in that you fully control.mock_api.return_value = {...}tells the mock exactly what to return when called, regardless of what the real API might actually say.- This lets you test your code's logic (
get_weather_description) completely independently of whether the real weather API is working.
Real-World Example
Mocking is essential for testing code that depends on databases, external APIs, sending emails, or the current date/time — anything slow, unpredictable, or with real-world side effects you don't want triggered during testing.
Common Mistakes
- Over-mocking, to the point where the test no longer verifies anything meaningful about your actual code's behavior.
- Forgetting to reset/verify mocks between tests, causing one test's setup to accidentally affect another.
Important Points
- Mocking isolates the code being tested from external dependencies.
- Python's built-in
unittest.mockmodule providespatch(),Mock(), andMagicMock()for this purpose.
6. Integration Testing
What is it?
While unit tests check one function in isolation, integration tests check that multiple parts of a system work correctly together — like a function that both queries a database and processes the results.
Simple Example
pythonimport sqlite3 import pytest def setup_test_db(): conn = sqlite3.connect(":memory:") # temporary, in-memory database conn.execute("CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT)") conn.execute("INSERT INTO students (name) VALUES ('Aditi')") conn.commit() return conn def get_student_count(conn): return conn.execute("SELECT COUNT(*) FROM students").fetchone()[0] def test_student_count_integration(): conn = setup_test_db() assert get_student_count(conn) == 1 conn.close()
Explanation: sqlite3.connect(":memory:") creates a temporary database that exists only in memory for the duration of the test — perfect for integration tests, since it behaves like a real database without needing an actual persistent file.
Important Points
- Integration tests are typically slower than unit tests, since they involve more moving parts.
- A healthy test suite usually has many unit tests and fewer, more targeted integration tests.
7. API Testing
What is it?
Testing that your API's endpoints return the correct status codes and data, using the same principles covered above.
Simple Example — Testing a Flask Application
pythonimport pytest from app import app # your Flask application @pytest.fixture def client(): app.config["TESTING"] = True with app.test_client() as client: yield client def test_home_page(client): response = client.get("/") assert response.status_code == 200 def test_api_students(client): response = client.get("/api/students") assert response.status_code == 200 data = response.get_json() assert isinstance(data, list)
Explanation of the Code
- Flask provides a built-in
test_client(), which simulates HTTP requests to your application without actually starting a real server. yield client(inside the fixture) is a generator-based fixture pattern — it hands the client to the test, and any code afteryieldwould run as cleanup once the test finishes.
Important Points
- API tests confirm status codes, response structure, and data correctness — without needing to manually click through a browser or use a tool like Postman every time.
8. Code Coverage
What is it?
Coverage measures what percentage of your actual code gets executed by your test suite — helping identify parts of your codebase that have no tests at all.
bashpip install pytest-cov pytest --cov=calculator
Sample Output:
Name Stmts Miss Cover
------------------------------------
calculator.py 8 1 88%Explanation
- This shows that 88% of
calculator.py's executable lines were run at least once during testing — the remaining 12% represents code paths (perhaps an edge case) with no test coverage yet.
Common Mistakes
- Chasing 100% coverage as a goal in itself — high coverage doesn't guarantee tests are actually meaningful; it just means the code was executed, not necessarily verified correctly.
- Ignoring coverage reports entirely and never checking which parts of the codebase are untested.
Important Points
- Coverage is a useful signal, not a perfect measure of test quality.
- Aim for solid coverage on critical logic, rather than obsessing over a specific percentage everywhere.
Common Beginner Mistakes — Summary for This Section
- Forgetting test functions/methods must start with
test_. - Writing tests that check too many things at once, making failures hard to diagnose.
- Not using mocks for external dependencies (APIs, databases, current time), making tests slow or unreliable.
- Treating code coverage percentage as the sole measure of test quality.
Cheat Sheet — Testing
python# unittest import unittest class TestSomething(unittest.TestCase): def test_example(self): self.assertEqual(1 + 1, 2) # pytest def test_example(): assert 1 + 1 == 2 @pytest.fixture def sample_data(): return [1, 2, 3] def test_with_fixture(sample_data): assert len(sample_data) == 3 # mocking from unittest.mock import patch with patch("module.function") as mock_func: mock_func.return_value = "fake result"
bashpython -m unittest test_file.py pytest pytest --cov=module_name
Interview Questions
Q1. What is the difference between unit testing and integration testing? Answer: Unit testing checks a single function or component in isolation. Integration testing checks that multiple components work correctly together (e.g., a function combined with a real database).
Q2. What is the difference between `unittest` and `pytest`? Answer: unittest is built into Python and requires test classes with self.assertEqual()-style methods. pytest is a third-party library using plain assert statements and simple functions, generally considered more concise and flexible, especially with fixtures.
Q3. What is mocking, and why is it useful? Answer: Mocking replaces a real, often slow or unpredictable dependency (like an API or database) with a controlled fake stand-in, allowing tests to run quickly and reliably without depending on external systems.
Q4. What is a test fixture? Answer: Reusable setup code that prepares data or conditions needed by multiple tests, avoiding repetition.
Q5. What does code coverage measure? Answer: The percentage of a codebase's lines that are actually executed when the test suite runs — useful for spotting untested code, though not a complete measure of test quality on its own.
Practice Questions
Beginner
- Write a
unittesttest for a functionsquare(n)that returnsn ** 2. - Write a
pytesttest for a functionis_palindrome(s). - Write a test that checks a function correctly raises a
ValueErrorfor invalid input. - Write a
pytestfixture that provides a sample list of student names, and use it in two different tests. - Run your test suite using
pytestand confirm all tests pass.
Intermediate
- Write unit tests for the Expense Tracker functions from the Functions file (
add_expense,calculate_total). - Use
unittest.mockto test a function that calls an external API, without making a real network request. - Write an integration test for a function that inserts a record into an in-memory SQLite database and then reads it back.
- Write API tests for a Flask application's
/and one other route, using Flask's test client. - Run
pytest --covon a small project and identify which lines aren't covered by tests.
Challenge
- Write a complete test suite (using
pytest) for the Student Record Management System mini project from the File Handling file, covering adding, viewing, and saving records. - Write tests (with mocking) for the Weather API Application mini project, simulating both a successful response and a connection error.
- Set up a small project with both unit tests and one integration test, and generate a coverage report showing at least 80% coverage.