Начну с небольшой предыстории:
У меня есть слоистая архитектура, оперирующая следующими понятиями: Request, ViewModel, Repository, Presenter
В нескольких словах это можно описать так описать так:
- Request - делает запрос в некий источник данных (например файл, база или Api)
- Далее с помощью некого билдера возвращает ViewModel (класс с геттерами)
- Presenter - некая обертка над ViewModel, которая модифицирует методы для видов.
- Repository особо в описании не нуждается.
При тестировании, я создал мок для Request, подсунул определенные данные и стал тестировать.
Проблема обнаружилось в том, что если вдруг тест падает на ViewModel, то это затрагивает другие тесты. Тоесть если не работает ViewModel, автоматически падают тесты Request, Presenter и Repository
Цепочка вызовов идет примерно так: Repository -> Request -> ViewModel -> Presenter
Соответственно, если упадет тест более низкоуровневой вези, например Request, упадут все тесты, которые используют данный Request.
Я очень сильно сэкономил на коде тестов, тоесть вроде все затестированно, может выполнятся в произвольном порядке, а вот нюанс с падением 4 тестов вместо одно, сомнительный.
Подскажите, пожалуйста, нужно ли мокать каждый класс и писать в 10 раз больше кода, чтобы такого не происходило, если да, то почему ?
Мне кажется, ничего нового вы не услышите. Чтобы тесты Presenter не падали при поломке ViewModel вам просто необходимо не использовать реальный ViewModel в тестах Presenter. Да, придется писать больше кода - волшебства не бывает. Если еще этого не делаете, попробуйте использовать более удобную библиотеку для создания моков, например Mockery - это облегчит страдания.
nepster-web: в теории вы должны покрыть код тестами и тесты не должны быть хрупкими, сколько для этого потребуется кода не важно. Если вам приходится писать прям тонны кода для проверки отдельных классов, возможно, у вас проблемы с архитектурой и вы тестируете поведение, которое можно было бы вынести в отдельный объект и там протестировать меньшим количеством кода.
Не стоит смешивать модульные тесты и интеграционные (или функциональные). Цель модульных тестов проверить работу одного модуля (класса, например). В этом случае все его зависимости мокаются. Целью интеграционных тестов является проверка взаимодействия цепочки модулей (сервисов, с БД и т.п.) друг с другом.
То что вы написали похоже на интеграционные тесты. Очевидно, что такие тесты будут падать, если что-то не работает в любом месте всей тестируемой цепочки модулей. И это не связано с изоляцией тестов друг от друга.
То есть правильно было бы сформулировать вопрос таким образом: "какой процент покрытия модульными тестами будет достаточным для моего кода?" Обычно останавливаются где-то на 70-80%
Также очевидно, что 100% работающих модульных тестов не гарантирует работу интеграционных тестов или функциональных. Поэтому необходимо писать и те и другие.
Я бы не стал фанатично закрывать все методы классов тестами, а только те, в которых имеется высокая цикломатическая сложность, либо которые скорее всего будут меняться. Короче, нет большого смысла в тестировании примитивных/стабильных участков кода.
я в основном делаю ui тесты и у меня некоторые тесты связаны друг с другом. конечное состояние первого теста является начальным сосотоянием следующего. Например первый тест "зайти в формуляр" а следующий за ним "ввести данные в первую строку" а затем тест "ввести данные во вторую строку" а потом "нажать на отправку данных" а потом "тест сообщения об отправке". Естественно если первый тест завалится, завалятся и все после него, хотя реальная проблема только одна. Единственный серьезный недостаток такого подхода, это возможное перекрытие ошибок. Т.е. в билде может сломаться заход в формуляр, и отображение сообщения об отправке. Вторая проблема будет перекрыта первой.
Но меня устраивает такой подход по следующим соображениям.
- Если что-то сломалось в приложение надо в первую очередь чинить приложение, а не писать тонны дополнительного тестировочного кода.
- Тестировочный код тоже код, чем его больше тем больше вероятность сделать ошибку в нем.
- Лучше потратить время на увеличение покрытие пользовательских сценариев, чем на создание красивой и каноничной автоматизации.
- Зависимые тесты выполняются быстрее, (а у нас время тестирования билда во всех вариациях уже перевалило за 16 часов)
- Чинить по одной ошибке за раз - хорошо, пытаясь починить сразу несколько можно сломать что-то еще.
Для того чтобы такой подход работал более менее чисто, я например стараюсь сдвигать все проверки в конец теста, уменьшая таким образом вероятность того что вылетевшее исключение не доведет приложение до нужного состояния для последующего теста.
Но это все не по вашему вопросу. По вашему вопросу - да, писать тонны моков (не нужно). У меня например есть тесты которые проверяют как данные от железа передаются в api графического клиента. при проверке фунцкионала графического клиента, я опираюсь на корректную работу api графического клиента, поскольку им пользуюсь. Так же я опираюсь на корректную работу виджетов. И если какие-то условия не будут работать многие ui тесты посыплются. Мы найдем причину и устраним ее. Чтобы в таком случае падал только один тест, который специально сделан для нахождения этой ошибки, вам придется отвязать тестируемую компоненту от всех внешних зависимостей. Теоретически написав столько же кода сколько в самом приложении. Вы готовы к этому?
P.S.: стоит добавить: наш тест-фреймворк выполняет тесты в алфавитном порядке и не поддерживает параллельное выполнение в принципе.