https://slides.com/ctrlweb/django-3/live
CEO at ctrlweb
@danidou
https://ca.linkedin.com/in/dleblanc78
YulDev Organizer
Montréal-Django Organizer
daniel@ctrlweb.ca
“ The intelligent have plans; the wise have principles. ”
- Raheel Farooq
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
"Most systems work best if they are kept simple rather than made complicated; therefore simplicity should be a key goal in design and unnecessary complexity should be avoided."
"Always implement things when you actually need them, never when you just foresee that you need them."
A class should have only a single responsibility.
class BlogPost(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
slug = models.SlugField()
author = models.ForeignKey(User, related_name='posts')
create_date = models.DateTimeField(auto_now_add=True)
modified_date = models.DateTimeField(auto_now=True)
publish_date = models.DateTimeField(null=True)
from .behaviors import Authorable, Permalinkable, Timestampable, Publishable
class BlogPost(Authorable, Permalinkable, Timestampable, Publishable, models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
class Authorable(models.Model):
author = models.ForeignKey(User)
class Meta:
abstract = True
class Permalinkable(models.Model):
slug = models.SlugField()
class Meta:
abstract = True
class Publishable(models.Model):
publish_date = models.DateTimeField(null=True)
class Meta:
abstract = True
[...]
Software entities should be opened to extension, but closed to modification.
In dynamic languages (such as Python), this is done by leveraging duck typing
for x in range(1, 46):
if x % 3 == 0 and x % 5 == 0:
print("Quack! *Flap*")
elif x % 5 == 0:
print("Quack!")
elif x % 3 == 0:
print("*Flap*")
else:
print(x)
class QuackFlapFactory:
def create():
rules = [QuackFlapRule(), FlapRule(), QuackRule(), NormalRule()]
return QuackFlap(rules)
create = staticmethod(create)
class QuackFlap:
def __init__(self, rules):
self.rules = rules
def say(self, x):
for rule in self.rules:
if rule.is_handled(x):
return rule.say(x)
class Rule:
def is_handled(self, x):
pass
def say(self, x):
pass
class NormalRule(Rule):
def is_handled(self, x):
return True
def say(self, x):
return x
class QuackRule(Rule):
def is_handled(self, x):
return x % 3 == 0
def say(self, x):
return "Quack!"
class FlapRule(Rule):
def is_handled(self, x):
return x % 5 == 0
def say(self, x):
return "*Flap*"
class QuackFlapRule(Rule):
def is_handled(self, x):
return x % 5 == 0 and x % 3 == 0
def say(self, x):
return "Quack! *Flap*"
if __name__ == '__main__':
quack_flap = QuackFlapFactory.create()
for i in range(1, 46):
print(quack_flap.say(i))
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
def can_quack(animal):
if isinstance(animal, Duck):
return True
elif isinstance(animal, Snake):
return False
else:
raise RuntimeError('Unknown animal!')
if __name__ == '__main__':
print(can_quack(Duck()))
print(can_quack(Snake()))
print(can_quack(Elephant()))
def can_quack(animal):
return animal.can_quack()
class Animal(object):
def can_quack(self):
pass
class Duck(Animal):
def can_quack(self):
return True
class Snake(Animal):
def can_quack(self):
return False
if __name__ == '__main__':
print(can_quack(Duck()))
print(snake.can_quack(Snake()))
Many client-specific interfaces are better than one general-purpose interface
The good news is: with duck typing, this is implicit.
One should depend upon Abstractions, and not upon concretions.
class Airline:
def book(self, origin, destination, date):
pass
class FlightBooker:
def __init__(self, starting_point, destination):
self.airline = Airline()
self.origin = starting_point
self.destination = destination
def book_two_way(self, start_date, return_date):
# Note: the definition for Airline.book is Airline.book(from, to, date)
self.airline.book(self.origin, self.destination, start_date)
self.airline.book(self.destination, self.origin, return_date)
def book_one_way(self, date):
self.airline.book(self.origin, self.destination, date)
class FlightBooker:
def __init__(self, airline, origin, destination):
self.airline = airline
self.origin = origin
self.destination = destination
def book_two_way(self, start_date, return_date):
# Note: the definition for Airline.book is Airline.book(from, to, date)
self.airline.book(self.origin, self.destination, start_date)
self.airline.book(self.destination, self.origin, return_date)
def book_one_way(self, date):
self.airline.book(self.origin, self.destination, date)
“ Code without tests is broken as designed. ”
- Jacob Kaplan-Moss
< DRY KISS!
< SOLID!
(Rinse and repeat)
from django.test import TestCase
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"')
< Creates test data
< Tests behaviours
$ ./manage.py test
Creating test database for alias 'default'...
E
======================================================================
ERROR: unittests.tests (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: unittests.tests
Traceback (most recent call last):
[...]
ImportError: cannot import name 'Animal'
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
Destroying test database for alias 'default'...
$ _
from django.db import models
class Animal(models.Model):
name = models.CharField(max_length=30)
sound = models.CharField(max_length=30)
def speak(self):
return 'The %s says "%s"' % (self.name, self.sound)
$ ./manage.py test
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.003s
OK
Destroying test database for alias 'default'...
from django.db import models
class Animal(models.Model):
name = models.CharField(max_length=30)
sound = models.CharField(max_length=30)
def speak(self):
return 'The %s says "%s"' % (self.name, self.sound)
def __unicode__(self):
return self.name
< WRONG! It's a new UNTESTED functionality!
In this case, the code isn't complex enough to require refactoring.
< YAGNI!
You are working in a feature branch. You can commit more than once per test cycle, but a pull request only when step 5 is completed.
You are hopefully working in develop, or perhaps (gasp!) in master. In any case, don't commit until step 5 is completed.
Pro tip : USE GIT FLOW.
“ Planning is everything. Plans are nothing.. ”
- Field Marshal Helmuth Graf von Moltke
(Lettuce + Splinter)
(Ruby)
Feature: Behave example
In order to play with Behave
As beginners
We'll test Google Search
Scenario: Find ctrlweb
Given I search for "ctrlweb"
Then the result page will include "CTRL+WEB"
Scenario: Find Montreal-Django
Given I search for "Montreal-Django"
Then the result page will include "Django Meetups"
tutorial.feature:
from behave import *
from pytest import fail
from selenium.common.exceptions import NoSuchElementException
@given('I search for "{text}"')
def step_impl(context, text):
context.browser.get('https://www.google.ca/search?q='+text)
@then('the result page will include "{text}"')
def step_impl(context, text):
try:
context.browser.find_element_by_partial_link_text(text)
except NoSuchElementException:
fail('%r not in result page' % text)
pass
tutorial.py:
from selenium import webdriver
def before_all(context):
context.browser = webdriver.Chrome()
def after_all(context):
context.browser.quit()
environment.py:
$ behave
Feature: Behave example # tutorial.feature:1
In order to play with Behave
As beginners
We'll test Google Search
Scenario: Find ctrlweb # tutorial.feature:6
Given I search for "ctrlweb" # steps/tutorial.py:6 1.773s
Then the result page will include "CTRL+WEB" # steps/tutorial.py:11 0.067s
Scenario: Find Montreal-Django # tutorial.feature:10
Given I search for "Montreal-Django" # steps/tutorial.py:6 0.508s
Then the result page will include "Django Meetups" # steps/tutorial.py:11 0.397s
1 feature passed, 0 failed, 0 skipped
2 scenarios passed, 0 failed, 0 skipped
4 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m2.745s
$ _
“ The heart of software is its ability to solve domain-related problems for its user. ”
- Eric Evans
A fake usually takes shortcuts in order to make its objects suitable for tests, but not for production.
A stub provide predefined answers to calls made during a test.
A mock is an object pre-programmed with specifications of the calls they are expected to receive.
from django.test import TestCase
from unittest.mock import Mock
from django.utils import timezone
from .models import BlogPost
class BlogPostTestCase(TestCase):
def test_blog_post_is_published(self):
blogpost = Mock(spec=BlogPost)
blogpost.publish_date = timezone.now()
self.assertTrue(blogpost.is_published)
< Mock instance
< This will work
$ ./manage.py test mocking.tests
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
$ _
Add this to your settings to use real data instead :
import sys
if 'test' in sys.argv or 'test_coverage' in sys.argv:
DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3'}
Clean Coder - Robert C. Martin Domain-Driven Design - Eric Evans Django Model Behaviors - Kevin Stone Using Mocks in Python - José R.C. Cruz Mocking Django - Matthew J. Morrison