Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Тестирование и Django

Тестирование и Django

Когда тестировать, что тестировать, как тестировать, Как ускорить тесты и упростить их написание. Отказываемся от классических фикстур в пользу динамически создаваемых моделей.

Moscow Python Meetup
PRO

October 24, 2012
Tweet

More Decks by Moscow Python Meetup

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. 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)

    View Slide

  12. 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)

    View Slide

  13. Smoke Testing

    View Slide

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

    View Slide

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

    View Slide

  16. Continious
    Integration

    View Slide

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

    View Slide

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

    View Slide

  19. >>>  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. Фикстуры

    View Slide

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

    View Slide

  25. 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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. Django test runner
    SUCKS

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. $  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

    View Slide

  37. 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"

    View Slide

  38. TESTING
    TESTING

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide