Improve your tests — Part 1

Matvei Koniaev
4 min readAug 11, 2023

Everyone knows that tests are very important for an application, not only because they can show problems in the app but also because they can save money, health, and unsleeping nights in the future. But how can we write good tests?

From my perspective, good tests come from healthy architecture, understandable tools and patterns for testing. Let’s look into it!

Use the same structure as in the application

First things first, tests can’t live without a good environment. Tests are still code, and we have to care how we collect them, how we reflect project structure in tests, and how we organize them.

Just imagine if you have a lot of different modules, integrations, entities, APIs, and so on. Wouldn’t it be great to see the same structure as in your app?

This way simplifies searching for particular tests, clarifies the place for new tests, and makes your tests tidy.

└── project
....
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── fixtures
│ │ └── api.py
│ ├── helpers.py
│ └── web
│ ├── __init__.py
│ ├── http
│ │ ├── __init__.py
│ └── jsonrpc
│ ├── __init__.py
│ └── api_v1
│ ├── __init__.py
| ├── test_increase_my_salary.py
└── web
├── __init__.py
...

For example, if you have a new JsonRpc method, you already know where you have to place it. And in the same situation, if you want to find any tests, you can use your structure!

Likewise, using this approach to organize your tests, we will find a way to name your file with tests.

Name file with tests the same as API

In this approach, we don’t create a main file for tests, because, as your app grows, the file with tests becomes messy without any possibility of being maintained or understood.

Instead of it, we create a file with a specific name. For instance, if you have the JsonRpc method “increase_my_salary”, we want to make a file with name “test_increase_my_salary” in the “jsonrpc” directory. And at the same time, our structure will explain itself, and we don’t need to even open a file to understand which tests are placed inside.

And now is a moment to go deep and break a particular test down.

One corner case is one test

From this hint, I want to extract one essential rule: a test should show that one particular case doesn’t work.

A bad example is when we call our own API to create an object, which we can use after. It’s completely incorrect because our object can be created with errors; therefore, business logic in the next calls can work with invalid data. When we find out about dropped tests, it will be really difficult to understand what happened: our target call is incorrect or our function for “preparation” went down. At the end, we can’t trust this test at all!

The gist is making a test that make only one target call. It will be one case in one particular test, which is easy to track, understand, and solve.

According to this hint, we have one little problem: we need to prepare data for tests without using APIs. In this case, we can use fixtures to create objects.

Use fixtures

Fixtures are an incredible and irreplaceable feature when we want to improve tests. It helps us reuse any part of code in tests without importing it. Let’s look at the example below.

In this case, we created a fixture “employee” and encapsulated dependency on another object, an account, which was also placed as a fixture. Pytest makes a graph with all dependencies and automatically creates our objects when we start the test.

Consequently, we have encapsulated the logic of building objects and their relations, which can provide us with tidy, maintainable, and readable tests!

Cook data correctly

Now we already know about fixtures, but what if we want to use different objects with different properties in one test? The naive answer is making new fixtures like “employee_1” or “employee_2”. Actually, it can work if we don’t need more instances.

As a solution, we can use factories. The easiest way is to just function inside fixtures, which can create objects and relations.

When we use factories, we can manage our logic in one place, and we don’t need to create unclear names for fixtures such as “employee_123”. Also, fixtures are easy to read, maintain, and understand.

In this part of a series of articles about improving tests, I shared my perspective about a friendly environment for testing.

In the next parts, we will go deep and talk about working with external systems, reconstruction problems from different environments, such as pre-production and production, and much more!

Please ask me any questions if you have any; I’m glad to answer them! And thank you for reading my article :)

--

--