by Martin Angelov @ EuroPython 2018, Edinburgh
https://www.google.com/maps
https://www.facebook.com/BurgasCity/photos
Proper Django unit testing is strongly bounded to proper Django app structure
Let's take a look at the following example...
class InvoiceCreateView(View):
def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request,
self.template_name,
{'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
Invoice.objects.create(**form.cleaned_data)
return HttpResponseRedirect('/success/')
return render(request,
self.template_name,
{'form': form})
class InvoiceCreateViewTests(TestCase):
def setUp(self):
self.url = self.reverse('invoices:create')
self.data = {'amount': faker.pyint(),
'description': faker.text(),
'created_by': AdminFactory()}
def test_view_can_be_accessed(self):
self.get(self.url, follow=True)
self.response_200()
def test_view_creates_new_invoice_with_valid_data(self):
assertFalse(
Invoice.objects.filter(**self.data).exists()
)
self.post(self.url, data=self.data, follow=True)
self.response_302()
assertTrue(
Invoice.objects.filter(**self.data).exists()
)
class InvoiceCreateViewTests(TestCase):
def setUp(self):
self.url = self.reverse('invoices:create')
self.data = {'amount': faker.pyint(),
'description': faker.text(),
'created_by': AdminFactory()}
def test_view_can_be_accessed(self):
self.get(self.url, follow=True)
self.response_200()
def test_view_creates_new_invoice_with_valid_data(self):
assertFalse(
Invoice.objects.filter(**self.data).exists()
)
self.post(self.url, data=self.data, follow=True)
self.response_302()
assertTrue(
Invoice.objects.filter(**self.data).exists()
)
This view would also need to:
View
Service
HTTP
Django ORM
Tasks
Services
Validations
Business Logic
View unit
Service unit
def validate_amount(amount):
if amount > 0:
return
raise InvalidInvoiceAmount()
def create_invoice(*, created_by, description, amount):
validate_amount(amount)
invoice = Invoice.objects.create(
created_by=created_by,
description=description,
amount=amount
)
tasks.sync_invoice.s(invoice_id)
return invoice
class InvoiceCreateView(View):
def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request,
self.template_name,
{'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
create_invoice(**form.cleaned_data)
return HttpResponseRedirect('/success/')
return render(request,
self.template_name,
{'form': form})
class InvoiceCreateApiView(APIView):
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(
data=request.data
)
serializer.is_valid(raise_exception=True)
invoice = create_invoice(
**serializer.validated_data
)
return Response(
{'invoice_id': invoice.id},
status=status.HTTP_201_CREATED
)
class InvoiceCreateViewTests(TestCase):
def test_view_can_be_accessed(self):
self.get(self.url, follow=True)
self.response_200()
def test_view_redirects_after_successful_post(self):
self.post(self.url, data=self.data, follow=True)
self.response_302()
@mock.patch('app.views.invoices.create_invoice')
def test_view_calls_service(self, service_mock):
self.post(self.url, data=self.data, follow=True)
service_mock.assert_called_once_with(
**self.data
)
class InvoiceCreateApiViewTests(TestCase):
@mock.patch('app.views.invoices.create_booking')
def test_view_calls_create_booking_service(
self, service_mock
)
self.client.post(
self.url, data=self.data, follow=True
)
service_mock.assert_called_once_with(
amount=self.data['amount'],
description=self.data['description'],
created_by=self.admin
)
class CreateInvoiceTests(TestCase):
@mock.patch('app.invoices.services.validate_amount')
def test_service_creates_new_invoice(
self, validate_mock
):
assertFalse(
Invoice.objects.filter(**self.data).exists()
)
self.service(**self.kwargs)
assertTrue(
Invoice.objects.filter(**self.data).exists()
)
class CreateInvoiceTests(TestCase):
@mock.patch('app.invoices.services.validate_amount')
@mock.patch('app.invoices.services.tasks.sync_invoice.s')
def test_service_calls_sync_invoice_task(
self, task_mock, validate_mock
):
invoice = self.service(**self.data)
task_mock.assert_called_once_with(invoice.id)
class CreateInvoiceTests(TestCase):
@mock.patch('app.invoices.services.validate_amount')
@mock.patch('app.invoices.services.tasks.sync_invoice.s')
def test_invoice_is_not_created_if_amount_is_not_valid(
self, task_mock, validate_mock
):
validate_mock.side_effect = InvalideInvoiceAmout()
with self.assertRaises():
self.service(**self.data)
assertFalse(
Invoice.objects.filter(**self.data).exists()
)
task_mock.assert_not_called()
tooling
unittest
django.test
test_plus
class InvoiceCreateViewTests(TestCase):
def test_view_redirects_after_successful_post(self):
self.post(self.url, data=self.data, follow=True)
self.response_302()
@mock.patch('app.views.invoices.create_invoice')
def test_view_calls_service(self, service_mock):
self.post(self.url, data=self.data, follow=True)
service_mock.assert_called_once_with(**self.data)
class InvoiceCreateViewTests(TestCase):
@mock.patch('app.views.invoices.create_invoice')
def test_successful_post(self, service_mock):
self.post(self.url, data=self.data, follow=True)
with self.subTest('Assert view redirects'):
self.response_302()
with self.subTest('Assert service is called'):
service_mock.assert_called_once_with(
**self.data
)
class InvoiceFactory(factory.DjangoModelFactory):
class Meta:
model = Invoice
amount = factory.LazyAttribute(
lambda _: faker.pyint()
)
description = factory.LazyAttribute(
lambda _: faker.text()
)
created_by = factory.SubFactory(AdminFactory)
original_pyint_provider = faker.pyint
def pyint_provider(*args, **kwargs):
return abs(original_pyint_provider(*args, **kwargs))
faker.pyint = pyint_provider
py.test app/tests/views/invoices.py --create-db --reuse-db --lf
other testing methodologies
facebook.com/martin.angelov056
twitter.com/_martin056
github.com/martin056
www.hacksoft.io/blog/
https://docs.python.org/3/library/unittest.html
https://docs.python.org/3/library/unittest.mock.html
https://docs.djangoproject.com/en/2.0/topics/testing/
https://github.com/revsys/django-test-plus
http://factoryboy.readthedocs.io/en/latest/
http://factoryboy.readthedocs.io/en/latest/
https://docs.pytest.org/en/latest/
https://slides.com/hackbulgaria/django-structure-for-scale-and-longevity#/