Upload
jetstyle
View
3.058
Download
5
Embed Size (px)
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', '[email protected]', '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
Доклад подготовили
Илья Шаляпин
Евгений Генералов[email protected]/generalov
ishalyapin@gmail.comwww.ishalyapin.ruwww.bookradar.orgbitbucket.org/ishalyapingithub.com/un1t
Спасибо за внимание!