Тестирование и django
DESCRIPTION
Когда тестировать, что тестировать, как тестировать, Как ускорить тесты и упростить их написание. Отказываемся от классических фикстур в пользу динамически создаваемых моделей.TRANSCRIPT
![Page 1: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/1.jpg)
Тестирование
Илья Барышев@coagulant
Moscow Django Meetup №6
и Django
![Page 2: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/2.jpg)
Защита от регрессий
![Page 3: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/3.jpg)
Быстрые изменения в коде
![Page 4: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/4.jpg)
Меняет подход к написанию кода
![Page 5: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/5.jpg)
Пойдёт на пользу вашему проекту
![Page 6: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/6.jpg)
Модульное тестирование
![Page 7: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/7.jpg)
def test_vin_is_valid(self): valid_vins = ('2G1FK1EJ7B9141175', '11111111111111111',) for valid_vin in valid_vins: self.assertEqual(vin_validator(valid_vin), None)
def test_vin_is_invalid(self): invalid_vins = ('abc', u'M05C0WDJAN60M33TUP6',) for invalid_vin in invalid_vins: self.assertRaises(ValidationError,
vin_validator, invalid_vin)
![Page 8: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/8.jpg)
Модели
Формы
Views?
Контекст-процессоры
Middleware
Template tags, filters
Unittest
![Page 9: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/9.jpg)
Тестируйте поведениеА не имплементацию
![Page 10: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/10.jpg)
Функциональное тестирование
![Page 11: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/11.jpg)
django.test.client.Client
def testPostAsAuthenticatedUser(self): data = self.getValidData(Article.objects.get(pk=1)) self.client.login(username="normaluser", password="normaluser") self.response = self.client.post("/post/", data) self.assertEqual(self.response.status_code, 302) self.assertEqual(Comment.objects.count(), 1)
![Page 12: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/12.jpg)
django.test.сlient.RequestFactory
def test_post_ok(self): request = RequestFactory().post(reverse('ch_location'), {'location_id': 77}) request.cookies = {}
response = change_location(request)
self.assertEqual(response.cookies['LOCATION'].value, '77') self.assertEqual(response.status_code, 302)
![Page 13: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/13.jpg)
Smoke Testing
![Page 14: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/14.jpg)
def test_password_recovery_smoke(self): """ Урлы восстановления пароля. Логика уже протестирована в django-‐password-‐reset """ response_recover = self.client.get(reverse('pass_recover')) self.assertEqual(response_recover.status_code, 200)
self.assertContains(response_recover, u'Восстановление пароля') self.assertTemplateUsed(response_recover, 'password_reset/recovery_form.html')
![Page 15: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/15.jpg)
Как мы тестируем
![Page 16: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/16.jpg)
ContiniousIntegration
![Page 17: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/17.jpg)
Покрытие важноНо не делайте из него фетиш
![Page 18: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/18.jpg)
mockhttp://www.voidspace.org.uk/python/mock/
![Page 19: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/19.jpg)
>>> real.method(3, 4, 5, key='value')
>>> my_mock.calledTrue
>>> my_mock.call_count1
>>> mock.method.assert_called_with(3, 4, 5)Traceback (most recent call last): ...AssertionError: Expected call: method(3, 4, 5)Actual call: method(3, 4, 5, key='value')
>>> real = SomeClass()>>> my_mock = MagicMock(name='method')>>> real.method = my_mock
![Page 20: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/20.jpg)
@patch('twitter.Api')def test_twitter_tag_simple_mock(self, ApiMock): api_instance = ApiMock.return_value api_instance.GetUserTimeline.return_value = SOME_JSON
output, context = render_template("""{% load twitter_tag %} {% get_tweets for "jresig" as tweets %}""")
api_instance.GetUserTimeline.assert_called_with( screen_name='jresig', include_rts=True, include_entities=True)
![Page 21: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/21.jpg)
from mock import patchfrom django.conf import settings
@patch.multiple(settings, APPEND_SLASH=True, MIDDLEWARE_CLASSES=(common_middleware,))def test_flatpage_doesnt_require_trailing_slash(self): form = FlatpageForm(data=dict(url='/no_trailing_slash', **self.form_data)) self.assertTrue(form.is_valid())
![Page 22: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/22.jpg)
from django.test.utils import override_settings
@override_settings( APPEND_SLASH=False, MIDDLEWARE_CLASSES=(common_middleware,))def test_flatpage_doesnt_require_trailing_slash(self): form = FlatpageForm(data=dict(url='/no_trailing_slash', **self.form_data)) self.assertTrue(form.is_valid())
![Page 23: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/23.jpg)
Фикстуры
![Page 24: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/24.jpg)
Обычный тест с фикстурами
[ { "model": "docs.documentrelease", "pk": 1, "fields": { "lang": "en", "version": "dev", "scm": "svn", "scm_url": "http://code.djangoproject.com/svn/django/trunk/docs", "is_default": false } }, { "model": "docs.documentrelease", "pk": 2, "fields": { "lang": "en", "version": "1.0", "scm": "svn", "scm_url": "http://code.djangoproject.com/svn/django/branches/releases/1.0.X/docs", "is_default": false } }, { "model": "docs.documentrelease", "pk": 3, "fields": { "lang": "en", "version": "1.1", "scm": "svn", "scm_url": "http://code.djangoproject.com/svn/django/branches/releases/1.1.X/docs", "is_default": false } }, { "model": "docs.documentrelease", "pk": 4, "fields": { "lang": "en", "version": "1.2", "scm": "svn", "scm_url": "http://code.djangoproject.com/svn/django/branches/releases/1.2.X/docs", "is_default": false } } { "model": "docs.documentrelease", "pk": 5, "fields": { "lang": "en", "version": "1.3", "scm": "svn", "scm_url": "http://code.djangoproject.com/svn/django/trunk/docs", "is_default": true } }]
![Page 25: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/25.jpg)
django-‐anyhttps://github.com/kmmbvnr/django-‐any
from django_any import any_model
class TestMyShop(TestCase): def test_order_updates_user_account(self): account = any_model(Account, amount=25,
user__is_active=True) order = any_model(Order, user=account.user,
amount=10) order.proceed()
account = Account.objects.get(pk=account.pk) self.assertEquals(15, account.amount)
![Page 26: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/26.jpg)
factory_boyhttps://github.com/dnerdy/factory_boy
![Page 27: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/27.jpg)
import factoryfrom models import MyUser
class UserFactory(factory.Factory): FACTORY_FOR = MyUser first_name = 'John' last_name = 'Doe' admin = False
![Page 28: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/28.jpg)
# Экземпляр User, не сохранённый в базуuser = UserFactory.build()
# Инстанс, сохранённый в базуuser = UserFactory.create()
# Создаём инстанс с конкретыми значениямиuser = UserFactory.create(name=u'Василий', age=25)
![Page 29: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/29.jpg)
class UserFactory(factory.Factory): first_name = 'Vasily' last_name = 'Pupkin' email = factory.LazyAttribute(
lambda u: '{0}.{1}@example.com'.format(u.first_name, u.last_name).lower())
>>> UserFactory().email'[email protected]'
![Page 30: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/30.jpg)
class UserWithEmailFactory(UserFactory): email = factory.Sequence(
lambda n: 'person{0}@example.com'.format(n))
>>> UserFactory().email'[email protected]'
>>> UserFactory().email '[email protected]'
![Page 31: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/31.jpg)
Django test runnerSUCKS
![Page 32: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/32.jpg)
INSTALLED_APPS = ( ...
#3rd-‐party apps 'south', 'sorl.thumbnail', 'pytils', 'pymorphy', 'compressor', 'django_nose', 'django_geoip', 'mptt', 'widget_tweaks', 'guardian', ...
Несколько сотен тестов
![Page 33: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/33.jpg)
/tests __init__.py
test_archive.py test_blog_model.py test_modified.py test_post_model.py test_redactor.py test_views.py test_cross_post.py
# -‐*-‐ coding: utf-‐8 -‐*-‐
from test_archive import *from test_blog_model import *from test_modified import *from test_post_model import *from test_redactor import *from test_views import *from test_cross_post import *
![Page 34: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/34.jpg)
django-‐nose
https://github.com/jbalogh/django-‐nose
![Page 35: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/35.jpg)
$ pip install django-‐nose
# settings.py INSTALLED_APPS = ( ... 'django_nose', ...)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
![Page 36: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/36.jpg)
$ manage.py test -‐-‐with-‐ids -‐-‐failed
$ manage.py -‐-‐pdb
$ manage.py -‐-‐pdb-‐failures
$ manage.py test apps.comments.tests
$ manage.py test apps.comments.tests:BlogTestCase
$ manage.py test apps.comments.tests:BlogTestCase.test_index
$ manage.py test
![Page 37: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/37.jpg)
from nose.plugins.attrib import attr
@attr(speed='slow', priority=1)def test_big_download(): import urllib # commence slowness..
$ nosetests -‐a speed=slow
$ nosetests -‐a '!slow'
$ nosetests -‐A "(priority > 5) and not slow"
![Page 38: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/38.jpg)
TESTING
TESTING
![Page 39: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/39.jpg)
SQLite для быстрых тестовЕсли ваш проект позволяет
![Page 40: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/40.jpg)
Параллелим тестыНетрудоёмкое ускоение
![Page 41: Тестирование и Django](https://reader034.vdocuments.mx/reader034/viewer/2022050907/557fb994d8b42a40118b4a41/html5/thumbnails/41.jpg)
Секунды
0 100 200 300 400
126
169
326
Ran 337 tests in 326.664sOK (SKIP=2)
$ ./manage.py -‐-‐processes=N
1 процесс
2 процесса
3 процесса