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,201