Pull to refresh

Comments 21

Как вы пишите много много юнит тестов и не огребаете при рефакторинге? Удается ли вам ловить реальные баги с помощью юнитов? Расскажите как, потому что сколько я не пытался заставить юнит тесты работать всегда получается одно и то же:
— Любой рефакторинг превращается в борьбу с тестами, время на рефакторинг увеличивается в разы.
— Тесты ломаются от чего угодно, только не от багов. Это из-за моков, но как писать юниты без них непонятно.

Отличная ссылка, спасибо! Оказывается я использую Traffic Cop :) Например, есть утилиты, чтобы перехватить запрос к базе и вернуть ошибку или модифицировать результат. Аналогично для запросов к разным сервисам.


С такой инфраструктурой нет смыла писать классические юнит тесты. Тесты высокоуровневых компонент, часто (но не всегда) протестируют низкоуровневые. Нет никакой нужды тестировать каждый сервис, если можно просто дернуть HTTP API, которое дёрнет тот же самый сервис и заодно проверить настройки сериализации, авторизацию и много чего ещё. Те же HTTP запросы проверят репозитории/DAO, генерацию SQL и т.п.


Хочется узнать как у автора все устроено.

ИМХО


  1. Тесты — документируют ваш интерфейс с точки зрения использования. Вряд ли вам или кому-то ещё захочется писать подробную документацию к вашим интерфейсам в вики, где они скоро устареют или в комментариях. Вообще есть такой тренд, как "комментарии — зло". Штош, в таких условиях тесты это лучик света в этом царстве :)
  2. Тесты позволяют протестировать юнит без запуска приложения. Иначе вы вынуждены запускать весь проект, чтобы проверить работает ли ваш компонент правильно, а ещё нужно все условия воспроизвести, чтобы протестировать разные кейсы. На момент разработки это здорово снижает производительность.
  3. Тесты позволяют проводить этот самый рефакторинг, так как иногда вскрывают некоторые проблемы в дизайне и архитектуре. Обычное дело, если написание теста к юниту превращается в хардкор и мешанину моков, с юнитом явно что-то не так.
  4. Чем хуже архитектура, тем болезненней использования тестов. Проект, которые переживает этот самые рефакторинг частыми приливами, приводит к переписыванию тестов, что сводит на нет весь смысл.
  5. Не надо переживать, что юнит-тесты почти не ловят баги. Это же наоборот — хорошо :)

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


Если тесты не ловят багов, а стоят в поддержке и написании как полноценная команда программистов, то наймите каждому программисту выделенного тестера, а разницу в зп положите в карман.

Вообще это указывает на проблемы в архитектуре, если вы хотите «перепаять» внутренности не ломая контракты (рефакторинг), а при этом контракты ломаются. Или тесты зацеплены не за контракты, а за «внутряк». Или вы делаете не рефакторинг, а модифицируете приложение.
Ну и юниты я бы не писал на все подряд, а только на контракты с внешними системами.
Когда я сомневаюсь правильно ли я выбрал охват теста, я стараюсь ответить себе на вопрос: «что является предметом этого теста». Предмет теста нужно четко выделять.

Ну и хорошие новости: если тест «ломается» значит он действительно что-то распознает. Другое дело дает ли он вам необходимую информацию. Ту которую вы бы хотели получать.

Что такое мок? Это обьект который при обращении к нему отвечает как настоящая система, но при этом ей не является. Т.е. с его помощью вы заполняете пробел в коммуникационном контуре приложения для выполнения тестировочной задачи. Мок может влиять на тест в том случае, если контракт/протокол общения с этим объектом поменялся, и тест уже общается по новому, а мок еще по старому.

А еще помогает, понять, что тесты это программный комплекс, и относиться к ним соответствующим образом. Это приложение. Прикладная программа, для проверки работоспособности различный частей основного приложения. Вы относитесь к тестам как к «гражданам второго сорта», а они совершенно равноправные и тоже являются приложением. Если применять к тестировочному ПО тот же уровень инженерной добросовестности как и к тестируемому приложению, то все должно начать работать на вас, а не против вас.

Перестаньте бороться с тестами, примите их как «сигнализацию» для вашего кода.
Вообще это указывает на проблемы в архитектуре, если вы хотите «перепаять» внутренности не ломая контракты (рефакторинг), а при этом контракты ломаются.


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

Перестаньте бороться с тестами, примите их как «сигнализацию» для вашего кода.


