Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>I really like the suggestions from @Hassek and want to stress out what an excellent point he makes about the obvious lack of standard practices, which holds true for many of Django's aspects, not just testing, since all of us approach the framework with different concerns in mind, also adding to that the great degree of flexibility we have with designing our applications, we often end up with drastically different solutions that are applicable to the same problem.</p> <p>Having said that, though, most of us still strive for many of the same goals when testing our applications, mainly:</p> <ul> <li>Keeping our test modules neatly organized</li> <li>Creating reusable assertion and helper methods, helper functions that reduce the LOC for test methods, to make them more compact and readable</li> <li>Showing that there is an obvious, systematic approach to how the application components are tested</li> </ul> <p>Like @Hassek, these are my preferences that may directly conflict with the practices that you may be applying, but I feel it's nice to share the things we've proven that work, if only in our case.</p> <h2>No test case fixtures</h2> <p>Application fixtures work great, in cases you have certain constant model data you'd like to guarantee to be present in the database, say a collection of towns with their names and post office numbers.</p> <p>However, I see this as an inflexible solution for providing test case data. Test fixtures are very verbose, model mutations force you to either go through a lengthy process of reproducing the fixture data or to perform tedious manual changes and maintaining referential integrity is difficult to manually perform. </p> <p>Additionally, you'll most likely use many kinds of fixtures in your tests, not just for models: you'd like to store the response body from API requests, to create fixtures that target NoSQL database backends, to write have fixtures that are used to populate form data, etc.</p> <p>In the end, utilizing APIs to create data is concise, readable and it makes it much easier to spot relations, so most of us resort to using factories for dynamically creating fixtures.</p> <h2>Make extensive use of factories</h2> <p>Factory functions and methods are preferable to stomping out your test data. You can create helper factory module-level functions or test case methods that you may want to either reuse across application tests or throughout the whole project. Particularly, <a href="https://github.com/dnerdy/factory_boy"><code>factory_boy</code></a>, that @Hassek mentions, provides you with the ability to inherit/extend fixture data and do automatic sequencing, which might look a bit clumsy if you'd do it by hand otherwise.</p> <p>The <strong>ultimate goal</strong> of utilizing factories is to cut down on code-duplication and streamline how you create test data. I cannot give you exact metrics, but I'm sure if you go through your test methods with a discerning eye you will notice that a large portion of your test code is mainly preparing the data that you'll need to drive your tests.</p> <p>When this is done incorrectly, reading and maintaining tests becomes an exhausting activity. This tends to escalate when data mutations lead to not-so-obvious test failures across the board, at which point you'll not be able to apply systematic refactoring efforts. </p> <p>My personal approach to this problem is to start with a <code>myproject.factory</code> module that creates easy-to-access references to <code>QuerySet.create</code> methods for my models and also for any objects I might regularly use in most of my application tests:</p> <pre><code>from django.contrib.auth.models import User, AnonymousUser from django.test import RequestFactory from myproject.cars.models import Manufacturer, Car from myproject.stores.models import Store create_user = User.objects.create_user create_manufacturer = Manufacturer.objects.create create_car = Car.objects.create create_store = Store.objects.create _factory = RequestFactory() def get(path='/', data={}, user=AnonymousUser(), **extra): request = _factory.get(path, data, **extra) request.user = user return request def post(path='/', data={}, user=AnonymousUser(), **extra): request = _factory.post(path, data, **extra) request.user = user return request </code></pre> <p>This in turn allows me to do something like this:</p> <pre><code>from myproject import factory as f # Terse alias # A verbose, albeit readable approach to creating instances manufacturer = f.create_manufacturer(name='Foomobiles') car1 = f.create_car(manufacturer=manufacturer, name='Foo') car2 = f.create_car(manufacturer=manufacturer, name='Bar') # Reduce the crud for creating some common objects manufacturer = f.create_manufacturer(name='Foomobiles') data = {name: 'Foo', manufacturer: manufacturer.id) request = f.post(data=data) view = CarCreateView() response = view.post(request) </code></pre> <p>Most people are rigorous about reducing code duplication, but I actually intentionally introduce some whenever I feel it contributes to test comprehensiveness. Again, the goal with whichever approach you take to factories is to minimize the amount of brainfuck you introduce into the header of each test method.</p> <h2>Use mocks, but use them wisely</h2> <p>I'm a fan of <a href="http://www.voidspace.org.uk/python/mock/"><code>mock</code></a>, as I've developed an appreciation for the author's solution to what I believe was the problem he wanted to address. The tools provided by the package allow you to form test assertions by injecting expected outcomes.</p> <pre><code># Creating mocks to simplify tests factory = RequestFactory() request = factory.get() request.user = Mock(is_authenticated=lamda: True) # A mock of an authenticated user view = DispatchForAuthenticatedOnlyView().as_view() response = view(request) # Patching objects to return expected data @patch.object(CurrencyApi, 'get_currency_list', return_value="{'foo': 1.00, 'bar': 15.00}") def test_converts_between_two_currencies(self, currency_list_mock): converter = Converter() # Uses CurrencyApi under the hood result = converter.convert(from='bar', to='foo', ammount=45) self.assertEqual(4, result) </code></pre> <p>As you can see, mocks are really helpful, but they have a nasty side effect: your mocks clearly show your making assumptions on how it is that your application behaves, which introduces coupling. If <code>Converter</code> is refactored to use something other than the <code>CurrencyApi</code>, someone may not obviously understand why the test method is suddenly failing.</p> <p>So with great power comes great responsibility--if your going to be a smartass and use mocks to avoid deeply rooted test obstacles, you may completely obfuscate the true nature of your test failures.</p> <h2>Above all, be consistent. Very very consistent</h2> <p>This is the most important point to be made. Be consistent with absolutely everything:</p> <ul> <li>how you organize code in each of your test modules</li> <li>how you introduce test cases for your application components</li> <li>how you introduce test methods for asserting the behavior of those components</li> <li>how you structure test methods</li> <li>how you approach testing common components (class-based views, models, forms, etc.)</li> <li>how you apply reuse</li> </ul> <p>For most projects, the bit about how your collaboratively going to approach testing is often overlooked. While the application code itself looks perfect--adhering to style guides, use of Python idioms, reapplying Django's own approach to solving related problems, textbook use of framework components, etc.--no one really makes it an effort to figure out how to turn test code into a valid, useful communication tool and it's a shame if, perhaps, having clear guidelines for test code is all it takes.</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload