pycon russia 2013 - Разработка через тестирование в python и django

53
Разработка через тестирование в Python и Django Илья Шаляпин Евгений Генералов

Upload: ilya-shalyapin

Post on 19-Jul-2015

259 views

Category:

Technology


4 download

TRANSCRIPT

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

Разработка через тестирование

в Python и DjangoИлья Шаляпин

Евгений Генералов

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

проектов

года

строк кода

строк тестов

194

8929950826

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

Писать тесты или нет?

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

Пример из жизни

Переезд с 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Перезд проекта плотно покрытого тестами

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

Перезд проекта менее плотно покрытого тестами

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

Перезд проекта без тестов

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

Преимущества

- Меньше ручной работы

- Спокойный рефакторинг

- Код легче читать

- Быстрое подключение людей к проекту

- Тесты являются спецификацией

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

Недостатки

- Затраты на обучение

- Дополнительные настроки в проекте

- Некоторые тесты сложно писать

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

TDD вид сбоку

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

$ pip install unittest2

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

# 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

# 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

$ python test_add.py

Запуск теста

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

$ 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

# 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

$ python test_add.py .-------------------------------------------------Ran 1 test in 0.000s

OK

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

...

./tests/

./tests/test_add.py

./tests/test_sub.py

./tests/test_div.py

./tests/test_mul.py

./tests/test_pi.py

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

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

$ nosetests..--------------------------------------------Ran 100500 tests in 0.219s

OK

$ pip install nose

Nose - запускалка тестовУстанавливаем nose

Запускаем тесты

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

Инструменты

unittest2 flexmock nose

django.test django_nose django_webtest

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

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

$ pip install django_nose$ pip install django_webtest

Создать тестовую конфигурацию

Установить приложения

testing_settings.py

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

# 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Запуск тестов в Django

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

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

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

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

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

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

$ 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Blog tutorial

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

Тест 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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

class HomePageTest(TestCase):

def setUp(self): pass

def tearDown(self): pass

def test_homepage_contains_posts(self): pass

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

def home(request): posts = Post.objects.all()[:10] return render(request, 'home.html', {'posts':posts})

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

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Отправка формы

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

$ pip install django_webtest

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

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Тесты админки

Почти такие же как тесты других view

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

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Прочее в Django

- Middleware- Template tags, filters- Context processors

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

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

Особенности тестов view в Django

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

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

Flexmock

- Заменять части объектов и классов

- Заменять функции, в том числе

встроенные

- Создавать объекты заглушки

- Проверять ожидания (сколько раз

вызван метод, с какими аргументами)

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

$ pip install flexmock

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

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Теория vs практика

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

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

Есть требования ...

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

def test_get_url_content(self): url = 'http://example.com' text = get_url_content(url) self.assertEquals(text, ???)

Как написать тест?

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

Тестирование реализации

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

Неверно с точки зрения теории, удобно на практике

Пишем тест имея представление о внутренностях

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

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

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: Pycon Russia 2013 - Разработка через тестирование в Python и Django

Примеры тестов

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

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

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

Доклад подготовили

Илья Шаляпин

Евгений Генералов[email protected]/generalov

ishalyapin@gmail.comwww.ishalyapin.ruwww.bookradar.orgbitbucket.org/ishalyapingithub.com/un1t

Спасибо за внимание!