Skip to content
C

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

TypeWhat it Checks
Unit TestingA single, small piece of code (usually one function) in isolation
Integration TestingMultiple pieces working together correctly
API TestingAn 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`

python
def 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`

python
import 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:

bash
python -m unittest test_calculator.py

Output:

...
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

Explanation of the Code

  • Every test class inherits from unittest.TestCase.
  • Every test method must start with `test_` — this is how unittest recognizes 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 with with) checks that the code inside the block correctly raises the expected exception.

Common unittest Assertions

MethodChecks
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_ — otherwise unittest silently ignores them.
  • Testing too much in one test method — each test should ideally check one specific behavior, making failures easier to pinpoint.

Important Points

  • unittest is built into Python — no installation required.
  • Test files are usually named test_<module_name>.py by convention.

Practice

  1. Write unittest tests for a function is_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.

bash
pip install pytest

Simple Example

File: `test_calculator.py`

python
from 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:

bash
pytest

Output:

===================== 3 passed in 0.02s =====================

Explanation of the Code

  • pytest uses plain assert statements — no special self.assertEqual() methods needed, which is one reason it's considered more readable.
  • pytest automatically discovers any file named test_*.py (or *_test.py) and any function starting with test_, without needing a class at all.
  • pytest.raises() works just like unittest's assertRaises, but as a simpler standalone function.

Comparison Table — unittest vs pytest

unittestpytest
InstallationBuilt-inRequires pip install pytest
Syntaxself.assertEqual(a, b)Plain assert a == b
StructureRequires a TestCase classPlain functions work fine
FixturessetUp()/tearDown() methods@pytest.fixture decorator (more flexible)
PopularityStandard, widely knownMore popular in modern real-world projects

Important Points

  • pytest is generally preferred in real-world professional Python development for its simplicity and powerful features.
  • Both frameworks can run tests written for unittest-style classes — pytest is 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

python
import 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.fixture marks sample_numbers() as a fixture — a function that provides reusable test data.
  • Any test function that includes sample_numbers as a parameter automatically receives the fixture's return value — pytest handles the connection behind the scenes.
  • Both test_sum and test_length reuse the exact same setup, without repeating the list definition in each test.

setUp() in unittest (Equivalent Concept)

python
class 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.
  • pytest fixtures are more flexible and reusable across multiple test files than unittest's setUp().

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

python
from 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.mock module provides patch(), Mock(), and MagicMock() 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

python
import 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

python
import 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 after yield would 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.

bash
pip 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"
bash
python -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

  1. Write a unittest test for a function square(n) that returns n ** 2.
  2. Write a pytest test for a function is_palindrome(s).
  3. Write a test that checks a function correctly raises a ValueError for invalid input.
  4. Write a pytest fixture that provides a sample list of student names, and use it in two different tests.
  5. Run your test suite using pytest and confirm all tests pass.

Intermediate

  1. Write unit tests for the Expense Tracker functions from the Functions file (add_expense, calculate_total).
  2. Use unittest.mock to test a function that calls an external API, without making a real network request.
  3. Write an integration test for a function that inserts a record into an in-memory SQLite database and then reads it back.
  4. Write API tests for a Flask application's / and one other route, using Flask's test client.
  5. Run pytest --cov on a small project and identify which lines aren't covered by tests.

Challenge

  1. 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.
  2. Write tests (with mocking) for the Weather API Application mini project, simulating both a successful response and a connection error.
  3. Set up a small project with both unit tests and one integration test, and generate a coverage report showing at least 80% coverage.

Mock Test

  • Testing - Quick Test

    10 questions covering unittest, pytest, fixtures, mocking, integration testing, API testing, and code coverage.

    10 questions · 10 min · Easy
    Start Mock Test

Coding Problems