ݺߣ

ݺߣShare a Scribd company logo
Разработка через
  тестирование
в Python и Django
             Илья Шаляпин
         Евгений Генералов
19 проектов
    4 года
89299 строк кода
50826 строк тестов
Писать тесты или нет?
Пример из жизни
Переезд с Ubuntu 8.04 на Ubuntu 12.04

Python 2.5                 Python 2.7
Django 1.3                 Django 1.4.0
lxml 1.3.6                 lxml 2.3.2
PIL 1.1.6                  PIL 1.1.7
...                        ...
Перезд проекта плотно
  покрытого тестами
Перезд проекта менее плотно
    покрытого тестами
Перезд проекта без тестов
Преимущества


- Меньше ручной работы
- Спокойный рефакторинг
- Код легче читать
- Быстрое подключение людей к проекту
- Тесты являются спецификацией
Недостатки


- Затраты на обучение
- Дополнительные настроки в проекте
- Некоторые тесты сложно писать
TDD вид сбоку
$ pip install unittest2
# test_add.py

import unittest2


class AddTest(unittest2.TestCase):

  def test_add(self):
    self.assertEquals(add(1, 1), 2)
    self.assertEquals(add(5, 2), 7)
    self.assertEquals(add(-1, -6), -7)


if __name__ == '__main__':
    unittest2.main()
# test_add.py

import unittest2

def add(a, b):
   pass


class AddTest(unittest2.TestCase):

  def test_add(self):
    self.assertEquals(add(1, 1), 2)


if __name__ == '__main__':
    unittest2.main()
Запуск теста




