Как стать автором
Обновить

Комментарии 19

Почему бы не замокать serInterval и clearInterval и не написать тесты на изначальный вариант? Такие тесты прямо бы документировали контракт createCounter и отпала бы необходимость иметь кучу файлов по одной функции и все эти сложности с DI.
С более сложным примером, где было бы больше веток и больше состояний, нас ждал бы Комбинаторный взрыв. Пришлось бы писать огромное количество тестов. И их количество было бы неустойчиво к рефакторингу. Добавил одну вилку в любую функцию — нужно удвоить общее количество тестов, что не есть хорошо.
С более сложным примером, где было бы больше веток и больше состояний, нас ждал бы Комбинаторный взрыв.


Кажется, что комбинаторный взрыв никуда не исчезает от того, что протестировали маленькие кусочки большой программы. Нужно будет еще протестировать как «большая» программа реагирует на все возможные выходы из каждого «кусочка». Если же мы мокаем «маленькие кусочки» в тесте «большой» программы, то нужно как-то гарантировать, что наши моки ведут себя равно также как настоящий «маленький кусочек». Все это крайне сложно и на практике не реализуемо. IMHO мокать нужно то, что невозможно или очень сложно контролировать в тесте.

И их количество было бы неустойчиво к рефакторингу.


В одном случае вы тестируете контракт 2х функции (createCounter и cancel), в другом тестируете контракты 3х (createCounter, cancel и onInterval). Рефакторинг с большей вероятностью затронет контракты внутренних функций и приведет к необходимости менять тесты. Если же принять за догму классическое определение рефакторинга, как улучшение кода без изменения контракта (видимо контракта createCounter и cancel, но не контракта onInterval), то тесты только для createCounter/cancel будут максимально устойчивы к такому рефакторингу. То есть я утверждаю обратное — тесты только createCounter/cancel более устойчивы к рефакторигу, чем тесты createCounter/cancel/onInterval.

Добавил одну вилку в любую функцию — нужно удвоить общее количество тестов, что не есть хорошо.


Почему удвоить, а не возвести в квадрат или взять факториал? Мне кажется, что варианты A и B ниже имеют одинаковое количество состояний и оттого, что часть функционала вынесли в функцию Step2 ничего не изменилось.

void A(Input) {
//STEP 1
//STEP 2
}

void B(Input) {
//STEP 1
Step2(Input);
}

void Step2(Input) {
//STEP 2
}
В целом соглашусь с вами.
комбинаторный взрыв никуда не исчезает

варианты A и B ниже имеют одинаковое количество состояний

Но юнит-тесты не гарантируют работоспособность программы в целом, только её кусочков. (см. примечание автора про терминологию). Юнит-тестов все-таки получится меньше.

А если же мы хотим протестировать весь модуль, то должны на него писать функциональные тесты. А в функциональных тестах можно и ограничиться основными use-case-ами.
Юнит-тестов все-таки получится меньше.


Не соглашусь. Положим мы тестируем белый ящик, тогда множество входов I разбивается на классы эквивалентности, на которых программа ведет себя «одинаково» с точки зрения потребителя. Для гипотетической страницы логина мы все корректные пары логин/пароль записываем к класс 1, валидный логин и неверный пароль в класс 2, валидный логин/пароль при нерабочей базе данных в класс 3 и т.д. до класса N. Так как мы знаем алгоритм (белый ящик), то мы может получить и эти N классов (на самом деле, на практике, мы этого не можем, но представим, что все таки можем). Для покрытия системы нам теперь нужно написать по одному тесту для каждого класса эквивалентности.

Обратите внимание, текст выше не говорит о юнитах или других видах тестов. Вам просто надо написать N тестов на контракт вашей функции и все тут. Если добавить еще и юниты, которые тестируют внутренности, вроде onInterval, то у вас будет N+M тестов вот и все.

Дискуссия unit test vs все остальное во многом искусственная и смысла большого не имеет. Цель же получить рабочий софт, который легче поддерживать. Идеальное решение вообще достигает этой цели без тестов. Все это ведет к более практическим вопросам
— Как выделить классы эквивалентности? Вроде бы ответ это опыт и знание мат. части, например знание того, что БД бывает отваливается, файлы не открываются, юзеры вводят 1Гб текста и т.п. Возможно хитрые системы типов. Хороших решений на горизонте не видно.
— Какие куски системы надо изолировать и мокать, а какие оставить как есть. Тут вообще ничего не ясно, вкусовщина и эвристика. Наука, к сожалению, молчит.

Очередное frontend безумие. Тчк.

Думаю, такой подход идеально подходит для всяких npm пакетов и либ, на реальном проекте такое раздувание файлов (вместо одного файла у нас куча файлов на каждую функцию ) ни к чему.

так, только наоборот
Простота и понятность кода немного ухудшилась, но это с лихвой компенсируется картиной 100% coverage в юнит-тестах.

Очень спорно. Вы предлагаете писать код, который проще тестировать, но сложнее понимать. И ради чего? 100% покрытие? Оно достигается проще: достаточно остановиться на декомпозиции, а остальное решается моками.
По моему надо стараться писать код для людей, а не для тестов.

То, что вы говорите, справедливо для разработки приложения, решения прикладных задач. Но для разработки библиотеки — не факт. Авторы многих npm-пакетов жертвуют читаемостью ради тестов.

А вы какой код больше пишете — библиотечный, или прикладной? Я вот 99% времени занимаюсь разработкой приложений, поэтому ответил так.
Но мне не кажется, что код библиотеки чем-то концептуально отличается. Да, там ценность тестов и покрытия ими выше, но почему это должно приводить к усложнению кода?
Я не отрицаю, что есть проблема сложно тестируемого кода, но я предпочту решать ее упрощая и разбивая такой код. Это пойдет на пользу не только тестам, но и читабельности.

Я — библиотечный. И пачки тестов на него. Поэтому такая проф. деформация

Было бы здорово, если вы это отразили в статье, это и правда своего рода деформация.
У меня кстати тоже есть своя: я вообще не верю в юнит тестирование на живом, развивающемся проекте (не библиотеке!). Смоук, e2e — да, но юниты — в моих глазах просто еще код, который надо поддерживать (переписывать вслед за любым изменением тестируемого кода). А поддерживать проще всего тот код, которого нет.
Да, юниты дают определенную уверенность в коде, но я не согласен на цену. Лучше проинвестирую в e2e по критическому пути.

По факту мы из объекта выкусили состояние и сделали наши функции зависимыми от этого состояния подающегося им на вход. Добро пожаловть в мир классов на старом добром C:) Там функции вполне чистые, и результат зависит только от аргументов(в том числе this), но всегда ли это хорошо и удобно? Да, я знаю про функуиональный стиль итд, но вот вопрос, не превращается ли это в написание кода только ради написания кода?

Не убедили. С помощью timer-mocks можно удобно замокать нативные методы и тестировать createCounter целиком, без DI.


Такие тесты проще поддерживать, потому что они поменяются только вместе с требованиями к библиотеке, и не зависят от внутренних модулей, которые могут меняться при внедрении новых фич и рефакторинге.


Возможно, пример с createCounter просто неудачный, но на нем преимуществ никаких не видно.

В чем преимущество такой записи
export const cancel = pool => {
// ...
}

перед такой:
export function cancel(pool) {
// ...
}

?

Применительно к нашей ситуации, преимуществ нет

Зарегистрируйтесь на Хабре, чтобы оставить комментарий