DJANGO,

PYTEST, TOX, Y.. ACCIÓN!

Angel Velásquez <angvp@archlinux.org>

social stuff: @angvp

Para los que no me conocen...

  • Usando Django desde el 2010~
  • Programando desde antes de eso ^
  • Arch Linux Developer
  • Coach del taller de Django Girls organizado por LinuxChix y Argentina en Python en la PyCon Argentina Mendoza 2015.
  • Contribuyo en varias apps para django: django-changuito, django-klingon, django-oml, drf-lafv, rpc4django, etc.. 

Algunas preguntas

  • Quién debería escribir tests?
  • Por qué deberían escribirse tests?
  • Cuáles son los mitos mas repetidos del testing?

¿Qué puedo testear de mi proyecto?

  1. Modelos
  2. Vistas
  3. Formularios
  4. Widgets de algunos forms
  5. Comandos
  6. template tags
  7. ... etc

¿Por qué pytest?

  1. Código mas python
  2. Atracciones innovadoras: (sistema de plugins, fixtures)
  3. Plugins que te van a ayudar a escribir mejores programas en una sola herramienta!: (django, cov, sugar, xdist, pep8, etc)
  4. Juega bien con otros (nose, unittest)
  5. El parámetro --pdb ! si un test se rompe podemos entrar a debuggear de una vez, super útil para los amantes del TDD

Testeando un modelo

from __future__ import unicode_literals

from django.db import models


class Book(models.Model):
    title = models.CharField(max_length=200)
    isbn = models.CharField(max_length=20)
    value = models.DecimalField(max_digits=5, decimal_places=2)

    def __unicode__(self):
        return "The book {} costs: {}".format(self.title, self.value)

La idea no es probar campo por campo, sabemos que Django tiene una batería de tests donde se probó todo esto, la idea acá sería hacer tests las cosas nuestras, en este caso vamos a hacer test de__unicode__

Testeando un modelo (unittest)

from ..models import Book
from django.test import TestCase
from decimal import Decimal
# una clase 2 métodos + lowerCamelCase

class BookModelTestCase(TestCase):
    def setUp(self):
        book_data = {
            'title': 'meetup 12 2015',
            'isbn': '1112223334445',
            'value': Decimal('132.23')
        }
        self.book = Book.objects.create(**book_data)

    def test_unicode(self):
        self.assertEquals(self.book.__unicode__(),
                          "The book {} costs: {}".format(
                              "meetup 12 2015", Decimal('132.23')))
$ python manage.py test
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...

Testeando un modelo (pytest)

import pytest
from ..models import Book
from decimal import Decimal

pytestmark = pytest.mark.django_db

@pytest.fixture()
def create_book():
    book_data = {
        'title': 'meetup 12 2015',
        'isbn': '1112223334445',
        'value': Decimal('132.23')
    }
    return Book.objects.create(**book_data)

def test_unicode(create_book):
    book = create_book
    assert book.__unicode__() == "The book {} costs: {}".format(
        "meetup 12 2015", Decimal('132.23'))
py.test -q 
.
1 passed in 0.27 seconds

Fixtures de pytest-django

from django.core.urlresolvers import reverse


def test_change_settings(settings):
    # despues de que este test corre se resetea el settings de nuevo
    settings.DATE_FORMAT = 'd-m-Y'


def test_index(client):
    # client es una instancia de django.test.TestCase.TestClient
    url = reverse('index')
    rq = client.get(url)
    assert rq.status_code == 200

Algunos plugins (cov)

$ py.test --cov book
================================== test session starts ==================================
platform linux2 -- Python 2.7.11, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
django settings: dpte.settings (from ini file)
rootdir: /home/angvp/workspace/git/dpte, inifile: pytest.ini
plugins: pep8-1.0.6, cov-2.2.0, django-2.9.1
collected 1 items 

