Slide 1

Slide 1 text

Тестирование Илья Барышев @coagulant Moscow Django Meetup №6 и Django

Slide 2

Slide 2 text

Защита от регрессий

Slide 3

Slide 3 text

Быстрые изменения в коде

Slide 4

Slide 4 text

Меняет подход к написанию кода

Slide 5

Slide 5 text

Пойдёт на пользу вашему проекту

Slide 6

Slide 6 text

Модульное тестирование

Slide 7

Slide 7 text

       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)

Slide 8

Slide 8 text

Модели Формы Views? Контекст-процессоры Middleware Template tags, filters Unittest

Slide 9

Slide 9 text

Тестируйте поведение А не имплементацию

Slide 10

Slide 10 text

Функциональное тестирование

Slide 11

Slide 11 text

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)

Slide 12

Slide 12 text

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)

Slide 13

Slide 13 text

Smoke Testing

Slide 14

Slide 14 text

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')

Slide 15

Slide 15 text

Как мы тестируем

Slide 16

Slide 16 text

Continious Integration

Slide 17

Slide 17 text

Покрытие важно Но не делайте из него фетиш

Slide 18

Slide 18 text

mock http://www.voidspace.org.uk/python/mock/

Slide 19

Slide 19 text

>>>  real.method(3,  4,  5,  key='value') >>>  my_mock.called True >>>  my_mock.call_count 1 >>>  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

Slide 20

Slide 20 text

@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)

Slide 21

Slide 21 text

from  mock  import  patch from  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())

Slide 22

Slide 22 text

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())

Slide 23

Slide 23 text

Фикстуры

Slide 24

Slide 24 text

Обычный тест с фикстурами [ { "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

Slide 25

Slide 25 text

django-­‐any https://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)

Slide 26

Slide 26 text

factory_boy https://github.com/dnerdy/factory_boy

Slide 27

Slide 27 text

import  factory from  models  import  MyUser class  UserFactory(factory.Factory):        FACTORY_FOR  =  MyUser        first_name  =  'John'        last_name  =  'Doe'        admin  =  False

Slide 28

Slide 28 text

#  Экземпляр  User,  не  сохранённый  в  базу user  =  UserFactory.build() #  Инстанс,  сохранённый  в  базу user  =  UserFactory.create() #  Создаём  инстанс  с  конкретыми  значениями user  =  UserFactory.create(name=u'Василий',  age=25)

Slide 29

Slide 29 text

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]'

Slide 30

Slide 30 text

class  UserWithEmailFactory(UserFactory):        email  =  factory.Sequence( lambda  n:  'person{0}@example.com'.format(n)) >>>  UserFactory().email '[email protected]' >>>  UserFactory().email     '[email protected]'

Slide 31

Slide 31 text

Django test runner SUCKS

Slide 32

Slide 32 text

INSTALLED_APPS  =  (        ...        #3rd-­‐party  apps        'south',        'sorl.thumbnail',        'pytils',        'pymorphy',                  'compressor',        'django_nose',        'django_geoip',        'mptt',        'widget_tweaks',        'guardian',                ... Несколько сотен тестов

Slide 33

Slide 33 text

/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  *

Slide 34

Slide 34 text

django-­‐nose https://github.com/jbalogh/django-­‐nose

Slide 35

Slide 35 text

$  pip  install  django-­‐nose #  settings.py   INSTALLED_APPS  =  (        ...        'django_nose',        ... ) TEST_RUNNER  =  'django_nose.NoseTestSuiteRunner'

Slide 36

Slide 36 text

$  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

Slide 37

Slide 37 text

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"

Slide 38

Slide 38 text

TESTING TESTING

Slide 39

Slide 39 text

SQLite для быстрых тестов Если ваш проект позволяет

Slide 40

Slide 40 text

Параллелим тесты Нетрудоёмкое ускоение

Slide 41

Slide 41 text

Секунды 0 100 200 300 400 126 169 326 Ran  337  tests  in  326.664s OK  (SKIP=2) $  ./manage.py  -­‐-­‐processes=N 1 процесс 2 процесса 3 процесса

Slide 42

Slide 42 text

Спасибо за внимание [email protected] @coagulant http://blog.futurecolors.ru/