Разработка через тестирование в Python и Django #pyconru

Preview:

DESCRIPTION

Презентация доклада Ильи Шаляпина и Евгения Генералова в первого Pycon'а в России.

Citation preview

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

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

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

проектов

года

строк кода

строк тестов

194

8929950826

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

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

Переезд с 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...

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

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

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

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

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

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

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

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

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

Недостатки

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

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

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

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

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

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

OK

$ pip install nose

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

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

Инструменты

unittest2 flexmock nose

django.test django_nose django_webtest

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

$ pip install django_nose$ pip install django_webtest

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

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

testing_settings.py

# testing_settings.pyfrom 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

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

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 - формы

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

Почти такие же как тесты других 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

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

Илья Шаляпин

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

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

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

Recommended