book/tests/test_models.py .
------------------- coverage: platform linux2, python 2.7.11-final-0 --------------------
Name                              Stmts   Miss  Cover
-----------------------------------------------------
book/__init__.py                      0      0   100%
book/admin.py                         1      0   100%
book/apps.py                          3      3     0%
book/migrations/0001_initial.py       6      0   100%
book/migrations/__init__.py           0      0   100%
book/models.py                        8      0   100%
book/tests/__init__.py                0      0   100%
book/tests/test_models.py            10      0   100%
book/views.py                         1      1     0%
-----------------------------------------------------
TOTAL                                29      4    86%

=============================== 1 passed in 0.33 seconds ================================
$ pip install pytest-cov 

Algunos plugins (sugar)

$ py.test book
Test session starts (platform: linux2, Python 2.7.11, pytest 2.8.5, pytest-sugar 0.5.1)
django settings: dpte.settings (from ini file)
rootdir: /home/angvp/workspace/git/dpte, inifile: pytest.ini
plugins: pep8-1.0.6, cov-2.2.0, django-2.9.1, sugar-0.5.1

 book/tests/test_models.py ✓                                           100% ██████████

Results (0.26s):
       1 passed
$ pip install pytest-sugar

Nota: sugar puede romper compatibilidad con otros plugins, pero para tener colores y ver algunas gráficas de progreso en la terminal de forma "linda" está genial. En este caso la slide no muestra, pero les juro que tiene colores!

Algunos plugins (xdist)

$ py.test -n 4
================================= test session starts =================================
platform linux2 -- Python 2.7.11, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
django settings: dpte.settings (from ini file)
rootdir: /home/angvp/workspace/git/dpte, inifile: pytest.ini
plugins: pep8-1.0.6, xdist-1.13.1, cov-2.2.0, django-2.9.1
gw0 [1] / gw1 [1] / gw2 [1] / gw3 [1]
scheduling tests via LoadScheduling
.
============================== 1 passed in 1.26 seconds ===============================
$ pip install pytest-xdist

Nota: No tiene sentido correr xdist con 1 solo test ;). Django soporta tests en paralelo desde la versión 1.9

Testeando en varios entornos

[tox]
skipsdist=True # para testear un proyecto y no un build
envlist =
       {py27,py34}-django{1.8,1.9}
       {py35}-django{1.8,1.9}

[testenv]
commands =  py.test -q 
deps =
       django1.7: Django<1.8
       django1.8: Django<1.9
       django1.9: Django<1.10
       pytest-django==2.9.1

tox es una herramienta que permite automatizar la creación de entornos virtuales y correr los tests que tenemos en diferentes versiones de Django, python se debe definir un archivo tox.ini en la raiz del proyecto

$ tox # and the magic begins ;)

Mas de tox

$ tox -e py27-django1.8,py27-django1.9 # tests de solo python27 y django 1.8 + 1.9

py27-django1.8 installed: Django==1.8.7,py==1.4.31,pytest==2.8.5,pytest-django==2.9.1,wheel==0.24.0
py27-django1.8 runtests: PYTHONHASHSEED='562148675'
py27-django1.8 runtests: commands[0] | py.test -qq
...
py27-django1.9 installed: Django==1.9,py==1.4.31,pytest==2.8.5,pytest-django==2.9.1,wheel==0.24.0
py27-django1.9 runtests: PYTHONHASHSEED='562148675'
py27-django1.9 runtests: commands[0] | py.test -qq
...
_______________________________________________ summary ________________________________________________
  py27-django1.8: commands succeeded
  py27-django1.9: commands succeeded
  congratulations :)

Algunas otras opciones de tox son:

  • -e : Para pasar la lista de entornos definidos en tox.ini
  • -l : Lista de los entornos disponibles que están en tox.ini
  • -r : Fuerza la re creación de los entornos virtuales
  • --help: muestra la ayuda y muchas mas opciones que no están acá

¿Preguntas?

Gracias!

  • github + twitter: @angvp
  • código en: https://github.com/angvp/dpte/
  • slides en: https://slides.com/angelvelasquez-1/deck-3/

django pytest tox y acción

By Angel Velásquez

django pytest tox y acción

Charla para el Django Meet-up BsAs de Diciembre 2015

  • 720