Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›pytest & Testing›Testing Interview Questions

Interview Prep

Testing Interview Questions

This lesson covers fundamental and advanced pytest concepts frequently tested in technical interviews for software engineering roles. Understanding these mechanics is essential for building robust, maintainable, and scalable test suites in professional environments. Mastering these patterns allows developers to isolate failures effectively and optimize their testing workflow during development.

Understanding Fixtures and Dependency Injection

Fixtures are the backbone of modular testing. Instead of writing repetitive setup and teardown code inside every test function, you define a fixture that pytest will inject into your test function as an argument. The power of fixtures lies in their declarative nature and their ability to handle resources cleanly, even if a test fails. When you decorate a function with `@pytest.fixture`, pytest automatically handles the lifecycle of that object. You should understand how scopes—function, module, package, and session—impact performance; for instance, a session-scoped fixture runs once for the entire test suite, whereas function-scoped fixtures recreate state for every test. This efficiency allows developers to maintain clean state without paying the cost of redundant initialization, which is crucial when dealing with complex objects or heavy database connections in a production environment.

import pytest

@pytest.fixture
def user_db():
    # Setup: create connection
    db = {"user": "admin"}
    yield db  # Provide the object to the test
    # Teardown: close connection
    db.clear()

def test_user_exists(user_db):
    # The fixture is injected as an argument
    assert user_db["user"] == "admin"

Parameterized Testing for Comprehensive Coverage

Parameterization is the most efficient way to test multiple scenarios with a single test logic block. Instead of duplicating a test five times for five different inputs, you use the `@pytest.mark.parametrize` decorator to feed different sets of inputs to the same test function. This is critical for data-driven testing where you need to ensure boundary conditions, edge cases, and typical inputs all yield the expected behavior. The framework generates individual test instances for each set of parameters, which means a failure in one parameter set does not stop the execution of others. By separating the test logic from the test data, you keep your suite readable and maintainable. This technique is vital in technical interviews because it demonstrates your ability to write compact, expressive tests that achieve high branch coverage without code bloat.

import pytest

@pytest.mark.parametrize("input_val, expected", [
    (1, 2), # Happy path
    (0, 1), # Boundary case
    (-1, 0) # Edge case
])
def test_increment(input_val, expected):
    # Logic is separated from the data points
    assert input_val + 1 == expected

Handling Expected Exceptions

A robust test suite must verify not only that code works under normal conditions but also that it fails correctly under exceptional circumstances. The `pytest.raises` context manager is the standard way to verify that a function raises a specific exception when given invalid input. Simply checking if an exception is raised is often insufficient; you should also assert the exception message or state to ensure the code is failing for the right reason. This prevents 'false passes' where an exception is caught but it originated from an unexpected part of the code path. Understanding how to inspect the raised exception object is a sign of a senior engineer who writes tests that are truly descriptive. This pattern enforces defensive programming and ensures your API contracts are strictly followed throughout the evolution of the codebase.

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def test_divide_by_zero():
    # Ensure the code fails as expected for invalid inputs
    with pytest.raises(ValueError, match="zero"):
        divide(10, 0)

Mocking with Monkeypatch

In a real-world system, testing components in total isolation is difficult if they rely on external resources like system time, environment variables, or global configuration. The `monkeypatch` fixture allows you to modify attributes, dictionary entries, or environment variables for the duration of a test and automatically restores them afterward. This is fundamental for 'mocking out' dependencies that might be slow or non-deterministic. By replacing an external call with a controlled substitute, you convert integration tests into fast, deterministic unit tests. Mastery of `monkeypatch` shows that you understand the boundaries of your unit under test. It allows you to simulate network failures, permission errors, or time-sensitive logic without relying on the underlying system to behave in a specific way during the test run.

import os
import pytest

def get_config():
    return os.getenv("API_KEY", "default")

def test_mock_env(monkeypatch):
    # Simulate a specific environment variable
    monkeypatch.setenv("API_KEY", "secret_key")
    assert get_config() == "secret_key"

Understanding Markers and Test Selection

