PyTest et WebTest

PyTest et WebTest

Sommaire

  • PyTest pour les tests unitaires
  • WebTest pour les tests fonctionnels

PyTest : pour les tests unitaires

Par Holger Krekel, l'auteur entre autres de Tox, pypy, devpi

Un remplaçant (mais existant depuis plus longtemps) à Unittest

Bien plus avancé, évolué, et pratique

Installation et utilisation

  1. pip install pytest pytest-django pytest-cov
  2. py.test

py.test est compatible avec votre code actuel

Bonus

  • Liste des 10 tests les plus lents : --duration=10
  • S'arrêter après le premier échec : -x (ou --maxfail=n)
  • Direct dans pdb : --pdb
  • Direct sur pastebin : --pastebin=failed
  • Modification des tracebacks : --showlocals --tb=long

Bonus (bis)

Lancer seulement certains tests :

  • par nom : -k test_foo
  • par chemin : tests/bar/
  • par module : test_baz.py
  • par « mark » : -m functional ou -m "not functional"

Réutiliser la base de données

  • Réutiliser la BDD : --reuse-db
  • Forcer la création de la BDD : --create-db
  • Options par défaut dans le fichier pytest.ini
        [pytest]
        addopts = --reuse-db --x --pdb --tb=native
      

Autoriser l'accès à la BDD

@pytest.mark.django_db

Autoriser, explicitement, l'accès à la BDD.

On écrit des tests unitaires, et dans la mesure du possible, rapides.

Autoriser l'accès, parfois

        @pytest.mark.django_db
        def test_avec_db(self):  # fonction/méthode
      
        @pytest.mark.django_db
        class TestFoo(TestCase):  # classe
      
        pytestmark = pytest.mark.django_db  # module
      

Nan sérieux, lâche moi la BDD

dans conftest.py

        def pytest_collection_modifyitems(items):
            for item in items:
                item.keywords['django_db'] = pytest.mark.django_db
      

Les « marks »

  • Lister les mark disponibles : --markers
  • Ajouter ses propres mark dans pytest.ini :
        [pytest]
        markers =
            functional: mark a test as functional.
      

Puis -m functional ou -m "not functional"

Les assertions selon Unittest

self.assertEqual(foo(), 2)
          File "/home/mathieu/tests/test_example.py", line 11, in test_bidon
            self.assertEqual(foo(), 2)
          File "/usr/lib64/python2.7/unittest/case.py", line 515, in assertEqual
            assertion_func(first, second, msg=msg)
          File "/usr/lib64/python2.7/unittest/case.py", line 508, in _baseAssertEqual
            raise self.failureException(msg)
        AssertionError: 1 != 2
      

Les assertions selon py.test

assert foo() == 2
          File "/home/mathieu/tests/test_example.py", line 13, in test_bidon
            assert foo() == 2
        AssertionError: assert 1 == 2
         +  where 1 = foo()
      

Les assertions selon unittest (bis)

self.assertEqual({'foo': 1}, {'foo': 2, 'bar': 2})
       AssertionError: {'foo': 1} != {'foo': 2, 'bar': 2}
       - {'foo': 1}
       + {'bar': 2, 'foo': 2}
      

Les assertions selon py.test (bis)

assert {'foo': 1} == {'foo': 2, 'bar': 2}
        AssertionError: assert {'foo': 1} == {'bar': 2, 'foo': 2}
          Differing items:
          {'foo': 1} != {'foo': 2}
          Right contains more items:
          {'bar': 2}
      

Configuration

Au niveau global, dans le fichier pytest.ini : principalement pour la déclaration des « mark » et les options par défaut

Au niveau local, impacte le répertoire en cours et ses descendants, dans le fichier conftest.py : pour les fixtures, les plugins, les hooks

Les fixtures : injection de dépendances

La partie (mal?)heureusement magique de py.test

        def test_view(rf, settings):
            assert rf.get('/').path == '/'
            settings.USE_TZ = True
            assert settings.USE_TZ
      

Les fixtures : setup_view

Pour tester unitairement ses vues

        @pytest.fixture(scope='session')
        def setup_view():
            def _setup_view(view, request, *args, **kwargs):
                view.request = request
                view.args = args
                view.kwargs = kwargs
                return view
            return _setup_view
      

Les fixtures : des exemples

  • setup_view : tester unitairement ses vues
  • stubber : pour des stub faciles
  • stub_save : pour tester Model.save() sans toucher à la BDD
  • app : une application WebTest
  • view, user, form...

Les paramètres

Écrire une seule fois le test, le lancer avec plusieurs entrées

        @pytest.mark.parametrize(("input", "expected"),
                                 [("3+5", 8), ("2+4", 6)])
        def test_eval(input, expected):
            assert eval(input) == expected
      

Les fixtures et les paramètres

        @pytest.fixture(scope="module",
                        params=["merlinux.eu", "mail.python.org"])
        def smtp(request):
            smtp = smtplib.SMTP(request.param)
            def fin():
                smtp.close()
            request.addfinalizer(fin)
            return smtp
      

WebTest : pour les tests fonctionnels

Par Ian Bicking, l'auteur entre autres de... pfouuu (pip, virtualenv, WebOb)

Un remplaçant au client de test de Django, qui fonctionne au niveau WSGI au lieu de HTTP

Installation et utilisation

  1. pip install webtest django-webtest
  2. py.test

webtest est compatible avec votre code actuel (ou presque)

Utiliser WebTest avec py.test

Pour utiliser django-webtest avec py.test, créer la fixture :

        @pytest.fixture(scope='function')
        def app(request):
            wtm = django_webtest.WebTestMixin()
            wtm._patch_settings()
            request.addfinalizer(wtm._unpatch_settings)
            return django_webtest.DjangoTestApp()
      

Tests automatiques

  • status code 2xx ou 3xx (sinon status=404 ou status="*")
  • réponse WSGI valide
  • pas d'erreurs envoyées dans environ['wsgi.errors']

CSRF

Les validations de CSRF ne sont pas désactivés

Du coup, les raw post sont plus compliqués... heureusement qu'on en fait pas souvent

        class MyTestCase(WebTest):
            csrf_checks = False
      

Soumission de formulaires

        def test_login(self)
            form = self.app.get(reverse('auth_login')).form
            form['username'] = 'foo'
            form['password'] = 'bar'
            res = form.submit().follow()
            assert res.context['user'].username == 'foo'
      

Clicks

click sur un lien dans une response suivant un pattern sur :

  • description : contenu (tout ce qui est entre <a> et </a>)
  • linkid : attribut id
  • href : attribut href
  • index : numéro du lien

Exemple : res.click(href=reverse('login'))

Validation d'un contenu avec PyQuery

Fournit une interface à la jQuery pour faire des requêtes

  • nb commentaires : len(res.pyquery.find('.comment'))
  • contenu : res.pyquery.find('#foo')) == 'foo'
  • et sinon, lxml, json, beautiful soup

Showbrowser

Juste magique quand on debug ses tests fonctionnels, a utiliser avec l'option --pdb de py.test :

        >>> res.showbrowser()
      

Conclusion

Plus aucune raison pour ne pas écrire de tests !

formation à py.test : http://www.merlinux.eu/~hpk/testing.pdf

ces slides : http://mathieu.agopian.info/presentations/2013_09_djangocong/

Questions !?