May 17, 2026
How to Onboard a Python Team to Testing: The Story Behind My Pytest Course
A practical story behind my pytest course: how weekly shared learning sessions, paired practice, python unit testing, legacy-code refactoring, TDD/BDD thinking, and GitHub Actions Python testing helped turn pytest from a tutorial topic into a real team engineering habit.
Introduction
Most Python teams do not fail at testing because developers are lazy or because pytest is hard.
They fail because testing is introduced as an isolated skill.
Someone says, “We need more tests.” A few developers agree. Maybe a new tests/ folder appears. Maybe a pytest tutorial is shared in Slack. Maybe one person adds a GitHub Actions workflow. But the team still does not change its daily engineering habits.
That was the real problem I had to solve.
A few years ago, I was working as a team lead at an AI automotive startup in Berlin. The company was later acquired by Valeo and is now part of Seeing Machines. At that time, we had around 15 Python developers, a large legacy codebase, and a growing need to bring more of the system under test coverage (well, it was almost 0% coverage at the beginning to be fair). So, our team started weekly shared learning sessions around Brian Okken’s book Python Testing with pytest and our own production codebase.
That experience eventually became the foundation for my course, Pytest Course: Python Test Automation & GitHub Actions CI/CD. But the course was not born from a simple idea like “pytest needs another video tutorial.”
It came from a more practical question:
How do you onboard an entire Python team to testing when the codebase is already complex, delivery pressure is real, and not everyone has the same testing background?
The Real Problem Was Not Pytest Syntax
Pytest itself is approachable.
You can learn how to write a basic test function, use assert, run pytest, and understand test discovery fairly quickly. A good pytest framework tutorial can teach fixtures, parametrization, markers, mocking, coverage, and configuration.
But team adoption is different from individual learning.
In a real Python project, the difficult questions are usually:
- Where should we start testing if the codebase has almost no coverage?
- Which tests are worth writing first?
- When should we write unit tests, integration tests, or API tests?
- How do we avoid testing implementation details?
- How do we test code with legacy dependencies?
- How do we add tests without stopping feature delivery?
- How do we make testing part of implementation design instead of an afterthought?
This is why a simple pytest tutorial for Python developers is not enough. A team needs shared judgment, shared habits, and repeated practice.
Practice Also Exposed the Real Barriers
Once we started writing tests together, two problems became very visible:
- perfectionism
- legacy dependencies
Both can quietly block testing adoption.
Problem 1: Perfectionism
Perfectionism often appears with good intentions.
A developer starts testing a module and quickly sees that the design is not ideal. Dependencies are mixed together. Functions are too large. Some behavior is hard to isolate. The temptation is to pause everything and redesign the whole area before writing tests.
That sounds responsible, but it often becomes a trap.
If every test requires a large refactor first, the team will soon conclude that testing is too expensive.
The pragmatic answer was to use the scout principle:
Leave the code better than you found it.
That principle gave us a useful middle path. We did not need to rewrite the whole system. We also did not need to accept bad design forever. We could make one small improvement, add one meaningful test, reduce one dependency, clarify one boundary, or extract one function.
This mattered because testing culture has to survive normal delivery pressure. A team cannot rely on rare heroic refactoring weeks. It needs continuous improvement that fits into daily work.
That lesson became central to how I now think about python test automation. The goal is not to reach a perfect architecture before testing. The goal is to use tests to gradually make the architecture safer to change.
Problem 2: Legacy Dependencies
Legacy dependencies were harder.
Some code had dependencies that were slow, unstable, global, hard to mock, or deeply coupled to infrastructure. If you try to test every class and every method in that situation, you can waste a lot of time and create brittle tests.
The most useful idea for this problem was Michael Feathers’ concept of pinch points from Working Effectively with Legacy Code.
A pinch point is a narrow place in a system where many effects can be observed. Instead of testing every internal path, you find a point where a small number of tests can cover a meaningful cluster of behavior. I later summarized this idea in a r/PracticalTesting post about using pinch points to refactor complex legacy code.
For legacy Python systems, this idea is extremely practical.
Instead of asking, “How do we test everything?”, ask:
- Where can we detect the most important breakage with the fewest tests?
- Which API endpoint, service boundary, report builder, command, or workflow observes several important behaviors at once?
- Can we write a characterization test around that point before changing internals?
- Can we then refactor safely behind that test?
This is much more realistic than trying to reach perfect unit test coverage immediately.
It also changes how developers think about pytest. Pytest is not just a tool for isolated unit tests. It can support a broader testing strategy: characterization tests, integration tests, API tests, coverage gates, regression tests, and later smaller unit tests as the design improves.
The Main Learning
The biggest lesson from this whole process was simple:
Testing strategy should be part of system design and implementation design.
It should not be bolted on at the end.
When designing a feature, a team should ask:
- How will we know this works?
- Which behavior should be protected by unit tests?
- Which behavior needs integration tests?
- What should run in CI on every pull request?
- Which dependencies should be real, mocked, or replaced with test doubles?
- What design would make this feature easier to test without over-engineering it?
This is where TDD and BDD are useful, even if you do not follow them literally all the time.
I do not think strict TDD is always the right answer for every team and every task. Used mechanically, it can feel like overkill, especially in messy legacy systems or exploratory product work.
But knowing TDD and BDD changes how you design software.
TDD teaches you to think about feedback loops, behavior, and small increments. BDD teaches you to describe expected behavior in a way that connects technical implementation with user-facing outcomes. Pytest gives Python developers a practical way to turn those ideas into tests that run locally and in CI.
The long-term speed comes from combining these ideas pragmatically:
- know TDD and BDD well enough to use them when they help
- understand pytest features deeply enough to choose the right tool
- avoid huge refactoring when a small improvement will do
- leave the code better than you found it
- make tests part of implementation design
- run the right tests automatically in CI/CD
That is how teams go faster in the long term.
Not by writing tests for the sake of coverage. Not by chasing 100% test coverage as a vanity metric. Not by turning every refactor into a rewrite.
They go faster because the system becomes safer to change.
Why I Turned This Into a Course
After that first team experience, I later joined another Berlin AI startup and saw the same pattern again: good Python developers, useful product work, but uneven testing habits and not enough confidence around basic software testing, test automation, and automation testing workflows.
That repetition convinced me the problem was not unique to one company.
Many Python developers need a learning path that connects:
- pytest for beginners
- python unit testing
- fixtures and parametrization
- mocking and patching
- integration testing
- Python API testing
- FastAPI testing
- code coverage
- testing levels and the testing pyramid
- TDD and BDD
- GitHub Actions Python testing
- python CI/CD testing
- practical work with legacy code
That is why I created complete 11.5-hours course Pytest Course: Python Test Automation & GitHub Actions CI/CD. It has 6 parts, covering pytest fundamentals, fixtures, parametrization, advanced mocking, coverage, testing levels, and CI/CD automation with GitHub Actions.
The course is intentionally practical. It is not only a list of pytest features. It is a python testing course for developers who want to understand how testing fits into real implementation work.
That is why the examples include realistic scenarios such as:
- testing a scikit-style feature
- testing a document editor
- testing FastAPI endpoints
- working with temporary files and logs
- using
monkeypatch,pytest-mock, and patching targets - practicing with finance tracker challenges
- applying coverage and coverage gates
- running pytest in GitHub Actions
- using matrix testing and parallel testing
Those topics came from the same core belief:
Developers do not only need to learn pytest syntax. They need to learn how to use testing to design, refactor, debug, and ship Python code with more confidence.
How I Would Onboard a Team Today
If I had to onboard a Python team to testing again, I would use a similar approach, but even more intentionally.
First, I would start with a short pytest tutorial for Python basics: test discovery, assertions, fixtures, parametrization, and running tests locally.
Second, I would immediately connect the learning to the team’s own codebase. Toy examples are fine for the first hour. After that, real adoption requires real code.
Third, I would create weekly shared learning sessions with paired practice. The practice part is not optional. It is the part where culture changes.
Fourth, I would identify pinch points in legacy areas instead of trying to test everything from the bottom up.
Fifth, I would add CI early. Even a simple GitHub Actions Python testing workflow changes behavior because tests become part of the pull request conversation.
Sixth, I would make testing part of implementation design. Every non-trivial feature should include a conversation about test level, test boundaries, dependencies, and feedback speed.
Finally, I would keep the tone pragmatic. No guilt. No purity contest. No giant rewrite required.
Just this:
Write the next useful test. Improve the next small piece of design. Leave the code better than you found it.
Final Thought
The story behind my pytest course is really the story of learning that testing is not just a technical skill.
It is a team habit.
It is a design habit.
It is a way of making software easier to change without pretending legacy code does not exist.
That is why I wanted the course to go beyond a normal pytest framework tutorial. I wanted to share the practical lessons that helped real Python teams move from “we should write tests someday” to “testing is part of how we build.”
If your team is trying to introduce python test automation, start smaller than you think. Start with shared practice. Start with real code. Start with one useful test near an important behavior.
Then keep going.