2. Немного о себе
Меня зовут Сергей
Я разрабатываю backend у Злых Марсиан
Я люблю писать тесты
3. Сегодня мы:
Поговорим о том, зачем мы пишем тесты;
Уменьшим объем тестов без потери качества;
Уменьшим время написания и цену тестов;
Упростим поддержку.
4. Disclaimer
Спорить о тестах можно много и долго. Все сказанное
здесь - мое мнение, основанное на личном опыте
7. Почему нужно писать тесты?
Чтобы беречь свое время при разработке;
Факт от Капитана: автоматические тесты выполняются на
порядок быстрее ручных.
8. Почему нужно писать тесты?
Чтобы беречь свое время при разработке;
Чтобы не бояться что-то сломать;
Факт от Капитана: Все делают ошибки. Кто не делает
ошибок, тот нагло врет.
9. Почему нужно писать тесты?
Чтобы беречь свое время при разработке;
Чтобы не бояться что-то сломать;
Чтобы держать архитектуру приложения в форме.
(касается в основном unit-тестов)
10. Почему нужно писать тесты?
Тесты - это взгляд на код со стороны.
Код сложно тестировать?
⬇
Код сложен, запутан
⬇
Код нуждается в рефакторинге
11. Почему иногда мы не пишем
тесты?
Потому что часто тесты выглядят вот так:
15. Test coverage
Не дает никакого представления о том, насколько
хорошо протестирован код;
Показывает, какие места точно не протестированы, но
не наоборот;
Не дает никакого представления о качестве кода;
Не та метрика, за которой стоит гнаться.
16. Test coverage
100% не стоит вашего времени;
90% — это очень хорошее покрытие;
70% вполне достаточно.
17. Test coverage
“Мне платят за код, который работает, а не за
тесты, поэтому моя философия заключается в
том, чтобы тестировать настолько мало,
насколько это возможно для достижения
нужного уровня уверенности„
(Кент Бек)
19. Выбрасываем лишнее
Тесты некритичного кода
Если ошибка в коде не повлечет за собой серьезных
последствий, то тестирование этого участка кода совсем
не обязательно.
20. Выбрасываем лишнее
Тесты поведения сторонних библиотек
Большинство библиотек уже протестированы
разработчиком;
Если вас не устраивает, как они протестированы, лучше
сделать контрибьют, чем держать тесты у себя.
21. Выбрасываем лишнее
Косвенно выполненные проверки
Проверка на наличие классов и методов;
Проверка количества аргументов функций;
Проверка на отсутствие исключений.
Иногда такие проверки необходимы, но такие случаи
редки.
25. Приводим оставшееся в порядок
“Пишите код так, как будто сопровождать
его будет склонный к насилию психопат,
который знает, где вы живёте„
(Мартин Голдинг)
Верно и для тестов.
28. Используйте говорящие имена
тестов
Если фреймворк не позволяет в полной мере описать тест
с помощью имени, напишите комментарий
# Sends email to the user
def test_send_message
# ...
end
29. Один тест - одна проверка
По выводу тестового фреймворка должно быть понятно,
какие конкретно действия выполняются не так, как
ожидалось.
30. Один тест - одна проверка
Bad:
it 'creates message and sends it to the user via email' do
# ...
end
Good:
it 'creates message' do
# ...
end
it 'sends message to the user via email' do
# ...
end
31. Один тест - одна проверка
Аналогично для фреймворков без возможности подробно
описать тест
# Creates message
def test_send_message__message_creation
# ...
end
# Sends message to user
def test_send_message__message_sending
# ...
end
32. Один тест - одна проверка
Некоторые фреймворки позволяют писать комментарии к
проверкам. В таком случае разделение не так важно.
Пример для testify (go):
// Creates message
func Test_sendMessage(t *testing.T) {
// ...
assert.Equal(t, expected, actual,
"Should create message")
// ...
assert.Equal(t, expected, actual,
"Should send message to user via email")
// ...
}
34. Используйте контексты
Bad:
it 'creates message when user is signed in' do
sign_in(user)
# ...
end
Good:
context 'when user is signed in' do
before { sign_in(user) }
it 'creates message' do
# ...
end
end
35. Используйте контексты
Если фреймворк не поддерживает контексты, можно опять
обратиться к комментариям
# = When user is signed in ==============================
# Creates message
def test_send_message__signed_in__message_creation
# ...
end
# = end When user is signed in
37. Правильные ожидания
Примеры хороших ожиданий:
Возвращаемое значение;
Изменения состояния класса, видимого извне;
Внешнее воздействие a.k.a. side effect;
Обращение к сторонним объектам.
38. Правильные ожидания
Примеры плохих ожиданий:
Изменения внутренних переменных класса;
Изменение состояния хранилища, используемого для
хранения состояния тестируемого объекта;
Вызовы приватных методов.
39. Правильные ожидания
Bad:
it "puts provided value to redis" do
subject.set("the value")
expect(REDIS.get("the key")).to eq("the value")
end
Good:
it "saves provided value" do
subject.set("the value")
expect(subject.get).to eq("the value")
end
41. Mocks & stubs
Палка о двух концах:
Помогают достичь нужного уровня изоляции;
При злоупотреблении могут сделать тест бесполезным.
42. Mocks & stubs
Хорошие кандидаты:
Передаваемые на вход объекты;
Сетевые службы;
Функции с трудно прогнозируемым или трудно
выводимым результатом, используемые в тестируемом
методе.
43. Mocks & stubs
Нужно очень осторожно подходить к стабу БД.
Если не уверены на 100%, не делайте этого
44. Работа с внешними связями
Ситуация №1:
Функция foo объекта A (A.foo) проводит вычисления со
сложной логикой, основываясь на результатах функции
bar объекта B (B.bar);
B.bar в свою очередь тоже проводит вычисления со
сложной логикой.
Необходимо протестировать метод A.foo
45. Работа с внешними связями
Вариант решения №1:
Написать тест, учитывающий логику функции B.bar.
Нарушение DRY, повторное тестирование B.bar,
тестирование логики, не относящейся к тестируемому
методу
46. Работа с внешними связями
Вариант решения №2:
Создать условия для получения заранее известного
результата B.bar, использовать этот результат для
тестирования A.foo.
Тест становится зависимым от логики B.bar.
Изменение логики стороннего метода сломает тест.
47. Работа с внешними связями
Вариант решения №3:
Сделать stub B.bar с известным результатом,
использовать этот результат для тестирования A.foo.
Тест не зависит от логики B.bar
48. Работа с внешними связями
Проблема варианта №3: Изменение интерфейса
функции B.bar сломает код, но оставит тест ложно
положительным.
Решение: Это тот самый случай, когда чистый прогон
функции с проверкой на отсутствие исключений имеет
место быть.
49. Работа с внешними связями
Ситуация №2:
Метод foo объекта A (A.foo) проводит вычисления со
сложной логикой и затем вызывает метод bar объекта B
(B.bar);
B.bar в свою очередь тоже реализует сложную логику.
Необходимо протестировать метод A.foo
50. Работа с внешними связями
Вариант решения №1:
Проверить side effect метода B.bar, учитывая его логику.
Нарушение DRY, повторное тестирование B.bar,
тестирование логики, не относящейся к тестируемому
методу
51. Работа с внешними связями
Вариант решения №2:
Проверить некоторую неизменную часть side effect'а
метода B.bar, не зависящую от его логики. Пример: mailer
отправляет письмо с неизменным заголовком.
Тест не зависит от логики B.bar. Изменение
интерфейса B.bar будет обнаружено сразу.
52. Работа с внешними связями
Вариант решения №3:
Сделать stub B.bar, проверить факт его вызова после
выполнения A.foo.
Тест не зависит от логики B.bar
Имеет ту же проблему и аналогичное решение, что и
вариант решения №3 предыдущей ситуации.
55. Составьте договоренности
Если работаете в команде, составьте styleguide для тестов,
хотя бы на словах. Это существенно снизит порог
вхождения в чужие тесты.
57. Почему test first?
Если строить код на основе тестов, то у вас практически
не возникнет проблем с тестируемостью;
Хорошо организованные тесты позволяют продумать и
описать структуру кода до реализации.
58. Почему test first?
Главный аргумент
Не зная точной реализации, вы будете вынуждены
тестировать только интерфейс, что и требуется
59. Test first
Перед написанием тестов, постройте дерево с помощью
контекстов. Постарайтесь отобразить все возможные
варианты развития событий.
60. Итоги
Не гонитесь за test coverage;
Не будьте параноиком, определите для себя, что нужно
тестировать;
Описывайте тесты так, чтобы другой человек мог понять
тестируемый функционал;
Тестируйте интерфейс, а не реализацию;
Делайте unit-тесты независимыми от функционала
других классов/методов;
Используйте тесты как спецификацию для вашего кода;