Testing and Designing Django Applications

Lucas Lavandeira
devartis
Published in
6 min readDec 20, 2019

--

Photo by Danganhfoto

In this post we’re going to walk through on how to design Django applications with testing in mind. These concepts are not new by any means, but they’ll probably come in handy for anyone going through an architecture design process using this framework as the system’s core.

First, let’s discuss why writing automated tests is important. The importance of automated tests comes from the fact they replace manual testing. Every time you write a feature you end up running your app manually to see if it works, right? You run through the new code changes, but also it’s a good idea to test previous workflows, to check that everything still works correctly, even after your change. Well written automated tests are meant to do exactly that, but at the click of a button, several times faster than any person running through the code manually.

Moreover, through Continuous Integration services and tools, we can run our project’s tests on every code change made to a repository. If any change makes the tests fail, we’ll know instantly that the code has a problem and should not reach any production environment. If a team wants to truly be Agile, and have its changes reach the end users as fast as possible, they need to have an extensive testing suite to ensure they’re not introducing bugs whenever they update their system.

The testing pyramid

According to a blog post on martinfowler.com, tests can be organized into a testing pyramid, with several unit tests serving as a base for the application logic at the bottom, and the end-to-end tests at the top ensuring the system comes together as expected. Starting at the bottom, tests usually cover a single component of our application, and are much faster than the ones at the top, where every system and interaction is covered. Just like the pyramid shape implies, it makes sense to have a bigger amount of simpler, faster unit tests, and less of the resource heavy end-to-end or UI tests.

The Testing Pyramid

Taking into account the concept of Unit Tests being isolated pieces of code being tested by themselves, with any existing dependencies being mocked, I want to emphasize the fact that most of our application’s logic is already implemented (and tested) by Django. The use of the database is completely left to the framework, and so is most of the HTTP Request/Response configuration, we just usually direct Django to use a specific HTML template or to serialize a python dict into a JSON as a response. Most of our daily work as web developers involves using these resources, leaving little to really unit test. The only relevant concepts that can be truly called Unit Testable by us, is Model logic that does not involve CRUD operations.

In a project where most of our job is plugging different components together, we should focus on testing these connections.

Django Model Integrity

So, we’d like to, at the very least, test our models in unit tests. Except, most of Django Model’s features are tightly coupled with Forms. As an example, field attributes such as choices, unique_for_date, among others, are only validated through forms, if you create a model through code, it will not validate anything.

Consider this Model, taken from Django’s docs on validators:

Running MyModel.objects.create(even_field=1) will not fail! Django only enforces this validation (and every other validation ran through Validators) only when the model’s full_clean() method is executed, and that only happens automatically when the model is saved through a (model) form.

[…] the core logic of your application [should] be code you can control, as decoupled from Django as possible.

So where does that leave us? How do we test our model’s integrity? The point I’d like to make is to skip out on these Django features. They require going through forms. This makes sense in several contexts, such as when you’re developing a RESTful API using REST Framework, your app just doesn’t use forms, and so you’re left to write your own validations anyway. And we’d like for our model logic and validations to apply in every single context they’re used (whether the model is constructed from a CRUD operation on the django-admin, from an API call, from a management command, …), and the in-framework validations do not provide this: they force us to use forms.

While it has the disadvantage of “reinventing the wheel”, usually these features are not so complex that it becomes hard to reimplement them yourself. Simple field validations are not hard to write, and in the long run, it’s beneficial for the core logic of your application to be code you can control, as decoupled from Django as possible.

Designing a Test Suite

An approach that has proven to be effective is to try to have a test environment that’s as close to the real production environment. That means:

  • Same Database (using a real PostgreSQL instance instead of an in-memory SQLite DB)
  • Maintaining test versions of other services used (Redis, Elasticsearch, …), with real test data
  • Avoid using mocks where possible, only mocking if it’s really an external service we can not control.

These last points, and the fact that unit testing has been losing importance over time (DHH’s “TDD is Dead” and the recommended link at the end of his post, “Why Most Unit Testing is Waste”), leave us with an approach to start testing our system as a whole: focusing on integration and endpoint testing.

Our proposal is to test our application as a black box as much as possible: for a given set of inputs, do an HTTP call that hits one of your Views, and parse the output to check if it works as expected. This is the only real way to test your system works as expected, where nothing is mocked, and it’s tested as a whole. In a project where most of our job is plugging different components together, we should focus on testing these connections. Of course we’re also, in a way, testing the framework, since most of the program’s results will be based on whatever the dependencies do, but the crucial point is we’ve also ensured everything is glued together nicely.

That is not to say we should neglect testing individual components as units! When our application grows bigger and our logic becomes complex enough, then it can be worthwhile to test a component in an isolated way. This component has to be completely decoupled from other parts, minimizing the database access, or directly reading Django settings. Focus on Dependency Injection, to make the testing of the component independent of the rest of the system, including Django itself.

To sum up, when testing our application we want, in order of importance:

  • To make sure our features work: write integration, or end-to-end tests, using real (not mocked) dependencies
  • To make sure our business logic is correct: write unit tests, decoupling meaningfrom external components using Dependency Injection

As a last question, what about Test Driven Development? Personally I find it useful to write the end-to-end test of the system before doing anything else, especially when working with REST APIs, as it is easy to incrementally define an interface, adding test cases for each field, one at a time. It helps me as the design tool the advocates usually claim it to be. Then, for writing the actual code, we can go down and start writing unit tests for a new or existing logic component. It’s not entirely unlike the BDD cycle, except we talk about concrete end-to-end tests instead of the abstract concept of features or acceptance tests.

Is DHH “wrong”? Integration tests being the most important is the key point I take from his infamous post, but testing first forces you to think about dependencies and code design, which if it’s not done early on, often it will never be. The biggest dependency on our project is Django itself, and it’s too easy to write code that relies too much on some of its features, and later on becomes hard to extend and maintain.

Recommended (further) reading:

Visit us!

--

--