Билд можно ускорить, в конце концов можно просто параллелить запуск.
Во-первых, это можно поправить. Во-вторых, это же тест, он независимый и быстрый - запустил в его дебаге и посмотрел
Связи не вижу. Любой плохой код можно юнит тестом покрыть, от этого он лучше автоматически не станет.
А может быть и так - на плохой код написаны тесты и теперь мы не можем этот плохой код переделать в хороший, т.к все тесты просто сломаются и будем вечно жить с плохим кодом. Вообще почему мы тут говорим о написании тестов на код, мы же хотим TDD, сначала тесты писать, а потом код? Перемудренная логика - это требование бизнеса. Ее можно и нужно упрощать, но это не всегда возможно. А обычно в бизнес логике и есть ценность it-продукта.
Либо проект в активной разработке и постоянно обрастает функционалом
Юнит тест отдельного метода - с точки зрения документации кода не много информации приносит. Мы же пишем понятный код с короткими методами, стараемся соблюдать single layer of abstraction и прочее. Чтобы понять, что метод делает должно быть проще сам метод посмотреть. А общую картину юнит тест на отдельный метод - не дает.
Рефакторить отдельный метод? Если рефакторить что - то серьезное, то тут только интеграционные тесты могут регресс поймать, а то и e2e. После рефакторинга у нас половина юнит-тестов будет пытаться несуществующие методы вызывать.
Тут тоже самое - если метод реально сложный - то ок, юнит тест необходим. Но считать, что по юнитам можно понять как работает фича - это странно на мой взгляд. А то будет ситуация, что замок на двери зарыли, а саму дверь захлопнуть забыли. Хотя оба теста на замок и дверь отлично работают. А в реальности фича, это не 2 составные части, а 50.
На практике утверждения вида "пишите сразу правильно и тогда тесты менять не придется" и "если при изменении логики надо менять много тестов - вы написали плохой код" и подобные им - с точки зрения юнит тестов отдельных методов актуальны либо в (1) простых проектах, либо в (2) медленно меняющихся.
Если проект относительно сложный и живой - это не будет работать. Если вы напишите на каждый метод по 20 тестов на каждый инвариант использования, а потом прилетит требование от бизнеса поменять логику так, что этот метод надо вообще будет удалить / объединить с другим методом, хотя в целом контракт внешне не сильно поменялся - то что делать с этими тестами? Удалить только.
А интеграционный тест - вы поправили пару полей в тестовых данных, пару проверок на границе фичи (внешние вызовы, очередь, БД и др.) и все - тест работает и регресс проверяет. Внутри меняйте и двигайте код как угодно.
Тезис о том, что юнит тесты отдельных методов заставляет писать более правильно декомпозированный код - это правда, но этот плюс почти всегда перекрывается тем, что (1) почти невозможно с 1го раза написать декомпозированный код для сложной фичи. И (2) если неудачную декомпозицию зацементировать юнит тестами, то потом вы уже ее без боли не отрефакторите.
Если честно, не согласен и считаю, что всё должно быть наоборот.
Основное поведение системы должно покрываться интеграционными тестами. А юнит-тесты стоит писать выборочно — там, где в одном методе или классе действительно сосредоточено много логики, или же для утилитных методов, какие-то сложно-читаемые штуки с регулярными выражениями и подобными вещами.
Аргументы следующие:
В большинстве проектов мы стараемся писать простые и читаемые методы. Тест на каждый из них — это гонка за покрытием без реальной пользы. Чем меньший объём логики покрывает тест, тем выше риск, что он упустит общий смысл поведения фичи/системы.
Юнит тесты в том виде, что вы предлагаете - "заливают весь проект эпоксидной смолой" / "прибивают все гвоздями". Такие тесты не помогут вам в рефакторинге и проверке регрессионных багов. При значимом рефакторинге тесты придется выкинуть или очень сильно менять. Если проект развивается и нужно корректировать декомпозицию / уровни абстракции / доменную область или контекст - то такое тесты будут только мешать.
TDD в такой интерпретации почти невозможен. Тут почти всегда будет 2 варианта:
Логика фичи настолько простая, что мы заранее можем разбить всё на методы, написать на них тесты, а потом реализовать — но тогда и тесты несут мало пользы - всё и так очевидно.
Логика фичи сложная — и тогда мы не сможем заранее угадать нужную декомпозицию. Придётся писать код, рефакторить его в пару итераций, а потом уже писать тесты. Это уже не TDD, да и такие тесты в любом случае не проверяют фичу в целом.
Медленные интеграционные тесты — почти всегда решаемая задача. Есть способы ускорить их запуск: поднимать только необходимый контекст, переиспользовать контекст / БД / контейнеры, использовать ленивую загрузку и прочее. Данные можно подготовить заранее и один раз, и например, миграции БД не накатывать на каждый тест. Миграции тестировать отдельными тестами, если они очень долгие. В общем, варианты есть.
Сложность подготовки тестовых данных — это тоже сигнал. Если мы не хотим писать интеграционные тесты, т.к не можем под них создать тестовые данные - значит система переусложнена. Тут уже вопрос - что мы вообще собираемся тестировать и какой смысл любых тестов на такой системе? Тут надо архитектурно декомпозировать систему, упрощать и так далее. Все-таки тестовые данные почти всегда создать можно, возможно чуть больше усилий нужно потратить, но это окупиться тем, что теперь у нас будут тесты, которые наконец-то дадут вам возможность использовать их для рефакторинга, регресса и прочее.
Итого, если есть возможность, то я бы рекомендовал делать 90% именно интеграционных тестов.
Билд можно ускорить, в конце концов можно просто параллелить запуск.
Во-первых, это можно поправить. Во-вторых, это же тест, он независимый и быстрый - запустил в его дебаге и посмотрел
Связи не вижу. Любой плохой код можно юнит тестом покрыть, от этого он лучше автоматически не станет.
А может быть и так - на плохой код написаны тесты и теперь мы не можем этот плохой код переделать в хороший, т.к все тесты просто сломаются и будем вечно жить с плохим кодом. Вообще почему мы тут говорим о написании тестов на код, мы же хотим TDD, сначала тесты писать, а потом код? Перемудренная логика - это требование бизнеса. Ее можно и нужно упрощать, но это не всегда возможно. А обычно в бизнес логике и есть ценность it-продукта.
Либо проект в активной разработке и постоянно обрастает функционалом
Юнит тест отдельного метода - с точки зрения документации кода не много информации приносит. Мы же пишем понятный код с короткими методами, стараемся соблюдать single layer of abstraction и прочее. Чтобы понять, что метод делает должно быть проще сам метод посмотреть. А общую картину юнит тест на отдельный метод - не дает.
Рефакторить отдельный метод? Если рефакторить что - то серьезное, то тут только интеграционные тесты могут регресс поймать, а то и e2e. После рефакторинга у нас половина юнит-тестов будет пытаться несуществующие методы вызывать.
Тут тоже самое - если метод реально сложный - то ок, юнит тест необходим. Но считать, что по юнитам можно понять как работает фича - это странно на мой взгляд. А то будет ситуация, что замок на двери зарыли, а саму дверь захлопнуть забыли. Хотя оба теста на замок и дверь отлично работают. А в реальности фича, это не 2 составные части, а 50.
На практике утверждения вида "пишите сразу правильно и тогда тесты менять не придется" и "если при изменении логики надо менять много тестов - вы написали плохой код" и подобные им - с точки зрения юнит тестов отдельных методов актуальны либо в (1) простых проектах, либо в (2) медленно меняющихся.
Если проект относительно сложный и живой - это не будет работать. Если вы напишите на каждый метод по 20 тестов на каждый инвариант использования, а потом прилетит требование от бизнеса поменять логику так, что этот метод надо вообще будет удалить / объединить с другим методом, хотя в целом контракт внешне не сильно поменялся - то что делать с этими тестами? Удалить только.
А интеграционный тест - вы поправили пару полей в тестовых данных, пару проверок на границе фичи (внешние вызовы, очередь, БД и др.) и все - тест работает и регресс проверяет. Внутри меняйте и двигайте код как угодно.
Тезис о том, что юнит тесты отдельных методов заставляет писать более правильно декомпозированный код - это правда, но этот плюс почти всегда перекрывается тем, что (1) почти невозможно с 1го раза написать декомпозированный код для сложной фичи. И (2) если неудачную декомпозицию зацементировать юнит тестами, то потом вы уже ее без боли не отрефакторите.
Если честно, не согласен и считаю, что всё должно быть наоборот.
Основное поведение системы должно покрываться интеграционными тестами. А юнит-тесты стоит писать выборочно — там, где в одном методе или классе действительно сосредоточено много логики, или же для утилитных методов, какие-то сложно-читаемые штуки с регулярными выражениями и подобными вещами.
Аргументы следующие:
В большинстве проектов мы стараемся писать простые и читаемые методы. Тест на каждый из них — это гонка за покрытием без реальной пользы. Чем меньший объём логики покрывает тест, тем выше риск, что он упустит общий смысл поведения фичи/системы.
Юнит тесты в том виде, что вы предлагаете - "заливают весь проект эпоксидной смолой" / "прибивают все гвоздями". Такие тесты не помогут вам в рефакторинге и проверке регрессионных багов. При значимом рефакторинге тесты придется выкинуть или очень сильно менять. Если проект развивается и нужно корректировать декомпозицию / уровни абстракции / доменную область или контекст - то такое тесты будут только мешать.
TDD в такой интерпретации почти невозможен. Тут почти всегда будет 2 варианта:
Логика фичи настолько простая, что мы заранее можем разбить всё на методы, написать на них тесты, а потом реализовать — но тогда и тесты несут мало пользы - всё и так очевидно.
Логика фичи сложная — и тогда мы не сможем заранее угадать нужную декомпозицию. Придётся писать код, рефакторить его в пару итераций, а потом уже писать тесты. Это уже не TDD, да и такие тесты в любом случае не проверяют фичу в целом.
Медленные интеграционные тесты — почти всегда решаемая задача. Есть способы ускорить их запуск: поднимать только необходимый контекст, переиспользовать контекст / БД / контейнеры, использовать ленивую загрузку и прочее. Данные можно подготовить заранее и один раз, и например, миграции БД не накатывать на каждый тест. Миграции тестировать отдельными тестами, если они очень долгие. В общем, варианты есть.
Сложность подготовки тестовых данных — это тоже сигнал. Если мы не хотим писать интеграционные тесты, т.к не можем под них создать тестовые данные - значит система переусложнена. Тут уже вопрос - что мы вообще собираемся тестировать и какой смысл любых тестов на такой системе? Тут надо архитектурно декомпозировать систему, упрощать и так далее. Все-таки тестовые данные почти всегда создать можно, возможно чуть больше усилий нужно потратить, но это окупиться тем, что теперь у нас будут тесты, которые наконец-то дадут вам возможность использовать их для рефакторинга, регресса и прочее.
Итого, если есть возможность, то я бы рекомендовал делать 90% именно интеграционных тестов.