Markers allow you to categorize and filter your tests, which is essential for managing large-scale projects where running every test is infeasible for every commit. You can define custom markers to group tests by functionality, performance characteristics, or environment requirements. For instance, you might use a `@pytest.mark.slow` marker to skip long-running tests during local development while running them in the CI pipeline. This ability to selectively execute tests based on metadata demonstrates architectural awareness and a focus on developer productivity. Furthermore, markers can be used to pass configuration data to fixtures, enabling advanced setups where specific tests trigger unique test environment states. Effectively managing your test suite execution through marking ensures that your team stays agile and feedback cycles remain short regardless of the size of the total codebase.

import pytest

@pytest.mark.smoke
def test_critical_path():
    # Tests marked as 'smoke' are core requirements
    assert True

# Run with: pytest -m smoke
# This selects only the critical tests.

Key points

  • Fixtures provide a clean way to manage test state and dependencies through dependency injection.
  • Scope control in fixtures directly impacts the performance and efficiency of your testing suite.
  • Parameterization enables high test coverage by testing multiple scenarios with minimal code duplication.
  • Context managers like pytest.raises are essential for verifying that error handling logic behaves as expected.
  • Monkeypatching allows you to safely isolate code from external side effects like environment variables or global settings.
  • Markers facilitate organized test selection, allowing for faster feedback loops during rapid development cycles.
  • Separating test data from test logic improves readability and makes test maintenance significantly easier over time.
  • Effective testing requires verifying both successful outcomes and failure paths to ensure robust system behavior.

Common mistakes

  • Mistake: Relying on print() statements for debugging. Why it's wrong: It clutters output and is hard to manage in CI/CD. Fix: Use pytest's built-in logging capture or the --pdb flag to inspect state interactively.
  • Mistake: Overusing global fixtures. Why it's wrong: It increases test coupling and obscures dependencies. Fix: Use function-scoped fixtures and inject them explicitly into the tests that require them.
  • Mistake: Writing 'god tests' that test multiple features at once. Why it's wrong: A failure in one part causes a cascade, making it hard to identify the root cause. Fix: Follow the 'one assertion per test' principle or ensure tests are atomic.
  • Mistake: Hardcoding test data directly inside test functions. Why it's wrong: It makes tests brittle and hard to maintain as requirements change. Fix: Use parametrization or external fixtures to decouple test logic from data.
  • Mistake: Ignoring test isolation. Why it's wrong: Tests that modify global state (like environment variables or databases) can affect subsequent tests. Fix: Use yield fixtures to ensure setup and teardown steps reset the environment.

Interview questions

What is the primary purpose of the 'assert' statement in pytest, and how does it differ from traditional unit testing assertions?

In pytest, the assert statement is the core mechanism for validation. Unlike other frameworks that require specialized methods like 'assertEqual' or 'assertTrue', pytest leverages standard Python assert statements. The genius of this approach is that pytest uses assertion rewriting to inspect the state of the expression at the moment of failure. When an assertion fails, pytest provides a detailed introspection report showing the values of all variables involved, which saves developers from writing custom error messages.

Can you explain the role of a pytest fixture and why it is considered superior to the setup and teardown methods found in older testing frameworks?

Fixtures are modular, reusable pieces of code that provide a fixed baseline for tests, such as database connections or configuration files. They are superior to setup/teardown methods because they utilize dependency injection. A test function requests a fixture as an argument, making the dependency explicit. Furthermore, fixtures support sophisticated scoping—such as 'module', 'class', or 'session'—allowing you to share resources efficiently across multiple tests, reducing redundant initialization time while maintaining clean, isolated test environments.

Compare the use of 'parametrize' versus using a 'for loop' inside a single test function. When should you choose one over the other?

While both approaches can iterate through data, 'parametrize' is the professional standard. If you use a for loop inside a test, the entire test stops at the first assertion failure, masking subsequent bugs. In contrast, 'parametrize' generates individual test cases for each set of inputs. If one input fails, the others continue to run, providing a complete report of which specific scenarios failed. This granular reporting is essential for identifying edge cases and debugging efficiently.

What is the purpose of the 'conftest.py' file in a pytest project, and how does it affect test discovery?

The 'conftest.py' file acts as a local configuration or plugin file. It is where you define fixtures that should be shared across multiple test files within a directory or subdirectories. When pytest runs, it automatically discovers 'conftest.py' files and loads the fixtures defined within them without requiring manual imports. This structure promotes a clean architecture, as common setup logic is abstracted away, allowing test files to focus strictly on validating behavior rather than boilerplate resource management.

