How we pretest at AppFollow
Table of Content:
Introducing a different sort of article from what you’re used to! Insights from our R&D team that you can snag for yourself immediately. Enjoy!
This article has been prepared by Vladimir Ivanov, AI R&D Engineer at AppFollow.
I don't like writing tests. I like to release stuff to production instead!
We're a small team, divided by domain. Lots of features, lots of services, lots of new requirements, not many people, hands, or hours. We cover our code with tests, but tests are also code that needs to be maintained, extended, and sometimes tested. We want new features. The business wants new features. But we also want the old ones not to break. So here's what we figured out.
This is part one of two:
- The basics of how we test (this one)
- How we do integration tests that don't break every time something changes, using ioc + factoryboy
A bit about us
Twenty people, divided by domain, Python + FastAPI, about 50 microservices, and a backlog that covers the next couple of years.
The load across domains is uneven, so people end up jumping into code they didn't write. To stop that from going south, we keep the test coverage up, but without being obsessive about it. Nobody here likes writing mocks, and nobody likes seeing 170 tests fail because of one change somewhere.
The dream
We open the codex, hold our breath, and type:
“Check the code of our entire service. I want everything to work and remain bug-free.”
I wish I could stop there, but not yet. Not now. AGI isn't ready for that one, if it’ll ever be.
How we approach testing
- We don't write unit tests (mostly)
- We write integration tests that cover as many layers as possible, without mocks
- We test features, not code
- CI/CD has to pass before any MR goes in
- We test real user scenarios
- When a bug appears, we cover it with a test first, then fix it
- We aim for 80-90% coverage, 100% for anything critical
- TDD is not for us
What we get out of this
Changes in one place don't blow up the whole pipeline and eat into development time.
The code is covered end-to-end, from HTTP calls down to database writes. We write real scenarios instead of arguing about how to mock an async mock. Tests run close to production conditions, and regressions mostly go away. The tricky parts get covered thoroughly.
What it looks like
Here's a real integration test from our codebase. Real database, multiple layers of abstraction, FastAPI:

No mocks, no wrangling with service layers or dependency injection in the test itself. The test says exactly what it's testing.
And if we rip out the whole architecture underneath, change how everything is structured, split things into new services, the test doesn't care. As long as the API contract holds, it keeps passing.
So when do we write unit tests?
We used to try to cover everything. Units and integration tests for every service, tracked as a separate task, pushed to hit coverage numbers.
- First, it was: let's lower coverage temporarily, we have a bug to ship.
- Then: we'll get back to those tests, too much going on.
- Then: tests are failing and CI is slow, let's disable it for now.
- Some services ended up with CI/CD just off.
We went from "let's write a lot of good tests" to no tests at all, and the issue wasn't the tests. We just wrote more than we needed.
Now we write unit tests when something meets roughly these conditions:
- It's an isolated function
- It has a lot of edge cases
- It's critical enough to stand alone
A good unit test usually looks like this: one or two lines of test code, and a few dozen lines of parameters:

Back to integration tests
The stack we use: FastAPI, httpx, dependency-injector, factoryboy, pytest, Docker, PostgreSQL.
In this example, we'll keep it simple. Just FastAPI's Depends, no factoryboy, no Docker or SQLite.
Say we wrote this in main.py:

Database connection, repository pattern, service layer, HTTP, model validation.
The classic temptation here is to mock everything separately.
- Mock the session to test the repo.
- Mock the repo to test the service.
- Mock the service to test the endpoint.
- Mock get_service to check it returns the right thing.
- Then rewrite all of that after QA's first pass.
There's a better way. Test the whole thing as one service. To do this, we will use `ASGITransport` from `httpx`.
Create `conftest.py`, and import everything we need right away:

Initialize our database for testing:

We override the real database with our test database:

And now, the most important thing. Let's run our entire application in the test environment:

The last fixture contains all the magic; it will allow us to communicate with the entire service via HTTP without any workarounds.
Next, we write a test that uses a real HTTP request that corresponds to the contract!
We simply make the api_client fixture a test parameter and use it as a regular `httpx.Client`

Done, we get a working test.

This test:
1. Knows nothing about the implementation behind the HTTP layer
2. Checks everything from data model validation to actual DB writes
3. Does not use any mocks
4. Tests the multi-layer architecture
5. Works in a prod-like environment
6. Doesn't break when changes are made!
The last point is the most important: we can rewrite our entire architecture, removing the service layer and repository layer, and we won't have to rewrite a single line of test code!

Still passes!
Thanks for reading. In part two, we'll do something more involved: a real database, a third-party service call, and a table with more than two fields. We'll also get into what doesn't work well with this approach.
And if you have an app, come check out AppFollow. If you want to work with us and you know Python or TypeScript, we're hiring! Send your CV over to hello@appfollow.io.
See you in the next one,
Vova
FAQ
What is the difference between unit tests and integration tests in Python?
Unit tests check a single isolated function, usually with mocks replacing any dependencies. Integration tests run multiple components together and check that they work as a system. In Python FastAPI projects, an integration test might send a real HTTP request and verify the response includes data that was written to the database, without mocking anything in between.
How do you test a FastAPI application without mocks?
You can use ASGITransport from httpx to run your FastAPI app in-process and send real HTTP requests against it. Combined with FastAPI's dependency_overrides to swap in a test database, this lets you test the full request lifecycle from HTTP routing through service and repository layers down to actual database reads and writes, without mocking any of those layers.
Why do integration tests break less often than unit tests when you refactor code?
Unit tests are usually written against specific implementation details like a function signature or a method call on a mock. When you refactor, those details change and the tests fail even if the behavior is identical. Integration tests written against an API contract only break if the contract changes, so you can restructure the code underneath without touching the tests.
What test coverage percentage should a Python microservice have?
There is no universal answer, but a reasonable target for most services is 80-90% coverage through integration tests that exercise real user scenarios. For critical services that handle payments, authentication, or data integrity, 100% coverage is worth the extra effort. Below 70% and bugs will regularly reach production.
How do you use pytest fixtures to set up a test database for FastAPI?
Create a pytest fixture that spins up an in-memory SQLite database using SQLAlchemy, runs create_all to build the schema, yields a session for the test to use, then tears everything down with drop_all afterward. In a second fixture, use FastAPI's dependency_overrides to replace the production get_db dependency with one that returns your test session. This gives every test a clean, isolated database without touching production data.