Я с ними не борюсь, более того, команда их пишет, буквально тысячи их. Только это не юнит тесты ни разу. Несмотря на то, что я честно старался научиться их писать и упражнялся в этом где-то 3 или 4 года, от юнит тестов я получил только головную боль и потерянное время. А коллеги говорят, что оно у них работает. Вот мне и интересно как они решают те проблемы, которые заставили меня отказаться от юнит тестов.
Без тестов у нас было M мест, где нужно поменять код, в связи с изменением сигнатуры TryGetCachedValue. С тестами появилось ещё N мест. Если N много больше M, то будем говорить, что моки затрудняют рефакторинг. Кажется N всегда больше M.
Мне не очень понятно как получается так, что в тесте метод претерпевший изменения используется «на каждом углу»? Обычно такие низкоуровневые вещи «плотно, в три слоя» обматывают абстракцией и уже в гораздо менее подверженом изменениям виде используют в тесте. Такие абстракции писать конечно тоже трудозатратно, но не приходится потом в миллионах мест в тесте адаптировать.

А тесту совершенно безразлично откуда он получит необходимое вспомогательное значение (хоть константу пиши). Так что вы мы по идее не нарушаем этим чистоту теста. Способ получения данных из кеша предметом теста не является, а проверяется какой-то более высокоуровневый функционал.

Представим, нам для эксперимента(теста) нужен ментос и банка кока-колы. Мы же не станем бегать каждый раз за ними в магазин, а ночью на заправку. Мы поставите автомат который всегда по требованию будет выдавать нам новую банку колы и пачку жевательных конфет. Мы постараемся максимально отвязаться от процесса приобретения вспомогательного материала, т.е. абстрагироваться от него, чтобы сконцентрироваться на том что нам важно. Так и тут.

Возьмем другую ситуацию (она мне немного ближе), у нас UI тестирование, фронтенд переименовал локаторы элементов — конечно это затронет тесты. И необходимо придумать способ при внесении таких изменений снизить трудозатраты на адаптацию тестов. Отказываться из-за этого от тестов совсем нет нужды. А трудозатраты на подгон тестирующей системы под изменившийся тестируемый обьект будут всегда, в том или ином обьеме.

Несмотря на это я совсем ни за что не агитирую. Каждый сам должен решать какие виды тестирования ему пользу приносят, а какие нет. Да, юнит тесты могут совсем быть нерентабельными, там кода нужно порой в полтора раза больше чем на само приложение. И я против оголтелого фанатизма и покрытия «квадратно-гнездовым», «потому что все так делают».
Добрый день, рефакторинг не представляет проблем — моки строго типизированы и в целом весь код очень дружественен к инструментам автоматического рефакторинга.

Баги можно ловить юнит тестами. Есть два ярких случая:
1. Код с неочевидным поведением — при прочтении кода, не понятно как код себя ведет. Примеры такого кода любят давать на собеседовании, на проверку знаний спецификации языка или среды.
2. Код с вычислениями — в вычислениях могут существовать критические случаи, которые в повседневном использовании редко возникают, и как следствие их сложно найти. Юнит тесты сразу позволяют проверить как поведет себя код в этих условиях.

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

Не понятно как получается так, что рефакторинг не представляет проблем.


Возьмём кеш, который повсеместно в приложении используется и положим у него есть метод “bool TryGetCachedValue(string key, out T value);” Представим себе рефакторинг — сигнатура этого метода меняется на “ItemState TryGetCachedValue(string key, out T value);“ Положим кеш не простой, а write thru, то есть в нем, кроме самих данных есть ещё соединение с БД и TCP соединение с парой других микросервисов. Из-за этой особенности кеш замокан в каждом тесте.


Без тестов у нас было M мест, где нужно поменять код, в связи с изменением сигнатуры TryGetCachedValue. С тестами появилось ещё N мест. Если N много больше M, то будем говорить, что моки затрудняют рефакторинг. Кажется N всегда больше M.


Эти рассуждения находят подтверждение в моей практике. Часто видел в проектах «комбинаторный взрыв» количества изменений из-за моков. Для меня теория и практика сошлись и я от моков и unit тестов отказался (не от тестов вообще).


Как вы решаете проблему с большим каскадом изменений в тестах, после изменений вроде описанного выше?

Тут вопрос, что вы называете моками и что юнит тестами.


Вот терминология у Фаулера моки


Моки
  • Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
  • Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
  • Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
  • Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
  • Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

Юнит тесты


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


Так же см разные паттерны у Шора. Например, использование signature shielding.


Еще можно сохранить совместимость с boolean либо просто добавив новый метод вместо переделки существующего, либо сделав imlicit cast к булеан. Последним, правда, я не пользовался, если у кого-то есть какой-то опыт, было бы интересно узнать.

Тут вопрос, что вы называете моками и что юнит тестами.

Опираюсь на то, с чего начал автор.
Я определяю юнит тестирования как тестирование одного продакш юнита в полностью контролируемом окружении.

Продакшн юнит — это обычно класс,…

Полностью контролируемое окружение — это окружение имитирующие среду,… Поведение окружения задается для конкретного тестового кейса через лаконичный API и любое поведение вне этого кейса для него не определено.


Исходя из этого, я делаю вывод, что а) unit test пишется для одного класа b) все вокруг него мокается с помощью чего-то, похожего на Moq. Это именно та среда, от корой я, в своем время, уходил. Но автор явно топит за этот подход, поэтому мне и интересно.