How would you implement a test that ensures a specific exception is raised, and why is this practice important?

To verify exceptions, you use the 'pytest.raises' context manager. For example: 'with pytest.raises(ValueError): func()'. This is crucial because testing the 'happy path' is insufficient for production-grade software. You must ensure the system handles invalid input or internal errors predictably. By explicitly asserting that an exception is raised, you guarantee that your error-handling logic is functioning as intended, preventing the application from entering an unstable state when it encounters unexpected data or conditions.

How does pytest handle test discovery, and what steps would you take to run a specific subset of tests in a large-scale project?

By default, pytest discovers tests by recursively looking for files starting with 'test_' or ending with '_test.py', then searching for functions or classes prefixed with 'test'. To run a subset, you can use marker expressions with '-m', file path filtering, or keyword expressions with '-k'. For example, 'pytest -k user_profile' runs only tests whose names contain that string. This is vital for large projects, as it enables developers to achieve rapid feedback loops by targeting only the relevant module during active development.

All pytest & Testing interview questions →

Check yourself

1. When designing a test suite, why is it considered better to use dependency injection via fixtures rather than calling setup/teardown functions directly?

  • A.It forces the test to run in a specific alphabetical order.
  • B.It allows fixtures to manage their own scope and lifecycle automatically, preventing state leakage.
  • C.It makes the test code faster by bypassing the need for object instantiation.
  • D.It is the only way to avoid using assertions in a test file.
Show answer

B. It allows fixtures to manage their own scope and lifecycle automatically, preventing state leakage.
Fixture injection enables automatic setup/teardown and scoping (e.g., session vs. function). Calling functions directly leads to manual cleanup, which is error-prone. The other options are incorrect because fixtures don't affect order, don't necessarily increase speed, and don't restrict assertion usage.

2. What is the primary benefit of using @pytest.mark.parametrize?

  • A.It automatically generates documentation for the test functions.
  • B.It prevents the test from failing if the inputs are incorrect.
  • C.It allows you to run the same test logic against multiple datasets without duplicating code.
  • D.It increases the test coverage score in the coverage report.
Show answer

C. It allows you to run the same test logic against multiple datasets without duplicating code.
Parametrization promotes DRY (Don't Repeat Yourself) principles by separating data from logic. It does not automatically document code, mask errors, or magically improve coverage metrics.

3. What happens if a test function has a yield fixture, and an exception is raised during the test?

  • A.The fixture teardown code after the yield will still execute.
  • B.The test runner crashes immediately and leaves the environment in an inconsistent state.
  • C.The fixture teardown is skipped to preserve the state for debugging.
  • D.The test is automatically retried by the pytest runner.
Show answer

A. The fixture teardown code after the yield will still execute.
Pytest guarantees that code after the 'yield' statement in a fixture executes regardless of whether the test passed or raised an exception. This is essential for proper resource cleanup. The other options describe incorrect behaviors.

4. Which of the following describes the most effective way to test code that interacts with an external API?

  • A.Send real requests to the API during every test run to ensure live connectivity.
  • B.Hardcode the API responses directly into the test file.
  • C.Mock the network layer to return deterministic responses, ensuring the test is fast and independent.
  • D.Check that the network is available before running the test suite.
Show answer

C. Mock the network layer to return deterministic responses, ensuring the test is fast and independent.
Mocking external dependencies ensures tests are deterministic, fast, and independent of external service uptime. Real network calls make tests flaky, while hardcoding responses inside the test function mixes concerns.

5. Why should you avoid having tests that are dependent on the execution order of other tests?

  • A.Because pytest runs tests in random order by default.
  • B.Because dependent tests make it impossible to run tests in parallel or in isolation.
  • C.Because test execution order is determined by the operating system's CPU scheduler.
  • D.Because pytest requires a specific decorator to allow dependencies.
Show answer

B. Because dependent tests make it impossible to run tests in parallel or in isolation.
Independent tests allow for parallel execution and allow you to run individual tests without running the entire suite. Options A, C, and D are factually incorrect regarding how the pytest runner works.

Take the full pytest & Testing quiz →

← PreviousTesting Patterns — AAA, Factory

pytest & Testing

25 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app