Slide 1

Slide 1 text

PYCON SETTE Florence, 17 April 2016 Pytest & Django are really good friends!!

Slide 2

Slide 2 text

PYCON SETTE about me Simone Dalla twitter @simodalla CTO @ SIA Unione dei Comuni Valli del Reno Lavino e Samoggia (Bologna, IT) Pythonista and Djangonauta

Slide 3

Slide 3 text

PYCON SETTE Topics: •testing and TDD overview •testing in Django overview •pytest and django-pytest intro •move from unittes to pytest https://[email protected]/simodalla/pycon7_pytest_django.git

Slide 4

Slide 4 text

PYCON SETTE My first programmig book…

Slide 5

Slide 5 text

PYCON SETTE

Slide 6

Slide 6 text

PYCON SETTE credits: @ThePraticalDev You’re a 10x hacker and it must be someone else’s fault!!! More books…

Slide 7

Slide 7 text

PYCON SETTE All code is guilty… …until proven innocent!

Slide 8

Slide 8 text

PYCON SETTE Test my software? https://en.wikipedia.org/wiki/Software_testing Unit test Functional test Integration test Regression test Acceptance test etc…

Slide 9

Slide 9 text

credits: @ThePraticalDev Oh yes, is true, testing is cool… but…

Slide 10

Slide 10 text

PYCON SETTE Test Drive Development

Slide 11

Slide 11 text

PYCON SETTE Testing in Django unittest & django testing tools

Slide 12

Slide 12 text

PYCON SETTE from django.core.urlresolvers import reverse
 from django.utils import timezone
 from django.test import TestCase
 
 from ..models import Question class QuestionViewTests(TestCase):
 def test_index_view_with_no_questions(self):
 response = self.client.get(reverse('polls:index'))
 self.assertEqual(response.status_code, 200)
 self.assertContains(response, "No polls are available.")
 
 def test_index_view_with_a_past_question(self):
 create_question(question_text="Past question.", days=-30)
 response = self.client.get(reverse('polls:index'))
 self.assertQuerysetEqual(
 response.context['latest_question_list'],
 ['']
 )

Slide 13

Slide 13 text

PYCON SETTE a mature full-featured Python testing tool

Slide 14

Slide 14 text

PYCON SETTE very pythonic tests & no boilerplate!

Slide 15

Slide 15 text

PYCON SETTE Plugin driven •pytest-django, Django integration •pytest-cov, coverage reporting •pytest-xdist, distributed/parallelized tests •… and more at:
 http://pytest.org/latest/plugins.html#external-plugins

Slide 16

Slide 16 text

PYCON SETTE many types of invocation: pytest - -help testing exception with context manager: raises marking test functions with attributes: markers monkeypatching/mocking modules and environments with fixture: monkeypatch many types of fixtures… Some cool features

Slide 17

Slide 17 text

PYCON SETTE $ mkvirtualenv pycon7_pytest_django $ workon pycon7_pytest_django $ pip install django pytest-django psycopg2 $ createdb pycon7_pytest_django $ django-admin startproject pycon7_pytest_django *** $ cd pycon7_pytest_django *** $ python manage.py startapp polls *** Demo time! $ git clone https://[email protected]/simodalla/ pycon7_pytest_django.git $ git checkout 3f5316d ***

Slide 18

Slide 18 text

PYCON SETTE CODE…CODE…CODE… https://docs.djangoproject.com/en/1.9/intro

Slide 19

Slide 19 text

PYCON SETTE class Question(models.Model):
 question_text = models.CharField(max_length=200)
 pub_date = models.DateTimeField('date published')
 
 def was_published_recently(self):
 now = timezone.now()
 return now - datetime.timedelta(days=1) <= self.pub_date <= now
 was_published_recently.admin_order_field = 'pub_date'
 was_published_recently.boolean = True
 was_published_recently.short_description = 'Published recently?'
 
 
 class Choice(models.Model):
 question = models.ForeignKey('Question', on_delete=models.CASCADE)
 choice_text = models.CharField(max_length=200)
 votes = models.IntegerField(default=0) models.py $ git checkout 44bd202

Slide 20

Slide 20 text

PYCON SETTE class IndexView(generic.ListView):
 template_name = 'polls/index.html'
 context_object_name = 'latest_question_list'
 
 def get_queryset(self):
 """
 Return the last five published questions (not including those set to be published in the future).
 """
 return Question.objects.filter(
 pub_date__lte=timezone.now()).order_by('-pub_date')[:5]
 views.py $ git checkout 44bd202

Slide 21

Slide 21 text

