Разработка через тестирование в python и django #pyconru
DESCRIPTION
Презентация доклада Ильи Шаляпина и Евгения Генералова в первого Pycon'а в России.TRANSCRIPT
![Page 1: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/1.jpg)
Разработка через тестирование
в Python и DjangoИлья Шаляпин
Евгений Генералов
![Page 2: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/2.jpg)
проектов
года
строк кода
строк тестов
194
8929950826
![Page 3: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/3.jpg)
Писать тесты или нет?
![Page 4: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/4.jpg)
Пример из жизни
Переезд с Ubuntu 8.04 на Ubuntu 12.04
Python 2.5 Django 1.3lxml 1.3.6PIL 1.1.6...
Python 2.7 Django 1.4.0lxml 2.3.2PIL 1.1.7...
![Page 5: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/5.jpg)
Перезд проекта плотно покрытого тестами
![Page 6: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/6.jpg)
Перезд проекта менее плотно покрытого тестами
![Page 7: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/7.jpg)
Перезд проекта без тестов
![Page 8: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/8.jpg)
Преимущества
- Меньше ручной работы
- Спокойный рефакторинг
- Код легче читать
- Быстрое подключение людей к проекту
- Тесты являются спецификацией
![Page 9: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/9.jpg)
Недостатки
- Затраты на обучение
- Дополнительные настроки в проекте
- Некоторые тесты сложно писать
![Page 10: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/10.jpg)
TDD вид сбоку
![Page 11: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/11.jpg)
$ pip install unittest2
![Page 12: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/12.jpg)
# 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()
![Page 13: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/13.jpg)
# 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()
![Page 14: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/14.jpg)
$ python test_add.py
Запуск теста
![Page 15: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/15.jpg)
$ 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)
![Page 16: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/16.jpg)
# 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()
![Page 17: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/17.jpg)
$ python test_add.py .-------------------------------------------------Ran 1 test in 0.000s
OK
![Page 18: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/18.jpg)
...
./tests/
./tests/test_add.py
./tests/test_sub.py
./tests/test_div.py
./tests/test_mul.py
./tests/test_pi.py
Проект растет - тестов становится много
![Page 19: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/19.jpg)
$ nosetests..--------------------------------------------Ran 100500 tests in 0.219s
OK
$ pip install nose
Nose - запускалка тестовУстанавливаем nose
Запускаем тесты
![Page 20: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/20.jpg)
Инструменты
unittest2 flexmock nose
django.test django_nose django_webtest
![Page 21: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/21.jpg)
Тестирование в Django
$ pip install django_nose$ pip install django_webtest
Создать тестовую конфигурацию
Установить приложения
testing_settings.py
![Page 22: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/22.jpg)
# testing_settings.pyfrom settings import *
DATABASES = { "default": dict( ENGINE = "django.db.backends.sqlite3", NAME = ":memory:", )}
INSTALLED_APPS += ( 'django_nose',)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
![Page 23: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/23.jpg)
Запуск тестов в Django
Запуск всех тестов в папке ./blog
$ manage.py test ./blog --settings project.testing_settings
Запуск тестов в одном файле
$ manage.py test ./blog/test/test_forms.py --settings project.testing_settings
![Page 24: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/24.jpg)
Запуск тестов только для одного класса
$ 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
![Page 25: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/25.jpg)
Blog tutorial
![Page 26: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/26.jpg)
Тест 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)
![Page 27: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/27.jpg)
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
![Page 28: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/28.jpg)
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)
![Page 29: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/29.jpg)
class HomePageTest(TestCase):
def setUp(self): pass
def tearDown(self): pass
def test_homepage_contains_posts(self): pass
![Page 30: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/30.jpg)
def home(request): posts = Post.objects.all()[:10] return render(request, 'home.html', {'posts':posts})
![Page 31: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/31.jpg)
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']
![Page 32: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/32.jpg)
Отправка формы
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'])
![Page 33: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/33.jpg)
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)
Загрузка файлов
![Page 34: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/34.jpg)
$ pip install django_webtest
![Page 35: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/35.jpg)
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 - XPath
![Page 36: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/36.jpg)
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)
django_webtest - формы
![Page 37: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/37.jpg)
Тесты админки
Почти такие же как тесты других view
![Page 38: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/38.jpg)
class PostAdminTest(TestCase):
def setUp(self): self.user = User.objects.create_user( 'admin', '[email protected]', 'password' ) self.user.is_staff = True self.user.is_superuser = True self.user.save()
def test_post_form_submit(self): ...
![Page 39: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/39.jpg)
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'])
![Page 40: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/40.jpg)
Прочее в Django
- Middleware- Template tags, filters- Context processors
- тестируются модульными тестами как простые функции, аналогично с примером 1+1 = 2
![Page 41: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/41.jpg)
Особенности тестов view в Django
----------------------------middleware-----------------------------context processors-----------------------------template-----------------------------view-----------------------------models-----------------------------network
![Page 42: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/42.jpg)
Flexmock
- Заменять части объектов и классов
- Заменять функции, в том числе
встроенные
- Создавать объекты заглушки
- Проверять ожидания (сколько раз
вызван метод, с какими аргументами)
![Page 43: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/43.jpg)
$ pip install flexmock
![Page 44: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/44.jpg)
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)
![Page 45: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/45.jpg)
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)
![Page 46: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/46.jpg)
Теория vs практика
![Page 47: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/47.jpg)
def get_url_content(url): # ToDo # Вернуть контент страницы # или None, в случае ошибки pass
Есть требования ...
![Page 48: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/48.jpg)
def test_get_url_content(self): url = 'http://example.com' text = get_url_content(url) self.assertEquals(text, ???)
Как написать тест?
![Page 49: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/49.jpg)
Тестирование реализации
def get_url_content(url): try: response = urllib.urlopen(url) content = response.read() response.close() except IOError: return None return content
Неверно с точки зрения теории, удобно на практике
Пишем тест имея представление о внутренностях
![Page 50: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/50.jpg)
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>")
Тест для случая нормального выполнения
![Page 51: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/51.jpg)
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)
Тест в случае ошибки сети
![Page 52: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/52.jpg)
Примеры тестов
https://bitbucket.org/ishalyapin/python-test-examples
https://bitbucket.org/ishalyapin/django-test-examples
![Page 53: Разработка через тестирование в Python и Django #pyconru](https://reader033.vdocuments.site/reader033/viewer/2022050804/547b2232b379593f2b8b4cd9/html5/thumbnails/53.jpg)
Доклад подготовили
Илья Шаляпин
Евгений Генералов[email protected]/generalov
ishalyapin@gmail.comwww.ishalyapin.ruwww.bookradar.orgbitbucket.org/ishalyapingithub.com/un1t
Спасибо за внимание!