Комментарии 24
Почему я решился на 100% тестирование?
Скорее всего, из-за непонимания или неправильно использования юнит тестов. Вы же сами пишите, что
Еще более сложная ситуация, если логика проверяемой функции серьезно переработана – в итоге, мы получаем не только то, что написано выше, но еще и тест полностью переписать придется.
То есть вы уже сталкиваетесь с ситуацией, когда из-за плохих тестов при внедрении нового функционала (или обновления его), вам приходится переписывать тесты. И теперь, из-за этого, вы решили вообще все покрыть тестами. Противоречие?
Хорошие тесты не должны завязываться на реализацию, а проверяют исключительно выходное значение при передачи различных параметров на вход. И место для тестирования - это бизнес слой, в первую очередь. Поэтому он должен быть изолированным, и должен быть представлен в каких-то формах, которые можно протестировать. Разумеется, в реальной жизни так далеко не всегда можно сделать, но лучше идти в эту сторону, улучшить структуру приложения, и покрывать бизнес логику тестами, а не стремиться покрыть все тестами на 100%.
То есть вы уже сталкиваетесь с ситуацией, когда из-за плохих тестов при внедрении нового функционала (или обновления его), вам приходится переписывать тесты.
А как вы предполагаете написать тест на функцию так, что бы они никогда не переписывался, если вам априори придется учитывать ветвления в коде и зависимости?
И теперь, из-за этого, вы решили вообще все покрыть тестами. Противоречие?
Ну, это не так. Мы решили покрывать тестами 100 кода, что бы быть уверенными, что те функции, которые надо тестировать, будут протестированы. А все остальное, получается, идет прицепом. В общем то, это есть в статье.
Хорошие тесты не должны завязываться на реализацию, а проверяют исключительно выходное значение при передачи различных параметров на вход.
Абсолютно верно. Это значит, что мы имеем либо функцию без зависимостей (что бы их не мокать), либо проверяем ее вместе со всеми зависимостями (что я бы уже не назвал юнит тестом).
Ну и иногда у функции внутренний контракт меняется, когда меняются условия в ветках или добавляются/изменяются ветки.
в реальной жизни так далеко не всегда можно сделать
Об этом и статья)
То, что функция имеет 100% покрытия не значит, что она протестирована
Тестировать надо не строки (ради % покрытия), а поведение (в том числе те самые граничные условия).
Например, у нас есть функция, которая должна заменять все табуляции, переносы строк и т.п. на пробелы.
Функция из 1 строки (просто вызов replace).
Любой тест даст сразу 100% покрытия, но не любой тест проверит что все нужные символы заменены на пробелы
если вам априори придется учитывать ветвления в коде и зависимости?
Я Вам о том и говорю, что если есть какие-то тесты, которые завязаны на реализацию методов, в которых есть какие-то ветвления и зависимости – это плохие тесты, вы на каждых чих будете вынуждены их переписывать. Чтобы такого не было, нужно пересматривать структуру логики, декомпозировать методы и т.д.
Ну, это не так. Мы решили покрывать тестами 100 кода, что бы быть уверенными, что те функции, которые надо тестировать, будут протестированы.
Уже есть понимание, что надо тестировать - бизнес логику. Бизнес логики от всего кода приложения может быть процентов 20, может и больше. Но вокруг нее всегда много кода, который не нужно покрывать именно юнит тестами. И лучше покрыть 80% от бизнес логики, что может быть 20-40% от всего кода, чем покрыть 100% всего кода. Вы эти тесты замучаетесь поддерживать и переписывать, а самое главное, что их ценность стремиться к нулю.
Абсолютно верно. Это значит, что мы имеем либо функцию без зависимостей (что бы их не мокать), либо проверяем ее вместе со всеми зависимостями (что я бы уже не назвал юнит тестом).
Опять же, без конкретных фрагментов – это абстрактные разговоры. Зависимости разные бывают. Если зависимости внешние - то да, их не надо включать, если это зависимости внутренние, сервисы какие-то, валидаторы - то почему бы и нет. Это реально сложно организовать нормальное тестирование, просто потому, что сложно писать какие-то бизнес правила в функциональном стиле, где что-то подается на вход, что-то всегда есть на выходе. На эту тему немало есть книг и статей.
Я лишь к тому, что гонка за цифрой в 100% покрытие кода – это путь в никуда. Единственное, кому этот путь может быть выгоден – менеджерам для отчетов, для согласования бюджетов и сроков, для раздувания штата тестировщиков и т.д.
Отвечу вам на оба поста.
Уже есть понимание, что надо тестировать - бизнес логику.
Я лишь к тому, что гонка за цифрой в 100% покрытие кода – это путь в никуда.
Я с вами согласен. Возможно, вы не заметили, но мои 100% - это не бездумное тестирование всего и вся - есть часть кода, которую решено не тестировать. Именно по той причине, что ее не надо тестить. Поэтому и покрытие выходит 100%, и мы точно знаем, что в нужных классах/пакетах протестировано все.
Ну и про тесты и реструктуризацию кода - в статье есть упоминание про то, что тесты заставляют задуматься над архитектурой кода. Но проблема в том, что вы сами пишете о том, что это а) довольно сложно и б) не всегда реально. Прибавьте к этому поддержку легаси кода... и получаем то, что имеем.
Но в общем случае - вашу позицию я понимаю и вполне с ней согласен.
Мне кажется больше всего пользы с тестов, когда они пишутся другим человеком и приходят вместе с задачей уже разработчику. Жалко, что такое мне редко встречалось.
Для некоторых функций у нас похожее реализовано - есть параметризированный тест, аналитики прописывают условия в json формате и подкладывают их в задачу.
Но в общем случае, такое далеко не всегда в принципе можно реализовать. Обычно, граничные случаи приведены в задаче, и они должны быть протестированы. Но зачастую в коде граничных случаем много больше, чем возможно описать в задаче.
Если в задаче не пишется псевдокод. Но это уже совсем другая история.
Но, если в код добавилась новая ветка или вызов какого-то сервиса – нам надо править и тестирование, ветку надо обработать в существующих тестах и/или написать под нее отдельный тест, а новые вызовы надо замокать.
Есть функция, которая получает какие-то значения на входе, и возвращает какое-то значение на выходе, которое мы сравниваем с ожидаемым - это и есть тест.
Если у нас появилась необходимость изменить поведение функции, то есть на входе/выходе у нас теперь не то, что мы ожидали раньше, то нам нужно создавать новую функцию, которая будет работать по-другому, и под неё добавить ещё один тест.
Есть контракт (интерфейс) и если логика изменилась, то это будет нарушение контракта, так что его просто нужно расширить, а изменяя поведение старых функций, мы нарушаем принципы SOLID.
Да здравствуют декораторы на декораторы)
Допустим вчера у нас был интерфейс и реализация, в которой мы получаем результат умножения.
public interface IWorker
{
int DoSomething(int input1, int input2);
}
public class Worker : IWorker
{
public int DoSomething(int input1, int input2)
=> input1 * input2;
}
// TEST
А сегодня нам бизнес сказал, что нужно не умножать, а делить и получать результат деления.
И это всё так же должен быть тот же самый метод, в котором нам нужно изменить интерфейс и реализацию?
public interface IWorker
{
// Меняем интерфейс
double DoSomething(int input1, int input2);
}
public class Worker : IWorker
{
// Меняем реализацию
public double DoSomething(int input1, int input2)
=> input1 / input2;
}
// Меняем тесты
Я склоняюсь к тому, что теперь должна быть ещё одна реализация.
public interface IWorker
{
int DoSomething(int input1, int input2);
double DoSomething2(int input1, int input2);
}
public class Worker : IWorker
{
// начальная реализация
public int DoSomething(int input1, int input2)
=> input1 * input2;
// Добавляем реализацию
public double DoSomething2(int input1, int input2)
=> input1 / input2;
}
// Добавляем TEST2
И если завтра нам нужно будет вернуть умножение, то в самой программе, мы просто изменим вызов worker.DoSomething2(4, 2) на worker.DoSomething(4, 2), и даже тесты не придётся править, и кстати все тесты, что сегодня, что завтра будут успешны.
А когда нужно будет остановиться и перестать писать строки мертвого кода? Вчера был один метод, сегодня новый, завтра ещё требование изменилось и захотят сложение, а мы уже два метода написали, покрыли тестами и держим просто так получается?
Хз почему минусы понаставили. Тоже глаз зацепился за этот пункт. Вопрос дискуссионный. Что хорошо для небольшого микросервиса или сервиса, может быть плохо для огромного монолита.
Как я разочаровался в юнит тестах КОГДА решил, что единственный вариант получить от них пользу — 100% покрытие
Вот к каким мыслям пришёл я по поводу тестов:
1) самое важное, что они дают - это понимание, хороший ли у тебя получается код. Если код тестировать сложно и неудобно - это плохой код. Написание теста не должно становиться болью, и если это вдруг становится болью, то это сразу же сигнал, что ты что-то делаешь не так
2) соответственно, покрыто тестами должно быть все, что имеет какую-либо логику. Если класс принимает какие-либо аргументы на входе, что-то с ними делает, и выдаёт какой-то результат, то это должно быть проверено
3) в конечном итоге ты пишешь тест не для каких-то абстрактных KPI, а для себя любимого. Всегда есть вероятность, что при каком-нибудь рефакторинге ты что-то случайно заденешь, и это что-то выстрелит в проде, и тогда ж именно тебе позвонят в 5 утра с криками, что все сломалась, и надо срочно чинить. Если есть возможность хоть как-то обезопасить себя от этого, то не стоит эту возможность упускать
Я, кстати, как раз размышляю над собрать все свои мысли в кучку, добавить примеров и оформить презентацию для коллег на работе, и если кто-нибудь накидает мне умных мыслей в копилку, то буду благодарен :)
А пробовали TDD? Для всего чуть сложнее crud это сразу делает и дизайн верный и тестируем не руками, а сразу тест. Итоговое время разработки сопоставимо чем без тестов, ведь на ручное тестовое мы тоже тратим время.
Простые вещи, слишком очевидные, не обязательно тестировать.
Тесты великая вещь в open source проектах, там можно найти примеры, как оно должно работать.
Тесты очень сильно помогают при рефакторинге, мы просто не боимся делать его.
На своих проектах тоже пришёл к выводу, что огромной пользы от юнит-тестов нет и получить не выйдет, как покрытие не повышай. Плюс лично я против того, чтобы, когда вносятся нефункциональные изменения в боевой код (рефакторинг), требовалось изменять тесты. Потому что тест должен защитить боевой код от потенциальных проблем при рефакторинге, а изменяя его — ты защищаешь этот самый баг ) И, к слову, даже наличие проблем в непроверенных краевых вызовах какого-то метода может спокойно быть нивелировано реальной невозможностью этот код таким образом вызвать (в рамках ввода пользователя).
Теперь у нас развёрнутая пирамида тестирования — больше всего кейсов у QA, backend-разработчики пишут преимущественно тесты по веткам пользовательский сценариев процессов (как хотите называйте, e2e / интеграционные), а юнитами покрываются только изолированные кирпичики. А TDD очень хорошо заходит, когда реально от QA или с боя прилетает баг — сперва тестом воспроизводится, затем фиксится.
Писать тесты для своего кода это значит сомневаться в своих скилах. Это признак слабости.
В разработке тесты больше помогают понять задачу, чем проверяют работоспособность. Тем более, как правильно писали выше, наличие тестов не гарантирует работоспособность модуля.
Я считаю, что тесты это больше слепок поведения, нежели проверки и нужны они для обезболивания расширения/изменения функционала. Тем более, привести в порядок тесты(написать/починить имеющиеся) + отрефачить код намного дешевле, чем рефачить без юнитов.
Почему автор как достижение говорит про вот такие вещи, что стало меньше ошибок по невнимательности:
Типичный пример: пишем в контроллере POST вместо GET.
Вот зачем на это вообще писать тесты? Объясните мне? Это пишется 1 раз, проверяется 1 раз и больше не меняется никогда. Нафига тратить время на тестирование статического функционала?
Писать тесты надо на вещи которые можно случайно испортить и не заметить этого. Как можно испортить метод в контроллере? Дописать логику и случайно переписать гет на пост? Запустить тест и понять что, ой, я сломал контроллер, надо было оставить гет как было раньше.
Ну смотрите.
Написать тест на конечную точку с использованием wire mock стоит 5-10 минут рабочего времени.
Взамен мы проверяем:
Корректность метода.
Корректность самого урла.
Корректность парсинга переменных пути/хедеров/параметров запроса.
Корректность десериализации тела запроса.
Плюсом если вдруг когда то конечная точка изменится - поправить тест и убедиться, что все ок - минутное дело.
Вы можете это все проверить, например, через постмана, но это не всегда быстрее и так же чревато "ошибками невнимательности". А можете вообще отдать функционал в тестирование без проверки, что, по моему мнению, не профессионально.
Я не призываю читателей тестировать весь код. И не утверждаю, что это "правильно" или "не правильно". Я просто написал те выводы, которые на данный момент извлек из своего опыта. Не надо представлять это "сектой".
И по поводу "почему автор как достижение говорит про вот такие вещи, что стало меньше ошибок по невнимательности " - хоть я и не пытался преподнести это как "достижение", а лишь указал один из плюсов тестирования, однако: а разве это не достижение?)
Как я разочаровался в юнит тестах и решил, что единственный вариант получить от них пользу — 100% покрытие