PYCON SETTE def create_question(question_text, days):
 """
 Creates a question with the given `question_text` and published the
 given number of `days` offset to now (negative for questions published
 in the past, positive for questions that have yet to be published).
 """
 time = timezone.now() + datetime.timedelta(days=days)
 return Question.objects.create(question_text=question_text, pub_date=time) class QuestionMethodTests(TestCase):
 
 def test_was_published_recently_with_future_question(self):
 """
 was_published_recently() should return False for questions whose
 pub_date is in the future.
 """
 time = timezone.now() + datetime.timedelta(days=30)
 future_question = Question(pub_date=time)
 self.assertEqual(future_question.was_published_recently(), False) 
 class QuestionViewTests(TestCase):
 
 def test_index_view_with_a_past_question(self):
 """
 Questions with a pub_date in the past should be displayed on the
 index page.
 """
 create_question(question_text="Past question.", days=-30)
 response = self.client.get(reverse('polls:index'))
 self.assertQuerysetEqual(
 response.context['latest_question_list'],
 ['']
 ) tests.py $ git checkout 44bd202

Slide 22

Slide 22 text

PYCON SETTE …demo time! $ python manage.py makemigrations $ python manage.py migrate $ python manage.py test Creating test database for alias 'default'... ........ ---------------------------------------------------- Ran 8 tests in 0.069s OK Destroying test database for alias 'default'...

Slide 23

Slide 23 text

PYCON SETTE $ touch pytest.ini py.test!! [pytest] DJANGO_SETTINGS_MODULE=pycon7_pytest_django.settings $ py.test polls/tests.py ============== test session starts =================================== platform darwin -- Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 django settings: pycon7_pytest_django.settings (from ini file) rootdir: /Users/simo/PycharmProjects/pycon7_pytest_django, inifile: pytest.ini plugins: django-2.9.1 collected 8 items polls/tests.py ........ ============== 8 passed in 1.79 seconds =============================== $ git checkout 551305f

Slide 24

Slide 24 text

PYCON SETTE Django TestCase just works!!

Slide 25

Slide 25 text

PYCON SETTE By default, tests are found in test_*.py Discovery Just run doctest & unittes Run tests written for nose Discovery configuration in pytest.ini with: python_files = check_*.py

Slide 26

Slide 26 text

PYCON SETTE Test reorganization tests.py tests package $ git checkout 6b7b4b7

Slide 27

Slide 27 text

PYCON SETTE $ py.test ===================== test session starts ========================= platform darwin -- Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 django settings: pycon7_pytest_django.settings (from ini file) rootdir: /Users/simo/PycharmProjects/pycon7_pytest_django, inifile: pytest.ini plugins: django-2.9.1 collected 8 items polls/tests/test_models.py ... polls/tests/test_views.py ..... ===================== 8 passed in 2.00 seconds ====================== Run all tests

Slide 28

Slide 28 text

PYCON SETTE $ py.test tests/ Run all the tests in a specific directory $ py.test -k test_foo Run all the tests cases that are named *test_foo* $ py.test —-durations=4 Run tests and show 4 slowest setup/test durations (0 for all) $ py.test —-n=4 Run tests in four parallel processses (plugin pytest-xdist) $ py.test —-reuse-db Reuse database from last run $ py.test —-reuse-db —-create-db Reuse database schema if models if changed Run tests…

Slide 29

Slide 29 text

PYCON SETTE # test_models.py
 
 import pytest def test_was_published_recently_with_future_question():
 pytest.fail()
 
 
 def test_was_published_recently_with_old_question():
 pytest.fail()
 
 
 def test_was_published_recently_with_recent_question():
 pytest.fail() unittest class $ git checkout ed46dd4 functions

Slide 30

Slide 30 text

PYCON SETTE $ git checkout 5df1f99 def test_was_published_recently_with_future_question(self):
 """
 was_published_recently() should return False for questions whose
 pub_date is in the future.
 """
 time = timezone.now() + datetime.timedelta(days=30)
 future_question = Question(pub_date=time)
 self.assertEqual(future_question.was_published_recently(), False) def test_was_published_recently_with_recent_question():
 time = timezone.now() - datetime.timedelta(hours=1)
 recent_question = Question(pub_date=time)
 assert recent_question.was_published_recently() is True testing with assert

Slide 31

Slide 31 text

PYCON SETTE $ git checkout c470411 @pytest.mark.parametrize("test_time,expected", [
 (timezone.now() + datetime.timedelta(days=30), False),
 (timezone.now() - datetime.timedelta(days=30), False),
 (timezone.now() - datetime.timedelta(hours=1), True),
 ])
 def test_was_published_recently(test_time, expected):
 question = Question(pub_date=test_time)
 assert question.was_published_recently() is expected tests parametrization $ py.test -v polls/tests/test_models.py::test_was_published_recently[test_time0-False] PASSED polls/tests/test_models.py::test_was_published_recently[test_time1-False] PASSED polls/tests/test_models.py::test_was_published_recently[test_time2-True] PASSED