$ python test_add.py
$ python test_add.py
F
=========================================
FAIL: test_add (__main__.AddTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_add.py", line 11, in test_add
   self.assertEquals(add(1, 1), 2)
AssertionError: None != 2

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
# test_add.py

import unittest2

def add(a, b):
  return a + b


class AddTest(unittest2.TestCase):

  def test_add(self):
    self.assertEquals(add(1, 1), 2)


if __name__ == '__main__':
    unittest2.main()
$ python test_add.py
.
-------------------------------------------------
Ran 1 test in 0.000s

OK
Проект растет - тестов
  становится много

 ...
 ./tests/
 ./tests/test_add.py
 ./tests/test_sub.py
 ./tests/test_div.py
 ./tests/test_mul.py
 ./tests/test_pi.py
Nose - запускалка тестов
            Устанавливаем nose
$ pip install nose


               Запускаем тесты
$ nosetests
..
--------------------------------------------
Ran 100500 tests in 0.219s

OK
Инструменты




unittest2       django.test
flexmock        django_nose
nose            django_webtest
Тестирование в Django

Установить приложения
   $ pip install django_nose
   $ pip install django_webtest


Создать тестовую конфигурацию
    testing_settings.py
# testing_settings.py
from settings import *

DATABASES = {
  "default": dict(
    ENGINE = "django.db.backends.sqlite3",
    NAME = ":memory:",
  )
}

INSTALLED_APPS += (
  'django_nose',
)

TEST_RUNNER = 'django_nose.
NoseTestSuiteRunner'
Запуск тестов в Django

Запуск всех тестов в папке ./blog

$ manage.py test ./blog --settings project.
testing_settings



Запуск тестов в одном файле

$ manage.py test ./blog/test/test_forms.py --settings
project.testing_settings
Запуск тестов только для одного класса

$ manage.py test ./blog/test/test_forms.py:PostFormTest
--settings project.testing_settings



Запуск только одного теста

$ manage.py test ./blog/test/test_forms.py:PostFormTest.
test_post_from_submit --settings project.testing_settings
Blog tutorial
Тест view

from django.test import TestCase, Client


class HomePageTest(TestCase):

  def test_homepage_is_available(self):
    c = Client()
    response = c.get('/')
    self.assertEquals(response.status_code, 200)
class HomePageTest(TestCase):

  def setUp(self):
    self.posts = [ ]
    for i in range(20):
           post = Post.objects.create(
                 title = "Hello %d" % i,
           )
           self.posts.append(post)

  def test_homepage_contains_posts(self):
    pass
class HomePageTest(TestCase):

  def setUp(self):
    self.posts = [ ]
    for i in range(20):
           post = Post.objects.create(
                 title = "Hello %d" % i,
           )
           self.posts.append(post)

  def test_homepage_contains_posts(self):
    c = Client()
    response = c.get('/')
    self.assertEquals(response.status_code, 200)
    self.assertIn(self.posts[-1].title, response.content)
    self.assertIn(self.posts[-2].title, response.content)
class HomePageTest(TestCase):

  def setUp(self):
    pass

  def tearDown(self):
    pass

  def test_homepage_contains_posts(self):
    pass
def home(request):
  posts = Post.objects.all()[:10]
  return render(request, 'home.html', {'posts':posts})
from django.db import models


class Post(models.Model):
   picture = models.ImageField(
       upload_to='posts', blank=True, null=True)
   title = models.CharField(max_length=255)
   body = models.CharField(max_length=255)

  class Meta:
     ordering = ['-id']
Отправка формы

class PostFormTest(TestCase):

  def test_post_from_submit(self):
    c = Client()
    params = {'title':'Hello Pycon'}
    response = c.post('/posts/add/', params)
    self.assertEquals(response.status_code, 302)
    post = Post.objects.get(title=params['title'])
Загрузка файлов

def test_post_from_submit_with_picture(self):
  f = open('blog/tests/fixtures/debian-logo.png')
  params = {
      'picture':f,
      'title':'My photo',
  }
  response = self.client.post('/posts/add/', params)
  self.assertEquals(response.status_code, 302)
  post = Post.objects.get(title=params['title'])
  self.assertIn('.png', post.picture.path)
$ pip install django_webtest
django_webtest - XPath
class HomePageWebTest(WebTest):

  def setUp(self):
    ...

  def test_homepage_contains_posts(self):
    response = self.app.get('/')
    self.assertEquals(response.status_int, 200)
    titles = response.lxml.xpath(
         "//*[@class='post-announce']/h2/text()"
    )
    self.assertEquals(titles[0], self.posts[-1].title)
    self.assertEquals(titles[1], self.posts[-2].title)
django_webtest - формы

from django_webtest import WebTest

class PostFormWebTest(WebTest):

  def test_post_from_submit(self):
    response = self.app.get('/posts/add/')
    self.assertEquals(response.status_int, 200)
    form = response.forms['add_post_form']
    form['title'] = 'Hello Pycon'
    form['body'] = 'Wazzup!'
    response = form.submit().follow()
    self.assertEquals(response.status_int, 200)
Тесты админки




Почти такие же как тесты других view
class PostAdminTest(TestCase):

  def setUp(self):
    self.user = User.objects.create_user(
       'admin',
       'mail@example.com',
       'password'
    )
    self.user.is_staff = True
    self.user.is_superuser = True
    self.user.save()

  def test_post_form_submit(self):
    ...
class PostAdminTest(TestCase):

  def setUp(self):
    ...

  def test_post_form_submit(self):
    c = Client()
    c.login(username='admin', password='password')
    response = c.get('/admin/blog/post/add/')
    self.assertEquals(response.status_code, 200)
    params = {'title': 'Hello Pycon', 'body': 'Text'}
    response = c.post('/posts/add/', params)
    self.assertEquals(response.status_code, 302)
    post = Post.objects.get(title=params['title'])
Прочее в Django

- Middleware
- Template tags, filters
- Context processors


- тестируются модульными тестами как
простые функции, аналогично с
примером 1+1 = 2
Особенности тестов view в Django

     ----------------------------
     middleware
     -----------------------------
     context processors
     -----------------------------
     template
     -----------------------------
     view
     -----------------------------
     models
     -----------------------------
     network
Flexmock

- Заменять части объектов и классов
- Заменять функции, в том числе
встроенные
- Создавать объекты заглушки
- Проверять ожидания (сколько раз
вызван метод, с какими аргументами)
$ pip install flexmock
from flexmock import flexmock
from blog.models import Post

def test_home_page_with_flexmock(self):
  posts = [
     Post(title='hello flexmock'),
     Post(title='hello flexmock'),
  ]
  (flexmock(Post.objects)
     .should_receive('all')
     .and_return(posts)
     .once())
  response = self.client.get('/')
  self.assertEquals(response.status_code, 200)
  self.assertIn('hello flexmock', response.content)
from flexmock import flexmock
import blog.views

def test_home_view_as_unittest(self):
  request = flexmock(
     GET={},
     POST={},
     META={'HTTP_HOST':'example.com'}
  )
  response = blog.views.home(request)
  self.assertEquals(response.status_code, 200)
Теория vs практика
Есть требования ...

def get_url_content(url):
  # ToDo
  # Вернуть контент страницы
  # или None, в случае ошибки
  pass
Как написать тест?



def test_get_url_content(self):
  url = 'http://example.com'
  text = get_url_content(url)
  self.assertEquals(text, ???)
Тестирование реализации
Пишем тест имея представление о внутренностях

def get_url_content(url):
  try:
     response = urllib.urlopen(url)
     content = response.read()
     response.close()
  except IOError:
     return None
  return content


Неверно с точки зрения теории,
удобно на практике
Тест для случая нормального
        выполнения
 def test_get_url_content(self):
   url = 'http://example.com'
   response = StringIO("<html>")
   (flexmock(urllib)
      .should_receive('urlopen')
      .with_args(url)
      .and_return(response)
      .once())
   text = get_url_content(url)
   self.assertEquals(text, "<html>")
Тест в случае ошибки сети

def test_get_url_content_on_ioerror(self):
  url = 'http://example.com'
  (flexmock(urllib)
     .should_receive('urlopen')
     .with_args(url)
     .and_raise(IOError("test exception"))
     .once())
  text = get_url_content(url)
  self.assertEquals(text, None)
Примеры тестов



https://bitbucket.org/ishalyapin/python-test-examples

https://bitbucket.org/ishalyapin/django-test-examples
Спасибо за внимание!
  Доклад подготовили

 Илья Шаляпин
 ishalyapin@gmail.com
 www.ishalyapin.ru
 www.bookradar.org
 bitbucket.org/ishalyapin
 github.com/un1t



 Евгений Генералов
 e.generalov@gmail.com
 github.com/generalov

More Related Content

Pycon Russia 2013 - Разработка через тестирование в Python и Django

  • 1. Разработка через тестирование в Python и Django Илья Шаляпин Евгений Генералов
  • 2. 19 проектов 4 года 89299 строк кода 50826 строк тестов
  • 4. Пример из жизни Переезд с Ubuntu 8.04 на Ubuntu 12.04 Python 2.5 Python 2.7 Django 1.3 Django 1.4.0 lxml 1.3.6 lxml 2.3.2 PIL 1.1.6 PIL 1.1.7 ... ...
  • 5. Перезд проекта плотно покрытого тестами
  • 6. Перезд проекта менее плотно покрытого тестами
  • 8. Преимущества - Меньше ручной работы - Спокойный рефакторинг - Код легче читать - Быстрое подключение людей к проекту - Тесты являются спецификацией
  • 9. Недостатки - Затраты на обучение - Дополнительные настроки в проекте - Некоторые тесты сложно писать
  • 11. $ pip install unittest2
  • 12. # test_add.py import unittest2 class AddTest(unittest2.TestCase): def test_add(self): self.assertEquals(add(1, 1), 2) self.assertEquals(add(5, 2), 7) self.assertEquals(add(-1, -6), -7) if __name__ == '__main__': unittest2.main()
  • 13. # test_add.py import unittest2 def add(a, b): pass class AddTest(unittest2.TestCase): def test_add(self): self.assertEquals(add(1, 1), 2) if __name__ == '__main__': unittest2.main()
  • 15. $ python test_add.py F ========================================= FAIL: test_add (__main__.AddTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_add.py", line 11, in test_add self.assertEquals(add(1, 1), 2) AssertionError: None != 2 ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
  • 16. # test_add.py import unittest2 def add(a, b): return a + b class AddTest(unittest2.TestCase): def test_add(self): self.assertEquals(add(1, 1), 2) if __name__ == '__main__': unittest2.main()
  • 18. Проект растет - тестов становится много ... ./tests/ ./tests/test_add.py ./tests/test_sub.py ./tests/test_div.py ./tests/test_mul.py ./tests/test_pi.py
  • 19. Nose - запускалка тестов Устанавливаем nose $ pip install nose Запускаем тесты $ nosetests .. -------------------------------------------- Ran 100500 tests in 0.219s OK
  • 20. Инструменты unittest2 django.test flexmock django_nose nose django_webtest
  • 21. Тестирование в Django Установить приложения $ pip install django_nose $ pip install django_webtest Создать тестовую конфигурацию testing_settings.py
  • 22. # testing_settings.py from settings import * DATABASES = { "default": dict( ENGINE = "django.db.backends.sqlite3", NAME = ":memory:", ) } INSTALLED_APPS += ( 'django_nose', ) TEST_RUNNER = 'django_nose. NoseTestSuiteRunner'
  • 23. Запуск тестов в Django Запуск всех тестов в папке ./blog $ manage.py test ./blog --settings project. testing_settings Запуск тестов в одном файле $ manage.py test ./blog/test/test_forms.py --settings project.testing_settings
  • 24. Запуск тестов только для одного класса $ manage.py test ./blog/test/test_forms.py:PostFormTest --settings project.testing_settings Запуск только одного теста $ manage.py test ./blog/test/test_forms.py:PostFormTest. test_post_from_submit --settings project.testing_settings
  • 26. Тест view from django.test import TestCase, Client class HomePageTest(TestCase): def test_homepage_is_available(self): c = Client() response = c.get('/') self.assertEquals(response.status_code, 200)
  • 27. class HomePageTest(TestCase): def setUp(self): self.posts = [ ] for i in range(20): post = Post.objects.create( title = "Hello %d" % i, ) self.posts.append(post) def test_homepage_contains_posts(self): pass
  • 28. class HomePageTest(TestCase): def setUp(self): self.posts = [ ] for i in range(20): post = Post.objects.create( title = "Hello %d" % i, ) self.posts.append(post) def test_homepage_contains_posts(self): c = Client() response = c.get('/') self.assertEquals(response.status_code, 200) self.assertIn(self.posts[-1].title, response.content) self.assertIn(self.posts[-2].title, response.content)
  • 29. class HomePageTest(TestCase): def setUp(self): pass def tearDown(self): pass def test_homepage_contains_posts(self): pass
  • 30. def home(request): posts = Post.objects.all()[:10] return render(request, 'home.html', {'posts':posts})
  • 31. from django.db import models class Post(models.Model): picture = models.ImageField( upload_to='posts', blank=True, null=True) title = models.CharField(max_length=255) body = models.CharField(max_length=255) class Meta: ordering = ['-id']
  • 32. Отправка формы class PostFormTest(TestCase): def test_post_from_submit(self): c = Client() params = {'title':'Hello Pycon'} response = c.post('/posts/add/', params) self.assertEquals(response.status_code, 302) post = Post.objects.get(title=params['title'])
  • 33. Загрузка файлов def test_post_from_submit_with_picture(self): f = open('blog/tests/fixtures/debian-logo.png') params = { 'picture':f, 'title':'My photo', } response = self.client.post('/posts/add/', params) self.assertEquals(response.status_code, 302) post = Post.objects.get(title=params['title']) self.assertIn('.png', post.picture.path)
  • 34. $ pip install django_webtest
  • 35. django_webtest - XPath class HomePageWebTest(WebTest): def setUp(self): ... def test_homepage_contains_posts(self): response = self.app.get('/') self.assertEquals(response.status_int, 200) titles = response.lxml.xpath( "//*[@class='post-announce']/h2/text()" ) self.assertEquals(titles[0], self.posts[-1].title) self.assertEquals(titles[1], self.posts[-2].title)
  • 36. django_webtest - формы from django_webtest import WebTest class PostFormWebTest(WebTest): def test_post_from_submit(self): response = self.app.get('/posts/add/') self.assertEquals(response.status_int, 200) form = response.forms['add_post_form'] form['title'] = 'Hello Pycon' form['body'] = 'Wazzup!' response = form.submit().follow() self.assertEquals(response.status_int, 200)
  • 37. Тесты админки Почти такие же как тесты других view
  • 38. class PostAdminTest(TestCase): def setUp(self): self.user = User.objects.create_user( 'admin', 'mail@example.com', 'password' ) self.user.is_staff = True self.user.is_superuser = True self.user.save() def test_post_form_submit(self): ...
  • 39. class PostAdminTest(TestCase): def setUp(self): ... def test_post_form_submit(self): c = Client() c.login(username='admin', password='password') response = c.get('/admin/blog/post/add/') self.assertEquals(response.status_code, 200) params = {'title': 'Hello Pycon', 'body': 'Text'} response = c.post('/posts/add/', params) self.assertEquals(response.status_code, 302) post = Post.objects.get(title=params['title'])
  • 40. Прочее в Django - Middleware - Template tags, filters - Context processors - тестируются модульными тестами как простые функции, аналогично с примером 1+1 = 2
  • 41. Особенности тестов view в Django ---------------------------- middleware ----------------------------- context processors ----------------------------- template ----------------------------- view ----------------------------- models ----------------------------- network
  • 42. Flexmock - Заменять части объектов и классов - Заменять функции, в том числе встроенные - Создавать объекты заглушки - Проверять ожидания (сколько раз вызван метод, с какими аргументами)
  • 43. $ pip install flexmock
  • 44. from flexmock import flexmock from blog.models import Post def test_home_page_with_flexmock(self): posts = [ Post(title='hello flexmock'), Post(title='hello flexmock'), ] (flexmock(Post.objects) .should_receive('all') .and_return(posts) .once()) response = self.client.get('/') self.assertEquals(response.status_code, 200) self.assertIn('hello flexmock', response.content)
  • 45. from flexmock import flexmock import blog.views def test_home_view_as_unittest(self): request = flexmock( GET={}, POST={}, META={'HTTP_HOST':'example.com'} ) response = blog.views.home(request) self.assertEquals(response.status_code, 200)
  • 47. Есть требования ... def get_url_content(url): # ToDo # Вернуть контент страницы # или None, в случае ошибки pass
  • 48. Как написать тест? def test_get_url_content(self): url = 'http://example.com' text = get_url_content(url) self.assertEquals(text, ???)
  • 49. Тестирование реализации Пишем тест имея представление о внутренностях def get_url_content(url): try: response = urllib.urlopen(url) content = response.read() response.close() except IOError: return None return content Неверно с точки зрения теории, удобно на практике
  • 50. Тест для случая нормального выполнения def test_get_url_content(self): url = 'http://example.com' response = StringIO("<html>") (flexmock(urllib) .should_receive('urlopen') .with_args(url) .and_return(response) .once()) text = get_url_content(url) self.assertEquals(text, "<html>")
  • 51. Тест в случае ошибки сети def test_get_url_content_on_ioerror(self): url = 'http://example.com' (flexmock(urllib) .should_receive('urlopen') .with_args(url) .and_raise(IOError("test exception")) .once()) text = get_url_content(url) self.assertEquals(text, None)
  • 53. Спасибо за внимание! Доклад подготовили Илья Шаляпин ishalyapin@gmail.com www.ishalyapin.ru www.bookradar.org bitbucket.org/ishalyapin github.com/un1t Евгений Генералов e.generalov@gmail.com github.com/generalov