Python Unit Testing Best Practices Guide
What is unit testing?
Unit testing is the independent verification of the smallest testable unit of code (usually a function, class method, or module) to ensure that the output and behavior of the unit are exactly as expected given the inputs, preconditions, and execution paths.
If you write tests first and then implement functions during development, it is often called Test Driven Development (TDD). The rhythm of TDD is: Red → Green → Refactor - first write a failing test (red), then make it pass with the least amount of code (green), and finally optimize the code structure under the protection of the test. This approach can force modular code with high cohesion and low coupling from the root.
Why do we need unit testing?
Many developers feel that "writing tests is a waste of time in functional development", but if you look at the entire project life cycle, the benefits of unit testing far outweigh the investment:
- Quickly verify functional correctness: After writing the core logic, run the test within a few seconds to catch most low-level errors.
- Prevent regression bugs: When refactoring, optimizing, or adding new features, old tests will tell you where they are broken immediately.
- Forcing code quality: If you want to write testable code, you must avoid strong coupling, global dependencies and overly complex functions, which will naturally dismantle the module more clearly.
- Replace part of the documentation: The test case itself is the most vivid and never-failed example of "how to use the code and how not to use it."
Core Practices of Modern Unit Testing
1. Prefer pytest over unittest
Python standard libraryunittesthas a long history, but nowpytesthas become the de facto standard in the community. Its obvious advantages are:
- Minimalist syntax: No need for class inheritance, no need to remember various things
assertEqual、assertTrueWait for redundant assertion methods, use them directlyassert。 - Powerful fixture system: Conveniently reuse test data and dependencies.
- Parameterized Testing: A single decorator can cover a large number of boundary conditions.
- Massive plug-ins: Coverage, Mock, concurrent testing, Benchmark... all have ready-made solutions.
- Seamless compatibility: can be run directly
unittestThe migration cost for old tests written is almost zero.
Let’s look at a simple comparison:
The simplicity is clear at a glance.
2. Follow the FIRST principle
A good test case should meet these five characteristics, and none of them can be missing:
- Fast (fast): single test in milliseconds, full test in seconds/minutes, otherwise no one would want to run it frequently.
- Isolated: Each test does not depend on the status of other tests, nor does it affect the external environment (such as databases, files, etc.).
- Repeatable (repeatable): Run repeatedly in the same environment, the results will always be consistent.
- Self-validating (self-validating): The test result can only be "passed" or "failed", and there is no need to manually check the log.
- Timely (timely): It is best to write simultaneously with the functional code, and no later than the function is online.
3. Pay reasonable attention to test coverage
usepytest-covThe plug-in can check the proportion of code covered by tests with one click:
💡 The higher the coverage rate, the better! Core business logic should be covered 100% as much as possible, and the general tool library can be kept above 80%. Don't write tests for illogical code such as getters/setters just to increase coverage. The cost-effectiveness is too low.
Complete example of modern testing
Let's first write a small tool library to be tested - a dictionary class that supports attribute access and a student class with fractional verification:
1. Basic function and exception testing
The test code directly corresponds to edge cases, normal usage and exception throwing:
2. Use fixtures to reuse test data
When multiple tests require the same initialization data, you can use@pytest.fixtureExtract the creation logic to avoid duplication of code:
3. Cover boundary conditions with parametric tests
Manually listing various boundary inputs is too tedious.@pytest.mark.parametrizeIt can be done in one go:
Commonly used pytest execution commands
It is recommended to integrate these commands into your daily development and CI processes and press the "Check" shortcut key at any time.
Best practice suggestions for avoiding pitfalls
- Test naming should be self-explanatory: Recommended
test_功能_场景_预期The format allows people to understand at a glance what is being measured, what the situation is, and what to expect. - Tests must be independent: Each test creates and destroys data by itself, and does not rely on global state or side effects of the previous test.
- Avoid testing implementation details: Test "what the code should do" rather than "how the code is implemented internally". For example, determine whether the sorting results are in order instead of worrying about whether to use bubble or quick sort.
- Mock external dependencies: If the code depends on the database, API or file system, use
pytest-mockorunittest.mockReplace to ensure the speed and stability of the test. - Integrate tests into CI/CD: Automatically run a full set of tests for each submission. If the test fails, it will be repaired immediately to avoid continuing development with problems.
Summarize
The core of modern Python unit testing has changed from "simple functional verification" to "using the pytest ecosystem to build efficient and maintainable test suites." Remember these key actions:
- Prefer using pytest, embracing concise syntax and rich ecology.
- Adhere to the FIRST principle and make testing a trustworthy safety net.
- The core logic is fully covered, and the edge code is appropriately relaxed.
- Treat testing as part of the development process rather than as an afterthought.
When you integrate these practices into your daily life, not only will the code be more robust, but the later maintenance costs will also be significantly reduced, and the entire development experience will be much smoother!

