📜 ⬆️ ⬇️

Implement TDD with django and postgres

In the Island there are two main products: for users (ostrovok.ru) and “admin” for hotels (extranet) , where the hotels we have signed enter the data. These are separate products, with their teams and different attitudes towards test-driven development (TDD) . Same platform: django and postgres. The extranet uses TDD and they have a bunch of tests. Initially, the tests were in ostrovok.ru, but due to the departure of some of the adepts to the extranet and very intensive development, they were no longer supported. In general, I was faced with the task of introducing testing. The first steps have been taken and I want to share this experience and the solutions that have been applied.
We have a QA department and Selenium autotests, but this is separate.

With django and tests in general, things are pretty good, and of course it's better to cover everything from the very beginning with tests, increasing functionality and refactoring.

In our case, there was already a huge functionality and a lot of comprehensive dependencies and integration with external APIs. And you need this to work in a test environment. You can forget about fast SQLite in memory, there are references to postgres features in the project, and the identity of the test environment is still important, so the tests also work on postgres.


What tests do I like and why TDD


')
There are many types of testing that vary in different aspects.

On isolation, I like integration tests more, on the object under test - functional ones.

Such tests have a very large code coverage, this is both a plus and a minus at the same time.

Minus:

Pros:


We are developing the web and ideally I don’t want to open a browser for manual testing of my code. I would like to record in the test all actions in the browser and add a number of checks (sending a letter, the presence of a log or some object in the database). When I write the code, I need to carry out all these actions manually once exactly, but in most cases it will be several times. Record actions in the test and run ten times for a few seconds, it is much cooler than manually making ten checks. In the browser, in addition to the main markup, styles, images, javascript are also loaded, and all this usually falls on our local runserver, but it is not the brightest and often works in one thread, since I don’t want to tweak the uwsgi and nginx bundle for development ... Well, in addition, the benefit is that the written test that helped in the development remains and plays an important role in regression testing .

In addition to testing http requests, there are other tests, for example, testing django commands, everything is the same with them. Ordinary unit tests are also helpful. When you get used to start up and write tests, then the development style changes, the process will most likely be iterative: a simple test is the necessary code, we complicate the test - we add the code. For example: you can make a typo, quickly run a test and see a typo and that the test failed.

And of course there are places where manual testing is difficult or even almost impossible, in this case tests are a necessity. For example: checking the correct interception of exceptions and error handling, fine points of logic ...

Ideally, test first.

To paint all the benefits of developing through testing is not the purpose of this article, let's leave it to others, for example, Kentu Beck .

How to make the test run faster?


In TDD, a very important operation is the launch of tests. Usually it is not even all the tests, but some kind of a set: tests from a package, a separate module, or even a separate test. Therefore, the launch of individual test cases should be quick.

In django there is a problem with it , a base is created in it before each test run, and if the circuit is large, then it may take 30 seconds, and the execution of a specific test is less than a second. I do not want to wait until the database is created.

Solution: put the creation of the base in a separate step (use the base from previous runs).

As part of our conditions, in addition to the base scheme, we also needed initial data:


It seems that here is new:


Use existing code!



In the "no test" time I had to participate in mega refactoring, which was associated with the import of hotels. In the course of this task, we have tests that cover imports well. These tests lived their lives, we kept them up to date, so that they did not become a dead weight, like other existing tests, most of which were removed.

Once again, hotels are a difficult entity, and we didn’t want to create all connected objects, and then support all this economy. Moreover, there is a working, tested import code, the task of which is just to create hotels, and it was used. Less code - less error.

Tests we drive with nose , in general, it is a very good tool for running tests with plug-in support.

As a result, we got our own runner and a number of plug-ins that solve several problems:





There is a process of creating a database depending on the command line parameters:

... --with-reuse-db #   ,     --create-db #       ... 


In this approach, there is a minus: you need to remember that if the base scheme changes, then you need to recreate the base. This is not critical, quick start is more important.

The process of creating the initial base from us can already take up to a minute when importing GIS and hotels. And we keep two initial bases: with hotels and without, because when testing imports we do not need hotels. In concrete TestCase we set the base template we need.

In the standard django approach, the TransactionTestCase is flush (complete database cleaning), then the initial one is restored. This approach does not work, because we have a separate step to create a base, and do not need to clean it. With the autocommit option for postgres , the flush performed on every test and this is bad - it is long.

To speed up the tests (relatively flush ) we used a unique database that was created from a template, postgres is able to:

 src = self.db_conf['TEST_NAME'] new = '{0}_{1}'.format(src, uuid.uuid4().hex) psql( 'DROP DATABASE IF EXISTS "{0}";' 'CREATE DATABASE "{0}" WITH TEMPLATE "{1}";' .format(new, src) ) 

The gain was relatively flush several times and it seemed to be quite good. Plus, the unique base for the test is that the probability of some kind of collisions in the database is zero, and with transactions they are possible. In the end, they came to the option: the default is to work in a transaction, because it is faster, and if some tests have problems, then a unique base.
To speed up the test database, you can also put in postgresql.conf:
 fsync = off # turns forced synchronization on or off 

The increase is also felt. Well, SSD hard drives are also good :).


Such tests are easier to include in the build process, they pass fairly quickly (3-4 minutes ~ 250 tests) and do not delay the release, they are next to the code. You need to keep track of the test execution time and take measures to accelerate, because the number of tests will only grow, and therefore the time of their execution.

Further in terms of acceleration, you need to parallel the test run, nose even knows how , but you need to refine your code.

In addition to the quick launch of tests, you also need to write them effortlessly. When we have a bunch of comprehensive dependencies, the first tests, which repeat the basic actions of the user, are hard. Many places need to be locked, with many places to figure out. Therefore, time was allotted to make helpers, simplifying the writing of such tests, with a minimum of code.

What we have?


Due to the significant acceleration of the launch of tests, they are now involved in building the package: the release does not roll out if there are fallen tests. This is also a very important point, because there is an obvious connection: working tests - release, non-working tests - no release (we have frequent releases, happen several times a day). Selenium autotests are still living a separate life, but the team is working on their inclusion in the process of continuous integration .

Tests already help us:




In principle, a start has been made, decisions have been made, what will happen next - time will tell.

PS python and postgres great tools - use.

Posted by: naspeh

Source: https://habr.com/ru/post/146552/


All Articles