Если для вас приемлемы sociable юнит тесты с фейками

Я так и делаю, типа такого получается
//Юзер продает крипту боту, а он перепродает другой бирже, 
//полученные от юзера ордера бот помещает в архив. 
//Архив это просто очередь на запись в базу.
//Нужно проверить, что бот переходит в состояние "ошибка" если 
//он получил ордер, но архив перегружен и сохранить ордер не получится.
[Test]
public void GoesToRecoveryOnArchiveOverload()
{
    //arrange
    var order = SampleData.Order1;
    OrderArchive.Overload = true;
    
    //act - notify bot about trade with a user
    PrimaryAccount.AddOrder(order.MakeTrade(0.5m, out _));

    //assert - проверить, что бот перешел в нужное состояние.
    InputEvents.WaitItem(_ => RiskManager.State.Should().Be(RiskManagerBotState.ErrorRecovery));
}


В этом тесте, наверное 50% всей системы задействовано — очереди сообщений, коннекторы к бирже, всякие кеши и in-memory представление аккаунта и рыночных данных и т.п. Тестовыми является только коннектор к бирже, «PrimaryAccount.AddOrder» это обертка над ним, которая генерирует нужное событие.

Еще можно сохранить совместимость с boolean либо просто добавив новый метод вместо переделки существующего, либо сделав imlicit cast к булеан.

Не надо так делать, будет больно. Оно начнет в boolean превращаться там где не ожидаешь, а увидеть это глазами в коде невозможно.
Очень сложно ответить на вопрос, с данным примером. Формально вы правы, мест для изменения больше. Вот только, в практике я с тами мало встречался. Если это рефакторинг снижающий технический долг, то он направлен на упрощение юнита, однако переход от boolean к ItemState увеличивает complexity. Если у вас рефакторинг обусловлен бизнес задачей, то скорее всего решение будет сложнее, больше юнитов будет вовлечено, но каждый юнит сам по себе должен быть простым и укладываться в 1-9 тест кейсов.

Как вы решаете проблем у с большим каскадом изменений в тестах, после изменений вроде описанного выше?

Стараюсь избегать комбинаторных взрывов при рефакторинг. Существуют практики, которые позволяют решить данную проблему. Основная идея — Не революция, а эволюция маленькими шагами. Можно создать ICache2 с новым методом, и не спешно переводить код на использование новой сигнатуры.

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

Вероятнее всего, написание тестов позволило избежать возникновения некоторых багов, которые вылезли бы, если тесты не были бы написаны. Когда пишешь тест, автоматически задумываешься о входных данных юнита, например.

Любой рефакторинг превращается в борьбу с тестами, время на рефакторинг увеличивается в разы.
Вы страшный человек, как представлю рефакторинг без тестов… Если пишете без тестов, это подразумевает отсутствие рефакторинга, написал и забыл. А любой рефакторинг — это гарантированная ошибка в логике, которая может быть отловлена тестами, если они есть, ну или заказчиком, в итоге. Собственно перед рефакторингом легаси-кода (да-да, вы пишете легаси, вы тот самый «боже, что за идиот это писал», вам должно икаться периодически) необходимо обложить его тестами, чтобы ничего не поломать, в любой книжке по рефакторингу этот пунктик чётко прописан.

«Я не пишу юнит-тесты с моками» и «я не пишу тесты» это разные вещи.

Постарайтесь не применять наследование. Вместо него используйте композицию зависимостей. Часто наследование применяют для реализации принципа DRY (don’t repeat yourself), вынося общий код в родителя, но тем самым нарушая принцип KISS (keep it simple stupid), увеличивая сложность юнитов.

Не понятная идея — «не делайте так». А как делать?
По моим наблюдениям, наследование также часто применяют для реализации отношения «является» при построениии иерархии классов. Вы предлагаете отказаться от иерархии?
Использовать композицию из различных сервисов, которые объявляются как зависимости юнита.

Для примера: скажем есть контроллеры и часть из них используют проверку прав пользователя, можно сделать BaseController и наследники через protected функцию будут проверять права пользователя. Я так предлагаю не делать.

Вместо этого, общий код выноситься в сервис и каждый контроллер, которому этот код нужен объявляет от него зависимость.

А почему


вынося общий код в родителя, но тем самым нарушая принцип KISS (keep it simple stupid)

Мы нарушаем принцип KISS (keep it simple stupid)? Насколько я понимаю, если следовать Принципу подстановки Барбары Лисков, то все будет просто и наследование очень даже хорошо, или я ошибаюсь?

Вы не ошибаетесь, наследование рабочий инструмент и принцип очень важен. Я говорю о случае, когда наследование вводиться из-за желания переиспользовать код. Вариант с отдельным сервисом даст более «чистый» код.
Sign up to leave a comment.

Articles