TDD
или как я стараюсь писать код
Владимир Филонов
labbler.com
Теория TDD
“Разработка через тестирование (англ. test-driven
development, TDD) — техника разработки
программного обеспечения, которая основывается
на повторении очень коротких циклов разработки:
сначала пишется тест, покрывающий желаемое
изменение, затем пишется код, который позволит
пройти тест, и под конец проводится рефакторинг
нового кода к соответствующим стандартам.” ©
Wikipedia.org
Тесты
Код
Рефакто-
ринг
Зачем все это?
Говорят, что TDD помогает
•  Улучшить качество кода
•  Уменьшить количество ошибок и багов
•  Ускорить разработку
В чем сила, брат?
В чем разница?
Тесты
КодРефакто-
ринг
Код
Рефакто-
рингТесты
IMHO
Вектор мышления
Сначала код:
•  Разработчик концентрируется на отдельных частях
кода больше чем на общем дизайне
•  К моменту написания тестов разработчик уже
устает
•  Тесты пишутся уже с учетом особенностей
реализации, в том числе и костылей, если таковые
присутствуют
class OneFieldForm(forms.Form):
value = forms.IntegerField()
class SimpleView(View):
def get(self, *args, **kwargs):
form = OneFieldForm()
return render_template(form)
def post(self, *args, **kwargs):
form = OneFieldForm(self.request.POST)
if form.is_valid():
#Какой-то сложный, утомительный код, который
#использует значение из формы
return render_to_response(“success.html")
else:
return render_template(form)
def render_template(self, form):
context = { "form": form,}
return render_to_response("template.html", context)
class SimpleViewTestCase(TestCase):
def test_simple_view(self):
response = self.client.get(”/”)
self.assertEqual(response.tempates[0].name, “template.html ")
response2 = self.client.post(”/”)
self.assertEqual(response2.tempates[0].name, “template.html")
response3 = self.client.post(”/”, {“count”: 1})
self.assertEqual(response3.tempates[0].name, “success.html")
.
Ситуации
class OneFieldForm(forms.Form):
value = forms.CharField()
if “value” in self.request.POST and 
self.request.POST[“value”]:
TDD
•  При написании тестов, мы не отвлекаемся на
детали реализации
•  Более чёткое и целостное представление о
дизайне кода
•  Выше скорость написания кода
•  Хорошего кода ;)
•  Меньше рефакторинга
Смена исполнителя
•  Проще понять, что уже сделано, а что еще нет –
достаточно запустить тесты
•  Тесты как документация – проще разобраться в уже
написанном коде
•  Более отчуждаемый код
Человечные интерфейсы
•  Сразу смотрим на задачу с позиции пользователя
интерфейсов
•  Не делаем допущений, основанных на знании
деталей реализации
•  Ниже порог вхождения, меньше подводных камней
•  Более отчуждаемый код
Мотивация
Обычное течение разработки
•  Начало. Уровень мотивации высокий, все рвутся в
бой
•  Понеслась
– Имплементация
– Передача в QA
– Баги
– Дебагинг
– Мотивация = - 1* Баги
TDD
•  Тесты пишутся на первой волне энтузиазма
•  Для написания кода после тестов появляются
дополнительные стимулы:
– Четко поставленная цель: пройти все тесты
– Каждый пройденный тест – достижение
– Это поддерживает положительный настрой
•  Стимулы для написания тестов после кода
– Требуется очень хорошая самоорганизация
– Формальных требований может быть недостаточно для
написания хороших тестов
Acceptance TDD
•  Пишем приёмочные тесты и ставим задачи
команде
•  Приёмочные тесты – тесты верхнего уровня
абстракции
•  Сами пишем тесты на невалидные входные данные
•  Кто, кроме нас? :)
•  Сам я ещё не пробовал, но очень хочу
Как писать тесты до
кода?
•  От абстракций верхнего уровня - к абстракциям нижних
–  Глобальные вещи (views) -> API, DAO -> утилитарные методы
•  Проще показать на примере
Сделаем виртуальную библиотеку.
Книга - название, автор, ISBN
Читатель
Чтобы получить билет, надо зарегистрироваться. Для простоты, в
качестве номера будем использовать django.contrib.auth.models.User.id
Читатели могут брать/сдавать книги, но не более 2х одновременно.
Если книгу кто-то взял, то другому ее не получить.
Брать книги можно по:
•  1. Название+Автор
•  2. ISBN
#Книги: title, author, year, isbn
#
#Капитал, Кащей Б.С., 942 г., 1234-5678-9
#100 диетических блюд из репы, Прекрасная В., 1142 г.,
# 4321-8765-9
#Ковка подков, Гефест, 2675 г. до н.э. 9876-5432-1
#Бустâн, Абу Мухаммад Муслих ад-Дин ибн Абд Аллах Саади
# Ширази, 1257 г., 6789-2345-1
class LibraryViewsTestCase(TestCase):
def setUp(self):
self.ivan = User.objects.get(username="ivan")
self.maria = User.objects.get(username="maria")
self.books = INITIAL_BOOKS_LIST
def test_library_urls(self):
self.assertEqual(reverse("library:take"), "/take/")
self.assertEqual(reverse("library:return"), "/return/")
def test_get_book_view__unauth(self):
"""
Книжки можно раздавать только авторизованным пользователям
"""
response = self.client.post("/take/", {"isbn": "1234-5678-9"})
self.assertEqual(response.status_code, 301)
self.client.login(username="ivan", password="durak")
response = self.client.post("/take/", {"isbn": "1234-5678-9"})
self.assertEqual(response.status_code, 200)
def test_get_book_view__isbn(self):
"""
Наберем книжек по ISBN
"""
self.client.login(username="ivan", password="pozhaluista")
#Возьмем книгу
response = self.client.post("/take/", {"isbn": "1234-5678-9"})
self.assertEqual(response.status_code, 200)
self.assertIn("book", response.context)
self.assertEqual(response.context["book"]["title"], u"Капитал")
self.assertEqual(response.templates[0].name, "take_success.html")
self.assertEqual(get_user_books_count(self.ivan), 1)
self.assertIn(self.books[0], get_user_books(self.ivan))
#Еще одну
self.client.post("/take/", {"isbn": "9876-5432-1"})
self.assertEqual(get_user_books_count(self.ivan), 2)
self.assertIn(self.books[0], get_user_books(self.ivan))
self.assertIn(self.books[2], get_user_books(self.ivan))
#Поробуем третью
failed_response = self.client.post("/take/", {"isbn": "6789-2345-1"})
self.assertIn("book", response.context)
self.assertEqual(response.context["book"]["title"], u"Бустâн")
self.assertEqual(response.templates[0].name, "take_failed.html")
self.assertIn("error", response.context)
self.assertEqual(response.context["error"], u"Хватит уже")
self.assertEqual(get_user_books_count(self.ivan), 2)
self.assertIn(self.books[0], get_user_books(self.ivan))
self.assertIn(self.books[2], get_user_books(self.ivan))
def test_get_book_view__title_and_author(self):
"""
Берем книги по заголовку и автору
"""
def test_get_book_view__taken(self):
"""
Попробуем взять книгу, которую уже унес кто-то
"""
def test_get_book_view__again(self):
"""
Попробуем взять одну и ту же книгу дважды
"""
def test_get_book_view__unknown(self):
"""
Попробуем взять книгу, которой нет в библиотеке
"""
#...
#Возврат книги, возврат не взятой книги,
#возврат книги не из библиотеки
Полученные тесты можно
использовать для ATDD
class LibriatyDaoTestCase(TestCase):
"""
Следующий уровень - функции, которые нам понадобятся,
чтобы решить ситуации, описанные выше
"""
def test_get_book_by_isbn(self):
"""
Получаем их базы книгу по isbn
"""
def test_get_book_by_title_and_author(self):
"""
Получаем из базы книгу по isbn
"""
def test_user_has_book(self):
"""
Есть ли эта книга у читателя
"""
def test_get_user_books(self):
"""
Все книги которые есть у читателя
"""
def test_get_user_books_count(self):
"""
Сколько книг у читателя
"""
def test_book_is_owned(self):
"""
Взяли ли кто-то эту книгу
"""
Та-дааа!
Эпилог
•  Более продуманный дизайн кода к моменту начала
реализации
•  Как следствие - более чистый код
•  Возможно, более быстрая реализация
•  Лучшее покрытие тестами (как по качеству, так и по
количеству)
•  Дополнительный источник мотивации в процессе
•  Более отчуждаемый код
•  Меньше багов, а значит и меньше итераций «QA-
багфиксинг»
Спасибо!
И…
Немного саморекламы =)
https://labbler.com
mailto: vladimir@labbler.com
Спасибо!

TDD или как я стараюсь писать код

  • 1.
    TDD или как ястараюсь писать код Владимир Филонов labbler.com
  • 2.
    Теория TDD “Разработка черезтестирование (англ. test-driven development, TDD) — техника разработки программного обеспечения, которая основывается на повторении очень коротких циклов разработки: сначала пишется тест, покрывающий желаемое изменение, затем пишется код, который позволит пройти тест, и под конец проводится рефакторинг нового кода к соответствующим стандартам.” © Wikipedia.org
  • 3.
  • 4.
    Зачем все это? Говорят,что TDD помогает •  Улучшить качество кода •  Уменьшить количество ошибок и багов •  Ускорить разработку
  • 5.
  • 6.
  • 7.
  • 8.
    Вектор мышления Сначала код: • Разработчик концентрируется на отдельных частях кода больше чем на общем дизайне •  К моменту написания тестов разработчик уже устает •  Тесты пишутся уже с учетом особенностей реализации, в том числе и костылей, если таковые присутствуют
  • 9.
    class OneFieldForm(forms.Form): value =forms.IntegerField() class SimpleView(View): def get(self, *args, **kwargs): form = OneFieldForm() return render_template(form) def post(self, *args, **kwargs): form = OneFieldForm(self.request.POST) if form.is_valid(): #Какой-то сложный, утомительный код, который #использует значение из формы return render_to_response(“success.html") else: return render_template(form) def render_template(self, form): context = { "form": form,} return render_to_response("template.html", context)
  • 10.
    class SimpleViewTestCase(TestCase): def test_simple_view(self): response= self.client.get(”/”) self.assertEqual(response.tempates[0].name, “template.html ") response2 = self.client.post(”/”) self.assertEqual(response2.tempates[0].name, “template.html") response3 = self.client.post(”/”, {“count”: 1}) self.assertEqual(response3.tempates[0].name, “success.html") .
  • 11.
    Ситуации class OneFieldForm(forms.Form): value =forms.CharField() if “value” in self.request.POST and self.request.POST[“value”]:
  • 12.
    TDD •  При написаниитестов, мы не отвлекаемся на детали реализации •  Более чёткое и целостное представление о дизайне кода •  Выше скорость написания кода •  Хорошего кода ;) •  Меньше рефакторинга
  • 13.
    Смена исполнителя •  Прощепонять, что уже сделано, а что еще нет – достаточно запустить тесты •  Тесты как документация – проще разобраться в уже написанном коде •  Более отчуждаемый код
  • 14.
    Человечные интерфейсы •  Сразусмотрим на задачу с позиции пользователя интерфейсов •  Не делаем допущений, основанных на знании деталей реализации •  Ниже порог вхождения, меньше подводных камней •  Более отчуждаемый код
  • 15.
  • 16.
    Обычное течение разработки • Начало. Уровень мотивации высокий, все рвутся в бой •  Понеслась – Имплементация – Передача в QA – Баги – Дебагинг – Мотивация = - 1* Баги
  • 17.
    TDD •  Тесты пишутсяна первой волне энтузиазма •  Для написания кода после тестов появляются дополнительные стимулы: – Четко поставленная цель: пройти все тесты – Каждый пройденный тест – достижение – Это поддерживает положительный настрой
  • 18.
    •  Стимулы длянаписания тестов после кода – Требуется очень хорошая самоорганизация – Формальных требований может быть недостаточно для написания хороших тестов
  • 19.
  • 20.
    •  Пишем приёмочныетесты и ставим задачи команде •  Приёмочные тесты – тесты верхнего уровня абстракции •  Сами пишем тесты на невалидные входные данные •  Кто, кроме нас? :) •  Сам я ещё не пробовал, но очень хочу
  • 21.
  • 22.
    •  От абстракцийверхнего уровня - к абстракциям нижних –  Глобальные вещи (views) -> API, DAO -> утилитарные методы •  Проще показать на примере Сделаем виртуальную библиотеку. Книга - название, автор, ISBN Читатель Чтобы получить билет, надо зарегистрироваться. Для простоты, в качестве номера будем использовать django.contrib.auth.models.User.id Читатели могут брать/сдавать книги, но не более 2х одновременно. Если книгу кто-то взял, то другому ее не получить. Брать книги можно по: •  1. Название+Автор •  2. ISBN
  • 23.
    #Книги: title, author,year, isbn # #Капитал, Кащей Б.С., 942 г., 1234-5678-9 #100 диетических блюд из репы, Прекрасная В., 1142 г., # 4321-8765-9 #Ковка подков, Гефест, 2675 г. до н.э. 9876-5432-1 #Бустâн, Абу Мухаммад Муслих ад-Дин ибн Абд Аллах Саади # Ширази, 1257 г., 6789-2345-1 class LibraryViewsTestCase(TestCase): def setUp(self): self.ivan = User.objects.get(username="ivan") self.maria = User.objects.get(username="maria") self.books = INITIAL_BOOKS_LIST def test_library_urls(self): self.assertEqual(reverse("library:take"), "/take/") self.assertEqual(reverse("library:return"), "/return/")
  • 24.
    def test_get_book_view__unauth(self): """ Книжки можнораздавать только авторизованным пользователям """ response = self.client.post("/take/", {"isbn": "1234-5678-9"}) self.assertEqual(response.status_code, 301) self.client.login(username="ivan", password="durak") response = self.client.post("/take/", {"isbn": "1234-5678-9"}) self.assertEqual(response.status_code, 200)
  • 25.
    def test_get_book_view__isbn(self): """ Наберем книжекпо ISBN """ self.client.login(username="ivan", password="pozhaluista") #Возьмем книгу response = self.client.post("/take/", {"isbn": "1234-5678-9"}) self.assertEqual(response.status_code, 200) self.assertIn("book", response.context) self.assertEqual(response.context["book"]["title"], u"Капитал") self.assertEqual(response.templates[0].name, "take_success.html") self.assertEqual(get_user_books_count(self.ivan), 1) self.assertIn(self.books[0], get_user_books(self.ivan)) #Еще одну self.client.post("/take/", {"isbn": "9876-5432-1"}) self.assertEqual(get_user_books_count(self.ivan), 2) self.assertIn(self.books[0], get_user_books(self.ivan)) self.assertIn(self.books[2], get_user_books(self.ivan))
  • 26.
    #Поробуем третью failed_response =self.client.post("/take/", {"isbn": "6789-2345-1"}) self.assertIn("book", response.context) self.assertEqual(response.context["book"]["title"], u"Бустâн") self.assertEqual(response.templates[0].name, "take_failed.html") self.assertIn("error", response.context) self.assertEqual(response.context["error"], u"Хватит уже") self.assertEqual(get_user_books_count(self.ivan), 2) self.assertIn(self.books[0], get_user_books(self.ivan)) self.assertIn(self.books[2], get_user_books(self.ivan))
  • 27.
    def test_get_book_view__title_and_author(self): """ Берем книгипо заголовку и автору """ def test_get_book_view__taken(self): """ Попробуем взять книгу, которую уже унес кто-то """ def test_get_book_view__again(self): """ Попробуем взять одну и ту же книгу дважды """ def test_get_book_view__unknown(self): """ Попробуем взять книгу, которой нет в библиотеке """ #... #Возврат книги, возврат не взятой книги, #возврат книги не из библиотеки
  • 28.
  • 29.
    class LibriatyDaoTestCase(TestCase): """ Следующий уровень- функции, которые нам понадобятся, чтобы решить ситуации, описанные выше """ def test_get_book_by_isbn(self): """ Получаем их базы книгу по isbn """ def test_get_book_by_title_and_author(self): """ Получаем из базы книгу по isbn """ def test_user_has_book(self): """ Есть ли эта книга у читателя """
  • 30.
    def test_get_user_books(self): """ Все книгикоторые есть у читателя """ def test_get_user_books_count(self): """ Сколько книг у читателя """ def test_book_is_owned(self): """ Взяли ли кто-то эту книгу """
  • 31.
  • 32.
  • 33.
    •  Более продуманныйдизайн кода к моменту начала реализации •  Как следствие - более чистый код •  Возможно, более быстрая реализация •  Лучшее покрытие тестами (как по качеству, так и по количеству) •  Дополнительный источник мотивации в процессе •  Более отчуждаемый код •  Меньше багов, а значит и меньше итераций «QA- багфиксинг»
  • 34.
  • 35.
  • 36.