Python: Automated testing
Theory: Data preparation
Most tests that test a given functionality are very similar, especially in initial data preparation. In the last lesson, each test started with the line: stack = []. It isn't duplication yet, but it's a step in a dangerous direction.
This lesson will learn how to prevent duplication when preparing identical data for different tests. Let's say we are developing the funcy library, which provides functions for working with collections in Python.
Among them, we can find such functions as:
compactselectflatten
How do we test them? For these functions to work, you need a prepared collection. Let's create the necessary collection, pass it to the function, and look at the result:
Then we'll do the same for all the other functions:
Next, we perform this operation for all other tested functions. The collection initialization fragment will start moving from place to place, generating more and more of the same code.
The easiest way to avoid this is to move the collection definition to the module level:
This simple solution eliminates unnecessary duplication. Keep in mind that this only works within a single module. The collection still needs to be defined in each test module. In our case, this is more of a plus than a minus. However, it's not always possible to transfer data to the module level.
First of all, what about dynamic data? What happens if at least one of the test functions changes this collection?
We use one collection for all tests. If it changes, it means that the tests are dependent on the order of execution. Subsequent tests will receive the modified collection. These situations are unacceptable in testing because they lead to fragile and hard-to-debug tests.
Test frameworks provide hooks — special functions that run before or after tests to solve this problem. In Pytest, we refer to them as fixtures.
Let us look at an example that creates a collection before each test:
The @pytest.fixture decorator adds an arbitrary function to the test execution process. The fixture runs before the tests requested by function parameters. The argument name must be the same as the fixture name.
Now for another example. Consider code that works with the current time:
The output will show two identical dates. The catch is that the code module is loaded into memory exactly once. It means any code defined at the module level will run once. In the example, we define the now variable before the tests, and only then does Pytest start executing them.
With each subsequent test, the discrepancy between the now variable and the actual value of now becomes larger:
- Pytest loads test modules into memory
- The
nowvariable is filled with the date when that piece of code runs - Pytest starts executing tests using the date from the previous step
Why might this be a problem? Code that works with the concept of now can expect now to be almost a snapshot of a particular moment. But in the example above, the new variable is starting to lag behind the real one.
The more tests and the more difficult they are, the greater the lag:
Fixtures are ideal for extracting common data needed in different tests. However, using fixtures can lead to more complex code than without them if the data slightly differs.