Przemek Lewandowski

@haxoza

The Ultimate

(Django)

Testing

About me

Overview

  • Unit Testing
  • Good practices
  • Mocking
  • Django Test Cases
  • Data Factories
  • Django Views Unit Testing
  • Integration Testing

Why Unit Testing?

  • Finds issues early
  • Isolation
  • Fast in execution
  • Red / Green Refactor
  • Architecture
  • Live documentation

Why Unit Testing?

Python is dynamically typed language

so

let's execute the code before running on production

but

tests don't replace static typing

<< Missing Gary Bernhardt PyCon US 2015 keynote >>

Why Unit Testing?

We are programmers

so

let's automate

Common structures

  • Tests
  • Test Cases
  • Suites
  • xUnit family

Is your unit testing real unit testing?

Read more here:

http://martinfowler.com/bliki/UnitTest.html

Good practices

Technical perfection

Meaningful names

Read more here:

http://stackoverflow.com/questions/96297/what-are-some-popular-naming-conventions-for-unit-tests

Test Cases names

[name of unit being tested]Tests


    class LoginViewTests(unittest.TestCase):
        pass

Test names

test_[feature being tested]_[expected behaviour]


    def test_post_should_login_user(self):
        pass

Test body structure

Given When Then

  • Given: state of the world before the test
  • When: behaviour that you're specifying
  • Then: changes expected due to the specified behaviour

Read more here:

http://martinfowler.com/bliki/GivenWhenThen.html

Given When Then

    def test_post_should_set_new_username(self):
        # Given

        # When

        # Then

Given When Then

    def test_post_should_set_new_username(self):
        user = create_user()
        data = {
            'new_username': 'ringo',
            'current_password': 'secret',
        }
        request = self.factory.post(user=user, data=data)

        response = self.view(request)

        self.assert_status_equal(response, status.HTTP_200_OK)
        user = utils.refresh(user)
        self.assertEqual(data['new_username'], user.username)

Given

When

Then

Empty lines between sections

Other good practices

  • Short tests (LOC)
  • Clean code obliges
  • Don't overuse setup method
  • Don't hit external services

Dependency injection

In Python?

Mocking to the rescue


    @mock.patch('stripe.Charge.create')
    def test_post_should_return_error(self, stripe_create_mock):
        stripe_create_mock.side_effect = stripe.StripeError()
        lead_offer = factories.LeadOfferFactory()
        request = self.factory.post(data={'token': 'wrong-token'})

        response = self.view(request, slug=lead_offer.slug)

        self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
        self.assert_emails_in_mailbox(0)

unittest.mock in Python 3

Testing in Django

Testing in Django

  • Models
  • Forms
  • Views
  • ...

Models testing

  • Isolation from views
  • State should represent business logic
  • Sometimes don't need to hit database

Models instantiation

Django fixtures - don't do it

Models instantiation

  • Model Managers
  • Factories (Factory Boy)
  • Fake data (Faker)

Factory Boy


class UserFactory(factory.DjangoModelFactory):
    FACTORY_FOR = auth.get_user_model()

    email = factory.Sequence(lambda n: 'user-{0}@example.com'.format(n))
    password = 'pass'

    @classmethod
    def _prepare(cls, create, **kwargs):
        password = kwargs.pop('password', None)
        user = super(UserFactory, cls)._prepare(create, **kwargs)
        if password:
            user.raw_password = password
            user.set_password(password)
            if create:
                user.save()
        return user

Factory Boy


    # Don't hit database
    user = UserFactory.build()

    # Hit database
    user = UserFactory.create()

Django specific
Test Cases

django.tests.TestCase

  • Rolls back the transaction at the end of the test
  • Additional assertions
  • Creates instance of django.tests.Client
  • The ability to run tests with modified settings
  • setUpTestData since Django 1.8

django.tests.testcases

Views testing

Views testing

  • django.tests.TestCase
  • django.tests.Client
  • django.tests.RequestFactory

django.tests.Client

  • Runs as fake browser
  • Integrated with django.contrib.auth
  • Invokes all Django layers
  • Mainly for integration testing

Django Middlewares

django.tests.RequestFactory

  • Calling the view manually
  • Middlewares are skipped
  • Hope for real view unit tests 
  • Not so easy in use

Common issues 

  • Integration vs. unit tests
  • All application layers
  • Strongly coupled components
  • Execution time of middlewares
  • Mocking Django backends

Djet

to the rescue

Django Extended Tests

https://github.com/sunscrapers/djet

Star please!

Djet Features

  • Easy views unit testing
  • Supports function- and class-based views
  • Runs only explicite passed middlewares

Djet in action

class YourViewTest(assertions.StatusCodeAssertionsMixin,
                   assertions.MessagesAssertionsMixin,
                   testcases.ViewTestCase):
    view_class = YourView
    view_kwargs = {'some_kwarg': 'value'}
    middleware_classes = [
        SessionMiddleware,
        (MessageMiddleware, testcases.MiddlewareType.PROCESS_REQUEST),
    ]

    def test_post_should_redirect_and_add_message_when_next_parameter(self):
        user = UserFactory()
        request = self.factory.post(data={'next': '/'}, user=user)

        response = self.view(request)

        self.assert_redirect(response, '/')
        self.assert_message_exists(request, messages.SUCCESS, 'Success!')

Other features

  • Easier operations on files (in memory)
  • Adds extra assertions
  • Supports django.contrib.auth
  • Supports Django Rest Framework

Djet combo

class TestModel(models.Model):
    field = models.CharField(max_length=100)
    file = models.FileField(upload_to='files')

class CreateAPIView(generics.CreateAPIView):
    model = models.TestModel

class CreateAPIViewTest(assertions.StatusCodeAssertionsMixin,
                        files.InMemoryStorageMixin,
                        restframework.APIViewTestCase):
    view_class = CreateAPIView

    def test_post_should_create_model(self):
        data = {
            'field': 'test value',
            'file': files.create_inmemory_file('test.txt', content=b'Hello multipart!'),
        }
        request = self.factory.post(data=data, format='multipart')

        response = self.view(request)

        self.assert_status_equal(response, status.HTTP_201_CREATED)

Why to use Djet?

  • Compact view tests
  • Faster execution
  • Fixed default FileStorage

Faster execution

Integration testing

Mocking everything out?


    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
        }
    }

    CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
        }
    }

Use real dependencies

Continuous Integration

with real database

Thanks!

Q & A

Follow me on @haxoza

The Ultimate (Django) Testing

By Przemek Lewandowski

The Ultimate (Django) Testing

  • 2,110