Slide 32

Slide 32 text

PYCON SETTE $ git checkout a8eb54a # test_views.py class QuestionViewTests(TestCase):
 def test_index_view_with_no_questions(self):
 """
 If no questions exist, an appropriate message should be displayed.
 """
 response = self.client.get(reverse('polls:index'))
 self.assertEqual(response.status_code, 200)
 self.assertContains(response, "No polls are available.") @pytest.mark.django_db
 def test_index_view_with_no_questions(client):
 response = client.get(reverse('polls:index'))
 assert response.status_code == 200
 assert "No polls are available." in str(response.content) pytest-django & database

Slide 33

Slide 33 text

PYCON SETTE $ git checkout a8eb54a # no @pytest.mark.django_db def test_index_view_with_a_past_question(client):
 question = create_question(question_text="Past question.", days=-30)
 response = client.get(reverse('polls:index'))
 assert (list(response.context['latest_question_list']) == [question]) pytest-django & database $ py.test -v . . . test_views.py::test_index_view_with_a_past_question FAILED . . . Failed: Database access not allowed, use the "django_db" mark to enable it.

Slide 34

Slide 34 text

PYCON SETTE $ git checkout a8eb54a pytest fixtures != django fixtures @pytest.mark.django_db def test_index_view_with_a_past_question(client):
 question = create_question(question_text="Past question.", days=-30)
 response = client.get(reverse('polls:index'))
 assert (list(response.context['latest_question_list']) == [question])

Slide 35

Slide 35 text

PYCON SETTE django-pytest fixtures rf = instance of django.test.RequestFactory client = instance of django.test.Client admin_client = instance of django.test.Client that is logged in as an admin user admin_user = instance of a superuser, with username “admin” and password “password” django_user_model = user model used by Django django_username_field = field name used for the username on the user model db = ensure the Django database is set up transactional_db = database including transaction support live_server = runs a live Django server in a background thread settings = provide a handle on the django settings module

Slide 36

Slide 36 text

PYCON SETTE $ git checkout 4cecda0 pytest fixtures @pytest.fixture
 def question_in_the_past(db):
 return create_question(question_text="Past question.", days=-30)
 
 @pytest.fixture
 def question_in_the_future(db):
 return create_question(question_text="Future question.", days=30)
 
 @pytest.fixture
 def two_questions_in_the_past(db):
 return [create_question(question_text="Past question 1.", days=-30),
 create_question(question_text="Past question 2.", days=-5)] @pytest.mark.django_db def test_index_view_with_a_past_question(
 self, client, question_in_the_past):
 response = client.get(reverse('polls:index'))
 assert list(response.context['latest_question_list']) == [
 question_in_the_past] re

Slide 37

Slide 37 text

PYCON SETTE Good idea!?! …“test of test” , another tests for complicated or critical fixtures.

Slide 38

Slide 38 text

PYCON SETTE my 2 cent on django fixtures… …use factory_boy!!! $ pip install factory_boy @pytest.fixture
 def two_questions_in_the_past(db):
 return [QuestionFactory.build(question_text=“Past question 1.", days=-30),
 QuestionFactory.build(question_text="Past question 2.", days=-5)] http://factoryboy.readthedocs.org/en/latest/index.html

Slide 39

Slide 39 text

PYCON SETTE last my 2 cent on pytest and django… …works with both worlds!

Slide 40

Slide 40 text

PYCON SETTE Crowdfunding a dev sprint in June 2016 for pytest 3.0 (+ tox) https://www.indiegogo.com/projects/python-testing-sprint-mid-2016 http://pytest.org/latest/announce/sprint2016.html

Slide 41

Slide 41 text

PYCON SETTE More info? Help? Docs? Florian Bruhin, pytest - Rapid Simple Testing - Swiss Python Summit 2016 https://www.youtube.com/watch?v=rCBHkQ_LVIs Holger Krekel, Improving automated testing with py.test - PyCon 2014
 
 https://www.youtube.com/watch?v=AiThU6JQbE8
 Andreas Pelme, Pytest: help you write better Django apps, DjangoCon Europe 2014
 
 https://www.youtube.com/watch?v=aaArYVh6XSM
 official Django testing documentation
 
 https://docs.djangoproject.com/en/1.9/topics/testing/
 pytest documentation
 
 http://pytest.org/
 pytest-django documentation
 
 https://pytest-django.readthedocs.org

Slide 42

Slide 42 text

PYCON SETTE twitter @simodalla github simodalla slides http://speakerdeck.com/simodalla Thank you!