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


 Moscow Django Meetup №6
Защита от
регрессий
Быстрые
изменения
  в коде
Меняет подход к
написанию кода
Пойдёт на пользу
 вашему проекту
Модульное
тестирование
 	
  	
  	
  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)
Unittest

Модели      Контекст-процессоры
Формы       Middleware
Views?      Template tags, filters
Тестируйте поведение
     А не имплементацию
Функциональное
 тестирование
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)
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)
Smoke Testing
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,
	
  	
  	
  	
  	
  	
  	
  	
  self.assertTemplateUsed(response_recover,
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  u'Восстановление	
  пароля')
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  'password_reset/recovery_form.html')
Как мы тестируем
Continious
Integration
Покрытие важно
  Но не делайте из него фетиш
mock
http://www.voidspace.org.uk/python/mock/
>>>	
  real	
  =	
  SomeClass()
>>>	
  my_mock	
  =	
  MagicMock(name='method')
>>>	
  real.method	
  =	
  my_mock
>>>	
  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')
@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)
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())
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())
Фикстуры
[
    {
         "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
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)
factory_boy
https://github.com/dnerdy/factory_boy
import	
  factory
from	
  models	
  import	
  MyUser

class	
  UserFactory(factory.Factory):
	
  	
  	
  	
  FACTORY_FOR	
  =	
  MyUser
	
  	
  	
  	
  first_name	
  =	
  'John'
	
  	
  	
  	
  last_name	
  =	
  'Doe'
	
  	
  	
  	
  admin	
  =	
  False
#	
  Инстанс,	
  сохранённый	
  в	
  базу
user	
  =	
  UserFactory.create()

#	
  Экземпляр	
  User,	
  не	
  сохранённый	
  в	
  базу
user	
  =	
  UserFactory.build()

#	
  Создаём	
  инстанс	
  с	
  конкретыми	
  значениями
user	
  =	
  UserFactory.create(name=u'Василий',	
  age=25)
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
'vasily.pupkin@example.com'
class	
  UserWithEmailFactory(UserFactory):
	
  	
  	
  	
  email	
  =	
  factory.Sequence(
                   lambda	
  n:	
  'person{0}@example.com'.format(n))


>>>	
  UserFactory().email
'person0@example.com'

>>>	
  UserFactory().email	
  	
  
'person1@example.com'
Django test runner
     SUCKS
INSTALLED_APPS	
  =	
  (
	
  	
  	
  	
  ...

	
  	
  	
  	
  #3rd-­‐party	
  apps
	
  	
  	
  	
  'south',
	
  	
  	
  	
  'sorl.thumbnail',
	
  	
  	
  	
  'pytils',
	
  	
  	
  	
  'pymorphy',	
  	
  	
  	
  	
  
	
  	
  	
  	
  'compressor',
                                                  Несколько сотен
	
  	
  	
  	
  'django_nose',                        тестов
	
  	
  	
  	
  'django_geoip',
	
  	
  	
  	
  'mptt',
	
  	
  	
  	
  'widget_tweaks',
	
  	
  	
  	
  'guardian',
	
  	
  	
  	
  
	
  	
  	
  	
  ...
/tests                               #	
  -­‐*-­‐	
  coding:	
  utf-­‐8	
  -­‐*-­‐
	
  	
  	
  	
  __init__.py
                test_archive.py      from	
  test_archive	
  import	
  *
	
  	
  	
  	
  test_blog_model.py   from	
  test_blog_model	
  import	
  *
	
  	
  	
  	
  test_modified.py     from	
  test_modified	
  import	
  *
	
  	
  	
  	
  test_post_model.py   from	
  test_post_model	
  import	
  *
	
  	
  	
  	
  test_redactor.py     from	
  test_redactor	
  import	
  *
	
  	
  	
  	
  test_views.py        from	
  test_views	
  import	
  *
	
  	
  	
  	
  test_cross_post.py   from	
  test_cross_post	
  import	
  *
django-­‐nose




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


  #	
  settings.py	
  
  INSTALLED_APPS	
  =	
  (
  	
  	
  	
  	
  ...
  	
  	
  	
  	
  'django_nose',
  	
  	
  	
  	
  ...
  )


  TEST_RUNNER	
  =	
  'django_nose.NoseTestSuiteRunner'
$	
  manage.py	
  test

$	
  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	
  -­‐-­‐with-­‐ids	
  -­‐-­‐failed
$	
  manage.py	
  -­‐-­‐pdb
$	
  manage.py	
  -­‐-­‐pdb-­‐failures
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"
TESTING




TESTING
SQLite для
быстрых тестов
 Если ваш проект позволяет
Параллелим тесты
    Нетрудоёмкое ускоение
Ran	
  337	
  tests	
  in	
  326.664s
                    OK	
  (SKIP=2)


1 процесс                                                         326

2 процесса
      Секунды                                   169

3 процесса                               126
                0                  100              200     300         400




                     $	
  ./manage.py	
  -­‐-­‐processes=N
Спасибо за внимание


baryshev@futurecolors.ru
@coagulant                      http://blog.futurecolors.ru/

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

  • 1.
    Тестирование и Django Илья Барышев @coagulant Moscow Django Meetup №6
  • 2.
  • 3.
  • 4.
  • 5.
    Пойдёт на пользу вашему проекту
  • 6.
  • 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)
  • 8.
    Unittest Модели Контекст-процессоры Формы Middleware Views? Template tags, filters
  • 9.
    Тестируйте поведение А не имплементацию
  • 10.
  • 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)
  • 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)
  • 13.
  • 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,                self.assertTemplateUsed(response_recover,                                                        u'Восстановление  пароля')                                                                'password_reset/recovery_form.html')
  • 15.
  • 16.
  • 17.
    Покрытие важно Но не делайте из него фетиш
  • 18.
  • 19.
    >>>  real  =  SomeClass() >>>  my_mock  =  MagicMock(name='method') >>>  real.method  =  my_mock >>>  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')
  • 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)
  • 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())
  • 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())
  • 23.
  • 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
  • 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)
  • 26.
  • 27.
    import  factory from  models  import  MyUser class  UserFactory(factory.Factory):        FACTORY_FOR  =  MyUser        first_name  =  'John'        last_name  =  'Doe'        admin  =  False
  • 28.
    #  Инстанс,  сохранённый  в  базу user  =  UserFactory.create() #  Экземпляр  User,  не  сохранённый  в  базу user  =  UserFactory.build() #  Создаём  инстанс  с  конкретыми  значениями user  =  UserFactory.create(name=u'Василий',  age=25)
  • 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 'vasily.pupkin@example.com'
  • 30.
    class  UserWithEmailFactory(UserFactory):        email  =  factory.Sequence( lambda  n:  'person{0}@example.com'.format(n)) >>>  UserFactory().email 'person0@example.com' >>>  UserFactory().email     'person1@example.com'
  • 31.
  • 32.
    INSTALLED_APPS  =  (        ...        #3rd-­‐party  apps        'south',        'sorl.thumbnail',        'pytils',        'pymorphy',                  'compressor', Несколько сотен        'django_nose', тестов        'django_geoip',        'mptt',        'widget_tweaks',        'guardian',                ...
  • 33.
    /tests #  -­‐*-­‐  coding:  utf-­‐8  -­‐*-­‐        __init__.py test_archive.py from  test_archive  import  *        test_blog_model.py from  test_blog_model  import  *        test_modified.py from  test_modified  import  *        test_post_model.py from  test_post_model  import  *        test_redactor.py from  test_redactor  import  *        test_views.py from  test_views  import  *        test_cross_post.py from  test_cross_post  import  *
  • 34.
    django-­‐nose https://github.com/jbalogh/django-­‐nose
  • 35.
    $  pip  install  django-­‐nose #  settings.py   INSTALLED_APPS  =  (        ...        'django_nose',        ... ) TEST_RUNNER  =  'django_nose.NoseTestSuiteRunner'
  • 36.
    $  manage.py  test $  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  -­‐-­‐with-­‐ids  -­‐-­‐failed $  manage.py  -­‐-­‐pdb $  manage.py  -­‐-­‐pdb-­‐failures
  • 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"
  • 38.
  • 39.
    SQLite для быстрых тестов Если ваш проект позволяет
  • 40.
    Параллелим тесты Нетрудоёмкое ускоение
  • 41.
    Ran  337  tests  in  326.664s OK  (SKIP=2) 1 процесс 326 2 процесса Секунды 169 3 процесса 126 0 100 200 300 400 $  ./manage.py  -­‐-­‐processes=N
  • 42.