Test Driven Development

With Python and Django

Israel Saeta Pérez

¿Por qué hacer tests?

  • Dormir tranquilo
  • Rápida incorporación gente nueva
  • Desarrollo más rápido. En serio.

¿Qué es TDD?

  1. Escribir test que falla
  2. Escribir código mínimo para que pase
  3. Refactorizar código para dejarlo más bonito

Teeest, teeest first!

unittest

# test*.py
import unittest
from mymodule import Rounder

class TestRounder(unittest.TestCase):
    # called before each test method (setup)
    # can be used to create fixtures
    def setUp(self):
        self.rounder = Rounder()

    def tearDown(self):  # called after each test method (cleanup)
        self.rounder.close()

    def test_round_down(self):  # all tests method names start with 'test_'
        self.assertEqual(self.rounder.round(3.1), 3)


if __name__ == '__main__':  # or 'python -m unittest discover'
    unittest.main()

unittest

dukebody@pepino:~/develop/testing > python test.py
F
======================================================================
FAIL: test_round_down (__main__.TestRounder)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 14, in test_round_down
    self.assertEqual(self.rounder.round(3.1), 3)
AssertionError: None != 3

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

unittest

dukebody@pepino:~/develop/testing > python test.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

Django

# rollback transaction on teardown
# isolated test db
from django.test import TestCase  # transaction for each test method
from myapp.models import Animal

class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')
django-admin test [module.path]

Testing views

from django.test import TestCase

# use built-in test client
class MyTestCase(TestCase):
    def test_create_book(self):
        """
        Posting creates a new book.
        """
        data = {'title': 'The catcher in the rye'}
        response = self.client.post('/books/', data)  # urlresolver

        books = Book.objects.all()
        self.assertEquals(len(books), 1)
        self.assertEqual(books[0].title, data['title']

Testing settings

from django.test import TestCase, override_settings

class LoginTestCase(TestCase):

    @override_settings(LOGIN_URL='/other/login/')
    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')

Functional tests

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import unittest

class NewVisitorTest(unittest.TestCase):

    def setUp(self):
        self.browser = webdriver.Firefox()
        self.browser.implicitly_wait(3)

    def tearDown(self):
        self.browser.quit()

    def test_can_start_a_list_and_retrieve_it_later(self):
        # Edith has heard about a cool new online to-do app. She goes
        # to check out its homepage
        self.browser.get('http://localhost:8000')

        # She notices the page title and header mention to-do lists
        self.assertIn('To-Do', self.browser.title)
        header_text = self.browser.find_element_by_tag_name('h1').text
        self.assertIn('To-Do', header_text)

Tips, tricks and goodies

For everything else, use Google

Identical DB types for testing and prod

From working code to working code

 

Obey the Testing Goat, not the Refactoring Cat!

factory_boy

# tests.py
class FooTests(unittest.TestCase):

    def test_with_factory_boy(self):
        # We need a 200€, paid order,
        # shipping to australia, for a VIP customer
        order = OrderFactory(
            amount=200,
            status='PAID',
            customer__is_vip=True,
            address__country='AU',
        )
        # Run the tests here


# factories.py
import factory
from . import models

class UserFactory(factory.Factory):
    class Meta:
        model = models.User

    first_name = 'John'
    last_name = 'Doe'
    admin = False

nose and django-nose

  • DB reuse - go faster
  • pdb on failure
  • plugin extensions

Mock

  • Mock calls to external services (ex. APIs)
  • Make unit tests really unit
  • Assert that a method was called
from freezegun import freeze_time

@freeze_time("2012-01-14")
def test():
    assert datetime.datetime.now() == datetime.datetime(2012, 01, 14)

If your code calls datetime.now

Tox

dukebody@pepino:~/develop/sklearn-pandas > cat tox.ini 
[tox]
envlist = py27, py34

[testenv]
deps =
     pip==7.0.1
     pytest==2.7.1
     setuptools==16.0
     wheel==0.24.0
     flake8==2.4.1
     py27: mock==1.3.0

commands =
         flake8 tests
         py.test

Best reference

Test Driven Development

By Israel Saeta Pérez

Test Driven Development

Intro to Test Driven Development with Python and Django. Some cool tricks are mentioned towards the end.

  • 2,906