Testing
Testing in Python rests on two things candidates usually know separately and almost never together. The first is a model of how many tests of each kind to keep and what stays real at each tier. The second is the mechanics of pytest and unittest.mock, where most of the interesting work happens before the run, not during it — parametrization expands at collection time, a fixture is supplied by matching the parameter name, and patch replaces a name inside one specific module's namespace.
The Python-specific part is entirely about names and timing. The line from x import y creates your module's own reference to the object — patching x.y will never touch it, hence the rule "patch where the name is looked up, not where it is defined". A Mock answers any attribute access with a fresh mock, so an assertion missing the assert_ prefix does not fail — it silently returns a truthy object. A default argument is evaluated once when def executes, so injecting via client=HttpClient() yields a single instance shared by the whole process. And a fixture's scope is not a configuration detail but a decision about how many tests share one piece of mutable state. Each mechanism is worked through in the layers below.
Topic map
- The test pyramid — a model of the suite's composition by count and cost per run, not by how important the checks are.
- Unit and integration tests — the boundary runs along the dependencies; this is also where
pytestmechanics live — collection, fixtures, scopes, parametrization. - Mocks and patch — what test doubles actually replace, how
Mockis built, and why the patch target goes where the name is looked up. - Dependency injection — a seam designed into the code — constructor, parameter,
Protocol— versus reaching in withpatch. - Linters and static analysis —
flake8,pylint,ruff,black,mypy— what each one really checks and what it never proves.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
| Calling a test a unit test while it hits a real DB or the network | The run becomes slow and flaky, and a failure no longer points at one module |
Targeting patch where the dependency is defined instead of where it is looked up | The patch lands in someone else's namespace and the code under test keeps calling the real dependency |
| Asserting on the mock's own behaviour instead of the code under test | The test proves that Mock returned the value you gave it, and stays green with completely broken logic |
Writing mock.called_once_with(...) without the assert_ prefix | Mock creates the attribute on the fly, the call returns a mock — the assertion always passes |
Keeping mutable state in a scope="session" fixture | Tests become order-dependent — failing in the suite and passing in isolation |
| Mocking the dependencies inside an integration test | There is nothing left to check — the very wiring the test existed for has been removed |
| Inverting the pyramid in favour of end-to-end tests | Feedback takes minutes, failures are hunted across the whole system, and a red CI stops being read |
Treating a clean mypy and pylint run as proof of correctness | Static analysis never executes the code and knows no values — it does not replace behavioural tests |
What interviews check
The topic is rarely the main one — it comes near the end of a section, to find out whether you have written tests yourself or only read about them. The first question is almost always the same — how a unit test differs from an integration test. "A unit test is smaller" is credited grudgingly; what is wanted is the answer phrased through dependencies — in a unit test every collaborator is substituted, so the test is deterministic and a failure points at one module, whereas an integration test deliberately keeps the DB, the HTTP client or a neighbouring service real and catches wiring problems. The pyramid follows immediately, and there the interviewer checks whether you understand that its axis is count and cost per run, not importance.
Then the conversation moves into mechanics. You are asked what a mock is and what it replaces, and nearly always for a follow-up on patch — where to put the target. The right answer is one sentence — where the name is looked up, not where it is defined — backed by the import mechanics rather than recited as a rule. Senior level is separated out by when injection beats patch, and there they want a distinction, not a slogan — injection is a seam in the design itself and survives a module rename, while patch is bound to a string path and breaks under refactoring. The classic failure looks like this — the candidate mocks everything, including the very thing under test, and cannot answer "what does this test actually prove". Expect separate questions on hands-on pytest — fixtures and their scopes, parametrization — and on linters, where the key point is that static analysis and tests cover different classes of defect.