Pull to refresh

Comments 223

Годно. Про Атомарность тестов ещё не мешало бы упомянуть. И листинги кода картинкой это сильный изврат в то время, когда Хабр уже давно умеет нормально вставлять и подсвечивать код.
А какие есть хорошие практики рефакторинга моков? Когда, например, метод, который мокаем, меняет возвращаемое значение (для примера — возвышает не количество секунд, прошедших с какого-то события, а UNIX время события — сигнатура не меняется, но смысл меняется). В этом случае придется искать все места, где используется мок класса, что сильно повышает когнитивную нагрузку
Я таких практик не знаю. Кажется вы правы, и придется искать вызовы этого метода у моков и руками менять возвращаемое значение.
А вообще логичнее было бы в таком случае сделать новый метод, а не менять возвращаемое значение в старом.
Когда, например, метод, который мокаем, меняет возвращаемое значение (для примера — возвышает не количество секунд, прошедших с какого-то события, а UNIX время события — сигнатура не меняется, но смысл меняется).

Это антипаттерн. В таких случаях лучше форсировать изменение сигнатуры:


  1. Завести специальный тип для даты-времени (в идеале)
  2. Добавить новый метод
  3. Пометить старый как deprecated (если больше не нужен)
  4. Исправить все предупреждения компилятора
  5. Выпилить старый метод (не раньше чем через релиз)

Использование в качестве моков анонимных классов не может помочь? Они должны быть более восприимчивы к такого рода рефакторингу, хоть и не во всем так удобны как моки.

Если вы в курсе правила Open/Closed и следуете ему, то такие ситуации будут редки.
Файлы копятся, база обрастает кучей мусора и в системе появляются толпы фейковых пользователей. Старайтесь сохраняйте в чистоте не только своё рабочее место, но и рабочее окружение.

Неужели вы работаете с БД, файловой системой напрямую в своем тестируемом модуле? Непонятна ситуация, когда в базе появляются толпы фейковых юзеров. Если они появляются, значит вы тестируете модуль создания/регистрации пользователей, а не модуль для работы с базой данных. Почему тогда не использовать мок низкоуровнего модуля?
Неужели вы работаете с БД

А почему бы и нет? Поднять прямо из теста H2, и в путь. Очень удобно, итог работы можно вынимать обычным селектом, что наиболее близко к боевой работе.
Я про работу с инфраструктурным уровнем не в тесте, а в самом тестируемом модуле. Если модуль регистрации должен отправлять нотификацию в виде email`а, то не думаю, что при запуске вашего теста должны уходить письма, скорее вы используете в своем модуле сервис из инфраструктуры, который при unit тестировании надо замокать и ожидать, что метод отправки будет вызван. Так же схема и для базы данных.
Для тестирования «близко к боевой работе» используются другие тесты. Unit тесты тестируют unit и только его в отрыве от всего остального. Разделяй и властвуй.
7 правил хорошего тона при написании Unit-тестов

Создали файл, сделали запись в базу или дернули ручку создания пользователя?

Что???
Если не заткнуть моками со всех сторон, то именно так все и происходит.
Тогда это не Unit тесты
Каюсь Допишу в статью, что этот пункт относится только к интеграционному тестированию.
8) Не используйте тестируемый код (SUT) частично или полностью в самих тестах для вычисления «правильного» ответа насколько это возможно.
Пишите осмысленные сообщения на случай падения теста.

Для юнит-теста это лишнее. Необходимо и достаточно иметь название метода и результат (прошел-упал). Если для вашего теста этого мало, то ваш тест либо проверяет слишком много и должен быть разбит на несколько тестов, либо не является юнит-тестом и не относится к заявленной теме.


Чаще используйте заглушки (моки) вместо реальных объектов

А где про разницу между стабами и моками и правило "моков не должно быть больше одного"?


Создали файл, сделали запись в базу или дернули ручку создания пользователя?

Это не юнит-тесты по определению


При этом не пришлось пересоздавать CalendarService,

Это грубое нарушение автономности тестов, что для юнит-тестирования недопустимо.

> 4. Чаще используйте заглушки (моки) вместо реальных объектов.

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

Я бы добавил, что для сущностей и объектов-значений и разного рода моделей скорее не стоит использовать моки.
Для сервисов наоборот, инфраструктурных вещей наоборот, лучше мочить.

> Для сервисов наоборот, инфраструктурных вещей наоборот, лучше мочить.

Безусловно. Сложность настройки внешнего тестового окружения обычно не то что сравнима — превышает сложность настройки моков. А по-этому в данном случае моки вполне полезный инструмент — то есть это как раз тот случай, когда «не обойтись».
они ведут к резкому росту сложности тестового кода и снижению качества самих тестов

Мнение, агрументированное… другие мнением.
Между тем, о каком качестве юнит-тестов может идти речь, если тестируемый объект не будет изолирован от остального приложения?

> Мнение, агрументированное… другие мнением.

Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов. При этом без моков этого кода бы просто не было.

> Между тем, о каком качестве юнит-тестов может идти речь, если тестируемый объект не будет изолирован от остального приложения?

Назначение тестов — поиск ошибок. Количество ошибок, которые отловил ваш тест, деленное на затраты для написания теста — это и есть прямая оценка качества данного теста.

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

Если у класса плохая тестируемость, это проблема не моков, а проектирования. Обычно один из двух вариантов: или тестируемый объект имеет более одной ответственности (и кучу зависимостей как результат) или не имеет определенной ответственности вообще (видимо, ваш случай — хорошо тестируемый класс разбит на несколько плохо тестируемых).
Фейки в данном случае полезны, так как выявляют плохой запах кода.
В любом случае, если ваш тест проверяет более одного объекта — это НЕ юнит-тест по определению.

> Если у класса плохая тестируемость, это проблема не моков, а проектирования.

Да нет никакой такой проблемы. Если у вас есть пара десятков зависимостей — вам их надо настраивать. Тот факт, что вы раскидаете условные 100 строк кода по 10 методам — никак не отменяет того, что это сумме те же сто строк.

> В любом случае, если ваш тест проверяет более одного объекта — это НЕ юнит-тест по определению.

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

Если у вас у одного класса есть пара десятков зависимостей — у него многовато ответственностей.

Если вы разобьете класс и снизите число зависимостей, то это никакого влияния на результат не окажет, т.к. эти зависимости просто будут устанавливаться _в других_ тестах. Вы перенесли код из одного места в другое. Да, это может в итоге привести к некоему упрощению (хоть и не всегда), но проблемы не решает — как куча лишнего кода была, так и осталась.

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

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


как куча лишнего кода была, так и осталась.

Как вы делите код на "лишний" и "нелишний"?

> Как вы делите код на «лишний» и «нелишний»?

Тот код, который не решает каких-то конкретных задач (добавляет новый функционал, улучшает качество архитектуры, увеличивает простоту поддержки) — лишний.

> Окажет. Проблема-то возникает только тогда, когда мы вынуждены для теста сетапить те зависимости, которые к тесту отношения не имеют.

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

Ну так моки упрощают поддержку тестов. По этому критерию они, очевидно, не лишние.


Проблема возникает, когда много зависимостей.

А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.


Я в своем опыте пока не видел класса, у которого реально была бы одна ответственность и при этом много зависимостей. Исключение — фасады и роутеры, но их, будем честными, юнит-тестировать можно в последнюю очередь (а composition root вообще можно не юнит-тестировать).

> Ну так моки упрощают поддержку тестов.

Каким образом? Надо тратить лишнее время на поддержку моков и их синхронизацию с реальным поведением зависимостей. Чем это проще?

> А много зависимостей (обычно) возникает тогда, когда много ответственностей. О чем и речь.

Вообще-то все наоборот. Количество зависимостей растет при декомпозиции. Чем меньше делает каждый отдельный класс — тем больше у него зависимостей, т.к. вместо выполнения работы самостоятельно, класс ее делегирует зависимости.

И, наоборот, чем больше ответственности — тем меньше зависимостей, с god-object в пределе, который делает все и у которого практически нет зависимостей.
Каким образом?

Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.


Чем меньше делает каждый отдельный класс — тем больше у него зависимостей.

Это, простите, как? У класса, который не делает ничего, осмысленных зависимостей быть не может.

> Это, простите, как? У класса, который не делает ничего, осмысленных зависимостей быть не может.

Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.

> Таким, что в каждом тесте сетапится только то, что нужно для его выполнения.

А в случае интеграционных — вообще обычно не сетапится. Сетап зависимостей для интеграционного теста — исключительная ситуация. Для сьюитов — ну да, бывает, хоть и не слишком часто.
Если он сам не делает ничего, то он делегирует кому-то всю полезную работу.

Нет, если он не делает ничего, то он и не делегирует. Делегирование — тоже работа (в программировании, по крайней мере).


А в случае интеграционных — вообще обычно не сетапится.

Это очень опасная иллюзия. Вы просто переиспользуете какой-то существующий сетап, со всеми опасностями антипаттерна shared fixture.


Сетап зависимостей для интеграционного теста — исключительная ситуация.

Ну да, БД с правильными данными, или мок этой БД — они сами собой возникают.

> Ну да, БД с правильными данными, или мок этой БД — они сами собой возникают.

Еще раз, _сетап для теста_. Глобальный сетап возникает не сам, он пишется. Иногда он правится под тест-сьюиты — и очень редко под конкретные тесты.
Глобальный сетап возникает не сам, он пишется.

И на его поддержку тоже нужны усилия.


и очень редко под конкретные тесты.

Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше "давайте зададим свой кейс в каждом тесте"?

> И на его поддержку тоже нужны усилия.

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

> Значит, ваш сетап должен (заранее) содержать кейсы под все тесты. И чем это лучше «давайте зададим свой кейс в каждом тесте»?

Да нет, конечно. Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей? Тогда, конечно, надо делать много сетапов. Если же это не так, вам одного сетапа хватит на много-много тестов.
Но благодаря тому, что нет многократного дублирования, их на порядок меньше.

Так не надо же (беспорядочно) дублировать код — тесты тоже прекрасно рефакторятся.


Я понял проблему, у вас, видимо, поведение зависимых объектов существенно зависит от поведения зависимостей?

У меня тестовая проверка зависит от поведения зависимостей (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).

> У меня тестовая проверка зависит от поведения зависимостей

Так если поведение метода не меняется из-за работы зависимостей, то вы и не сможете выделить тесткейз.

> (самый тривиальный пример — API, читающий объект. Очевидно, он зависит от того, что в хранилище).

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

"Поведение метода" всегда одинаковое — "правильно смапить".


Результат зависит, а поведение?

А поведение всегда одинаковое — "правильно смапить".


Вот только критерии "правильно" — они сложные. Где-то объект плоский, где-то многоуровневый, где-то ссылки, где-то включения. Для всего этого в БД надо построить исходные данные.


Тоже банальный пример: у объекта есть коллекция вложенных объектов (например, строчки в заказе). Мы должны проверить, что если в заказе нет ни одной строчки, возвращается именно пустая коллекция, а не null (ну нет у нас null-safety в языке). Вот, надо уже два заказа в БД завести.

> Где-то объект плоский, где-то многоуровневый, где-то ссылки, где-то включения. Для всего этого в БД надо построить исходные данные.

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

Все равно там будет столько объектов, сколько у меня тестовых сценариев. И выгоды по сравнению с "объяви объект прямо в тесте" нет.


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

Просто считайте, что я тестирую не "читающее апи", а "маппящий метод". Аргументация не меняется.

> Все равно там будет столько объектов, сколько у меня тестовых сценариев. И выгоды по сравнению с «объяви объект прямо в тесте» нет.

Конечно же, есть. Вы объявляете все это только один раз (а не в каждом тесте).

> Просто считайте, что я тестирую не «читающее апи», а «маппящий метод».

Мапящий метод в бд не лезет, он о ней не в курсе. Он принимает какие-то данные и возвращает смапленные данные. Пишется он одинаково, что с моками, что без моков, просто потому что зависимостей не имеет (по крайней мере от бд, остальные мы на данный момент не обсуждаем).
Вы объявляете все это только один раз (а не в каждом тесте).

Еще раз, по буквам. В каждом тесте я объявляю тот объект, который ему нужен. Один объект на тест. Суммарно k объектов. Я не объявляю все остальные объекты они мне не интересны.


В случае глобального сетапа я объявляю те же k объектов, но в одном месте. Кода ровно столько же.


Мапящий метод в бд не лезет, он о ней не в курсе.

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

> Еще раз, по буквам. В каждом тесте я объявляю тот объект, который ему нужен. Один объект на тест. Суммарно k объектов. Я не объявляю все остальные объекты они мне не интересны.

Давайте подведем некоторый итог:

1. В вашем коде неким образом получается избегать большого количества зависимостей, не нарушая SRP, в моем — нет (например — у вас бд, бизнес-логика содержится в сервисах, их много, они разделены согласно доменной логике, какой-нибудь метод-двустрочник может использовать полдесятка разных зависимостей, как вы тут будете количество зависимостей снижать?)
2. У вас есть всякий indirect output, я его по возможности избегаю и стараюсь тестировать при помощи черного ящика.

Возможно, в _вашей_ ситуации юнит-тесты имеют смысл, в моей — очевидно, не совсем так.
какой-нибудь метод-двустрочник может использовать полдесятка разных зависимостей, как вы тут будете количество зависимостей снижать?

Что-то не так с таким методом-двухстрочником. Выделять логику дальше.


Возможно, в вашей ситуации юнит-тесты имеют смысл

В моей и аналогичных, да. Это как раз те ситуации, на которые ориентированы (и которые порождают и порождаются) юнит-тесты.

> Выделять логику дальше.

А некуда выделять. Все, что делает этот метод — дергает 5 других методов. Конечно, можно, например, разбить его так, чтобы было 2 метода по два вызова и один скомбинированный с тремя. Причем каждый из новых этих методов будет полностью бессмысленным, да и количество зависимостей тут не изменится.
Все, что делает этот метод — дергает 5 других методов.

И все пять из них нужны для выполнения одной ответственности? Значит, у вас типичный координатор. Координаторов в системе мало.

> И все пять из них нужны для выполнения одной ответственности?

Да, для вполне конкретной и определенной задачи.

> Координаторов в системе мало.

Ну вот в моей практике все строго наоборот.
> а метаинформацию о том, как именно делается маппинг, берет из хранилища.

Ну вот у вас и нарушение SRP — вы в одном методе и в хранилище лезете, и какую-то логику мапинга реализуете. Кто еще этот мапинг разбирает и смотрит, как по нему, с-но, мапить?
Ну вот у вас и нарушение SRP — вы в одном методе и в хранилище лезете

Нет, не нарушение. Я всего лишь вызываю _metadataProvider.GetMappingMetadataFor(someId), а за общение с "реальным" хранилищем отвечает провайдер.

> Нет, не нарушение.

С моей точки зрения — нарушение. У вас в одном методе смешана логика и доступ к хранилищу.

Если _metadataProvider.GetMappingMetadataFor(someId) — это доступ к хранилищу, то _mapper.Map(request, mappingMetadata) — это логика маппинга, тогда метод


Request Map(Request request)
{
  return _mapper.Map(request, _metadataProvider.GetMappingMetadataFor(request.Type));
}

все равно имеет две ответственности, и так далее вверх по стеку.


(Вообще, для проверки SRP иногда удобнее считать "причины для изменения", а не "ответственности".)

Ну вот у вас метод _mapper.Map, его и тестируйте для проверки логики маппинга. Зачем вам при этом мокать _metadataProvider?

То есть метод Request Map(Request), приведенный мной выше, тестировать не надо?

Конечно, надо — достаточно одного единственного теста, который вытягивает некоторые данные из бд и применяет некоторый маппинг. Если получилось — метод работает правильно. В итоге вам одного тестового сетапа и достаточно (о чем я выше говорил). А если вы хотите проверить именно правильность мапинга (или правильность взаимодействия с БД) — вы проверяете маппер (или провайдер), потому что это их ответственность, а не ответственность Request Map(Request).

Можно сказать тут, что любой тест для Request Map(Request) будет автоматически интеграционным, потому что сам метод единственное что делает — это обеспечивает интеграцию между двумя модулями, это метод-клей. Если вы в этом методе изолируете зависимости — то вы просто ничего не протестируете, за отсутствием какой-либо логики в данном методе.
Конечно, надо — достаточно одного единственного теста, который вытягивает некоторые данные из бд и применяет некоторый маппинг.

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


Если вы в этом методе изолируете зависимости — то вы просто ничего не протестируете, за отсутствием какой-либо логики в данном методе.

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


А еще есть обработка ошибок, которую мы традиционно в примере пропустили, но которая есть — и за которую потом отрывают руки. Вот банальный пример: IMapper.Map ожидает, что переданный на вход маппинг — не null (повторюсь, я исхожу из того, что у нас язык без управления nullability; в противном случае замените все на Option[T]). А IMetadataProvider.GetMappingMetadataFor может вернуть null (он используется другими местами, которые на это опираются). Чья ответственность проверить этот null? Точно не маппера, он только guard на входе может поставить (и если мы эту ошибку прокинем вверх, получим нечитаемое сообщение). Значит, нам нужен либо (еще один) враппер вокруг IMetadataProvider.GetMappingMetadataFor, либо проверка внутри нашего SUT — и в любом случае этот кейс надо покрыть.

> Только вам для этого надо знать, что БД — это неявный вход для этого метода.

Как я могу этого не знать, если согласно спецификации этот метод использует мапинг из бд?

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

И один единственный тест эту логику проверит. Вы же выше речь вели о том, что вам надо много сетапов, т.к. разные мапинги. Но в данном случае валидность метода от того, какой мапинг лежит в бд, вообще никак не отличается, если вы и напишете кучу тестов с разными мапингами (которые будут возвращаться из моков), то если один тест прошел — остальные практически гарантированно пройдут. Зачем тогда эти тесты нужны, и почему бы не перенести их в тестирование маппера, где это будет проще (т.к. туда все что надо передается рпотсо аргументом, это всегда удобнее чем настраивать моки, согласитесь)?

> Значит, нам нужен либо (еще один) враппер вокруг IMetadataProvider.GetMappingMetadataFor

Вроде, вы сами ответили на свой вопрос?
Как я могу этого не знать, если согласно спецификации этот метод использует мапинг из бд?

Нет, согласно спецификации этот метод использует маппинг, предоставленный провайдером (если мы говорим о спецификации на компонент, а не на приложение). И это легко понять, если представить себе, что провайдер грузит маппинги из БД в память на старте приложения.


Зачем тогда эти тесты нужны, и почему бы не перенести их в тестирование маппера, где это будет проще (т.к. туда все что надо передается рпотсо аргументом, это всегда удобнее чем настраивать моки, согласитесь)?

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


Но, еще раз обращу ваше внимание: в итоге от "не надо мокать зависимости" вы перешли к "давайте делать как можно меньше зависимостей в том коде, который нуждается в обширном тестировании".


Вроде, вы сами ответили на свой вопрос?

Там не было вопроса (кроме риторического), там был пойнт, что эти кейсы тоже надо тестировать (и они снова требуют знания indirect inputs).

> Нет, согласно спецификации этот метод использует маппинг, предоставленный провайдером

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

> Но, еще раз обращу ваше внимание: в итоге от «не надо мокать зависимости» вы перешли к «давайте делать как можно меньше зависимостей в том коде, который нуждается в обширном тестировании».

Ну так вы тоже перешли от «надо мокать все зависимости» к «функциональные можно и не мокать» :)

> Там не было вопроса (кроме риторического), там был пойнт, что эти кейсы тоже надо тестировать (и они снова требуют знания indirect inputs).

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

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


Может быть, я вообще не буду использовать в этом методе указанный провайдер?

Архитектор против.


Так если обработать ошибку и выкинуть нужное исключение — это ответственность обертки (или и вовсе провайдера, почему нет? он же с хранилищем взаимодействует), то почему надо делать эти тесты на Map? Тестируйте обертку.

Вот только надо протестировать, что SUT использует обертку, а не провайдер, да же?


(точнее, надо протестировать, что SUT в ответ на реквест, для которого не определен маппинг, бросает конкретную строго определенную ошибку, а вот остальное — детали реализации)

> Архитектор против.

Я думаю, в таком ключе смысла обсуждать, что и как тестировать, нет, т.к. архитектор вам также скажет и моки, или не моки. Безусловно, что выбор оптимальной методологии тестирования будет зависеть от архитектуры, это факт самоочевидный. Если архитектура фиксируется неким «архитектором», то это и на способ тестирования автоматически наложит ограничения.

> Вот только надо протестировать, что SUT использует обертку, а не провайдер, да же?

SUT не сможет использовать провайдер, если не содержит зависимостей от провайдера :)
Безусловно, что выбор оптимальной методологии тестирования будет зависеть от архитектуры, это факт самоочевидный.

Ура. Так вот, есть архитектуры, которые удобно и выгодно тестировать с моками.


SUT не сможет использовать провайдер, если не содержит зависимостей от провайдера

Муа-ха-ха. Надо протестировать, что он не получает их скрытым образом. Черный ящик же, да?

> Муа-ха-ха. Надо протестировать, что он не получает их скрытым образом.

Это тестировать не надо. Это ограничение архитектуры :)

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

> Ограничения архитектуры тоже надо тестировать (до тех пор пока они не гарантируются чем-то, что вам неподконтрольно, конечно).

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

Проблема в том, что в достаточно большом проекте у разработчика всегда найдется "разумная причина" сделать не так, как от него ожидают. В том числе — нарушить изоляцию.

> Проблема в том, что в достаточно большом проекте у разработчика всегда найдется «разумная причина» сделать не так, как от него ожидают.

Если разумная причина есть (без кавычек), то это вполне можно сделать.

… вот только тест про это ничего не знает (черный ящик же), и все посыпалось.

> … вот только тест про это ничего не знает (черный ящик же), и все посыпалось.

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

Он рабочий (в том смысле, что система в целом работает). Просто тесты упали.

> Он рабочий (в том смысле, что система в целом работает). Просто тесты упали.

Так это же плохо, если тесты падают, когда не надо?

Конечно, плохо. Осталось понять, как отделить "надо" от "не надо".

> «не надо»

Когда нет ошибок (работа программы соответствует функциональным требованиям).

Люди, отвечающие за дизайн, в печали.

> Ура. Так вот, есть архитектуры, которые удобно и выгодно тестировать с моками.

Да с этим вобщем-то никто и не спорил. Обычно под рекомендацию можно почти всегда построить ситуацию, в которой она неверна (или верна), максим в программировании практически не существует.
Да все абсолютно, кто когда-либо писал юнит-тесты, встречались с адом, когда настройка моков занимает 3/4 самих тестов.

Все, кто когда-либо писал код, встречался с адом. Это повод код не писать?


При этом без моков этого кода бы просто не было.

А протестировать при этом все еще можно?

> А протестировать при этом все еще можно?

А почему бы нельзя? Пишете тот же самый тест, что с моками, только без моков. В итоге затрат меньше (т.к. не надо тратить время на моки), а результат — лучше (т.к. поймано больше багов).
А почему бы нельзя? Пишете тот же самый тест, что с моками, только без моков.

Вот у меня есть простенькая преобразовалка: вход — конверсия по метаданным из БД — выход. И штук сорок текст-кейсов вида "вход — метаданные — ожидаемый вход". Как мне это удобно сделать без моков?


Заодно как мне проверить без моков, что мой код ведет себя верно, когда зависимость (а) отдает неверные данные (б) отдает ошибку (ц) зависает?


а результат — лучше (т.к. поймано больше багов).

А почему поймано больше багов?..

> вход — конверсия по метаданным из БД

Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать, это попадает под «использовать только в тех случаях, когда без них не обойтись».

> А почему поймано больше багов?..

Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив. В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов. Кроме того, вы просто можете забыть обновить моки, в итоге с реальными зависимостями тест падает (т.к. их поведение изменилось), а с моками — проходит. Естественно, может быть обратная ситуация — когда несколько багов, взаимонакладываясь друг на друга, в итоге дают правильное поведение — но это очень большая редкость по сравнению с предыдущими двумя пунктами
Давайте сразу определимся — внешнее окружение (бд, удаленные сервисы и т.д.), естественно следует мокировать

Вот только преобразовалка смотрит не в БД, а в сервис, поставляющий метаданные в удобном ей формате. Мне мокировать БД или этот сервис? И почему?


И еще — локальные зависимости ошибаться не могут?


В первом случае выполняется только код непосредственно тестируемого модуля, а во втором случае — и код всех используемых зависимостей, то есть один интеграционный тест заменяет 1+количество_используемых_зависимостей юнит-тестов.

А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно). Мы поменяли Ц. Какие тесты нам надо запустить, чтобы быть уверенными, что мы ничего не сломали? А теперь представьте, что наш сосед написал модуль Г, зависящий от Ц, на который он решил тесты не писать, потому что "модуль тривиальный".


Или вот наоборот: модуль Х зависит от У. У, в свою очередь, зависит от внешних зависимостей З1, З2 и З3. Согласно вашему же правилу, эти зависимости надо мокировать. Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У). Теперь наш сосед добавляет в У зависимость З4. Что происходит? Правильно, падают тесты на Х, хотя казалось бы.

> Мне мокировать БД или этот сервис? И почему?

Бд. Потому что затрат меньше, чем в случае использование тестовой бд. Если в вашем случае тестовая бд дает меньше затрат (что бывает редко, но вдруг) — ну тогда, конечно, можно и тестовую бд. Чем ближе тест к реальности — тем он, конечно же, лучше.

> И еще — локальные зависимости ошибаться не могут?

Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.

> А вот теперь смотрите: у вас есть модули А и Б, оба зависят от Ц и Д. Мы решили, по вашей методике, сэкономить тесты, и написали только интеграционные тесты на А и Б (тем самым Ц и Д тестируются имплицитно).

Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков. Просто мы «искаробки» получаем некоторое дополнительное, как вы выразились, «имплицитное», тестирование зависимых модулей, что позволит поймать больше багов в этих модулях. То есть не «меньше тестов при том же результате», а «выше результат при тех же тестах».

> Значит, в вашем тесте на Х есть три мока (З1, З2, З3) вместо одного (У).

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

Стоп-стоп. Вы сравниваете затраты с использованием тестовой БД, а я спрашиваю, мне мокать БД или сервис. Речи о тестовой БД не идет.


Могут, конечно же, но вероятность того, что два модуля согласованно ошибутся так, чтобы съесть ошибку, весьма мала.

Вы снова отвечаете не на тот вопрос. Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?


Конечно же, мы не пишем тесты только на А и Б, мы пишем все те же самые тесты, что писали бы и в случае использования моков.

То есть вы предлагаете писать тесты на А, Б, Ц и Д?


Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.


Так это глобальные моки. Они настраиваются раз и для всех тестов.

Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?


Хуже того, если у вас общий сетап, то он отнесен от теста, и прочитать, что же делает тест — и почему — становится намного сложнее.

> Стоп-стоп. Вы сравниваете затраты с использованием тестовой БД, а я спрашиваю, мне мокать БД или сервис.

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

> Мне надо проверить, что SUT корректно себя ведет, если его зависимость (внутренняя!) ведет себя некорректно. Как это сделать без мока?

Никак. А зачем решать бесполезные задачи?

> То есть вы предлагаете писать тесты на А, Б, Ц и Д?

Те же самые тесты, я же указал.

> Тогда у вас получается больше кода, а не меньше, потому что тесты те же, только еще и для А и Б надо засетапить все, что нужно для Ц и Д.

Сетапить вообще ничего не надо кроме глобального сетапа.

> Так в разных тестах разное поведение нужно, вообще-то. Какие тут глобальные моки?

Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте. В любом случае, это обычно требует в десятки меньше кода, чем на полноценный сетап всех моков.
Я ответил. Мокать БД.

Так почему же не сервис?


Никак. А зачем решать бесполезные задачи?

То есть тоже нужен мок?


Сетапить вообще ничего не надо кроме глобального сетапа.

Глобальный сетап — зло.


Если надо подменить какую-то конкретную часть сетапа — ну не проблема, подменяйте.

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


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

… если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого "в десятки меньше кода".

> Так почему же не сервис?

Потому что лучше мокать только БД, чем и сервис и БД (вы же сам сервис тоже тестировать будете, это ваш код? или я неверно понял вопрос?)

> То есть тоже нужен мок?

Зачем?

> Глобальный сетап — зло.

В чем? Я вижу только добро — экономию при прочих равных.

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

Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.

> … если зависимостей мало, то и моков мало. А если моков мало, то их сетап — это ровно то же самое, что подмена в глобальном. Так что никакого «в десятки меньше кода».

Но на практике их много, либо у вас у god-object'ы вместо классов.
Потому что лучше мокать только БД, чем и сервис и БД

А почему лучше?


Зачем?

Чтобы протестировать описанные кейсы.


В чем?

Антипаттерн shared fixture. Сетап не виден в тесте, возникают неявные зависимости между тестами, могут быть проблемы при конкурентном выполнении и так далее.


Зачем для этого какая-то особая инфраструктура? Подмена куска сетапа не сложнее, чем его определение.

Покажите пример, пожалуйста.


Но на практике их много, либо у вас у god-object'ы вместо классов.

Или нет. У меня на практике мало зависимостей (кроме тех классов, где я знаю, что нарушен SRP, и которые стоят в очереди на рефакторинг).

> А почему лучше?

Потому что нет кода, который не выполняет никакой задачи (мок сервиса в данном случае).

> Чтобы протестировать описанные кейсы.

Ну так тестируйте с моком БД.

> Антипаттерн shared fixture.

Почему это антипаттерн?

> Или нет. У меня на практике мало зависимостей (кроме тех классов, где я знаю, что нарушен SRP, и которые стоят в очереди на рефакторинг).

Если у вас весь весь функционал в куче, то, конечно, зависимостей мало. Но мы же о качественном коде говорим?

> Покажите пример, пожалуйста.

Пример чего? Использования операции присваивания? Или вызова методов фреймворка для создания моков?
Потому что нет кода, который не выполняет никакой задачи (мок сервиса в данном случае).

Почему же не выполняет? Он выполняет задачу "подать на вход тестируемому объекту ровно те данные, которые описаны в тесткейсе".


Ну так тестируйте с моком БД.

Мок БД не позволяет протестировать, как поведет себя SUT, зависящий от сервиса, зависящего от БД, при зависании кода сервиса.


Почему это антипаттерн?

В следующем предложении было написано. Ну и у Мезароса тоже.


Если у вас весь весь функционал в куче, то, конечно, зависимостей мало. Но мы же о качественном коде говорим?

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


Пример чего?

Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.

> Он выполняет задачу «подать на вход тестируемому объекту ровно те данные, которые описаны в тесткейсе».

Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).

> В следующем предложении было написано. Ну и у Мезароса тоже.

То, что там написано — по-просту неверно.

> Да, о качественном. И вот у качественного кода в моей практике мало зависимостей

Тогда либо у вас нарушается SRP, либо вам везет.

> Пример того, как вы получаете SUT с глобальным сетапом, в котором заменено поведение одной зависимости.

Я не понимаю что конкретно вы хотите. Чем этот код, по-вашему, должен отличаться от построения сетапа с нуля, кроме того, что часть сетапа оказывается опущена?
Это аргументы метода и внешнее состояние (сервисов, бд, етц.). То, что возвращают те или иные зависимости, данными, конечно, не является, так как мы даже не знаем (и не должны) о том, какие зависимости данный метод тянет (черный ящик же).

А вот и нет. В тесткейсе описана доменная сущность (метаданные Х), а не состояние БД. Соответственно, если у меня есть сервис, который мне эту доменную сущность возвращает, мне проще как раз его замокать, чем думать, как же эта сущность отображается в БД.


То, что там написано — по-просту неверно.

Аргументируйте.


Тогда либо у вас нарушается SRP, либо вам везет.

Видимо, я очень везуч.


Я не понимаю что конкретно вы хотите.

Пример кода.

> А вот и нет. В тесткейсе описана доменная сущность (метаданные Х), а не состояние БД. С

Если это доменная сущность, то она тогда в аргументах. В промежуточном слое ее быть не может, т.к. я просто не знаю ничего об этом промежуточном слое. Я знаю, что мой метод принимает определенные аргументы, лезет в бд, и что-то там возвращает. Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.
То есть если я знаю о том, что метод работает с доменной сущностью — то значит этот метод в базу не лезет, в базу за него полез (и построил потом доменную сущность) кто-то другой и в аргументах передал.

> Пример кода.

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

Нет, это indirect input.


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

Тоже нет. Метод принимает определенные аргументы, и, опираясь на определенные где-то метаданные, что-то возвращает. В тесткейсе метаданные сформулированы в виде доменной сущности, не ее представления в БД.


Я ничего не знаю о существовании (и особенностях работы) каких-либо промежуточных слоев.

Значит, вы тестируете не свой метод. Не мой случай.


Вы можете его увидеть в описании любого фреймворка для моков.

Фреймворки для моков

Фреймворки для моков

Оу, плохо отвлекаться. Читать "фреймворки для моков редко показывают примеры глобальных повторно используемых сетапов". Я, по крайней мере, не видел ни одного. Можно ссылочку?

> Читать «фреймворки для моков редко показывают примеры глобальных повторно используемых сетапов».

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

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

> А вот как мне сделать, чтобы SUT использовал зависимости, которые используют другие зависимости, которые где-то потом неизвестно где используют эти моки?

Эм… Вам надо конфигурировать только те, последние моки. Точно так же, как вы все моки конфигурируете.

Моки надо не только конфигурировать (т.е., определять их поведение), но еще и передать пользователям (т.е., сказать, что их надо использовать).


Когда у меня моки конфигурятся под тест, я прямо в тесте создаю SUT и в качестве зависимостей передаю ему моки. А если у меня глобальный сетап, то мне нужно то ли взять откуда-то готовый SUT, то ли взять откуда-то зависимости, которые ему передать. Откуда?

> А если у меня глобальный сетап, то мне нужно то ли взять откуда-то готовый SUT, то ли взять откуда-то зависимости, которые ему передать. Откуда?

Ну а как вы это делаете обычно? Через IoC-контейнер, я полагаю. Зарегистрировать/заменить моки для теста в IoC-контейнере вы можете откуда угодно.
Ну а как вы это делаете обычно? Через IoC-контейнер, я полагаю.

В тестах? Конечно, нет: просто создаю SUT напрямую, передавая зависимости параметрами.


Зарегистрировать/заменить моки для теста в IoC-контейнере вы можете откуда угодно.

Вот этот IoC контейнер, со всей его настройкой, и есть "инфраструктура для глобального сетапа", о которой я говорил.

> Нет, это indirect input.

Тогда я о ней не могу знать.

> Значит, вы тестируете не свой метод.

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

Тогда вы не можете протестировать.


Нет, просто я не хочу привязывать тесты к реализации. Тесты проверяют спецификацию, спецификация не зависит от реализации.

В спецификации написано "given mapping defined as follows", и дальше домен. Куда бы вы это ни записали — все равно будет привязка к реализации, просто в одном случае — к доменной модели, а в другом — к БД.

> В спецификации написано «given mapping defined as follows»

Я буду передавать эти мапинги аргументом в данном случае.

Технические ограничения не позволяют (я не зря про request filter написал).

> Технические ограничения не позволяют (я не зря про request filter написал).

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

А вот так. SUT — это компонент, находящийся в request pipeline (например, WebAPI), у которого на входе запрос (и только запрос), и на выходе — тоже запрос (преобразованный как хочется). Соответственно, эти ваши два метода — это хорошо, но они окажутся внутри SUT.


(это кстати, иллюстрация к вашему "тестировать как черный ящик")

> Соответственно, эти ваши два метода — это хорошо, но они окажутся внутри SUT.

Если по смыслу они должны быть снаружи — то кто вам мешает их вынести?

Контракт SUT мне мешает. Используемый фреймворк требует, чтобы фильтры в конвеере имели сигнатуру Request ProcessRequest(Request) — соответственно, у SUT сигнатура строго такая же.


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


Собственно, когда у вас контракт SUT строго определен требованиями, и логика завязана на информацию, не поступающую во входных данных, вам придется иметь indirect inputs, вы никуда не можете от этого деться.

> Контракт SUT мне мешает.

Как он может мешать, если мы говорим о внутренней реализации функции? Сигнатура у нее та же самая, данные она принимает и возвращает те же. Просто логика мапинга выделена в метод, который мы и тестируем. Тестировать же надо логику, а не БД или работу моков.
Просто логика мапинга выделена в метод, который мы и тестируем.

Вот так взяли и заменили один SUT на другой. Не надо так, первый SUT остался непротестированным.

"Indirect input" Явное всегда лучше неявного в отрефакторном коде их быть не должно

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


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

> (ну то есть за исключением «чистых» функциональных зависимостей, конечно, но их тоже на моей памяти не мокают, ибо незачем, а значит, тоже за пределами обсуждения)

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

А эти ваши зависимости — "чистые" функциональные? Никаких побочных эффектов и полная детерминистичность?

Там методы генерируют спецификации. Исполняются спецификации уже отдельно.

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

Так мы в итоге и пришли к предлагаемому мной изначально варианту — мокать только внешние зависимости, т.к. все остальные — могут (и должны) быть функциональными.

Ну вот в моем опыте внутренних зависимостей с поведением чистой функции — подавляющее меньшинство (наверное, следствие ООП?). Поэтому ваш вариант получается неприменим.

> Ну вот в моем опыте внутренних зависимостей с поведением чистой функции — подавляющее меньшинство

Так это уже вопрос исключительно вашей архитектуры. Никто же вам не мешает максимально выделять логику в чистые функции.

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

"Мешает" используемая парадигма и принятые архитектурные соглашения.


Кроме того, не совсем понятно, почему чистые зависимости мокать не надо, а «грязные» — надо. Почему такое разделение?

Не "не надо", а незачем. Их поведение (если они покрыты тестами) предсказуемо, и они чаще всего не вносят собственный эффект в тест. Грубо говоря, никто же не мокает string.Split? (ну, я надеюсь)


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

> Не «не надо», а незачем. Их поведение (если они покрыты тестами) предсказуемо, и они чаще всего не вносят собственный эффект в тест.

Давайте немного уточним, вот есть метод, у него нет зависимостей. Теперь мы какой-то из аргументов перенесли в поле класса (сделав зависимостью). В каком случае эта зависимость будет «функциональной», а в каком — нет?

Ну то есть было void Do(x arg), стало void Do() и x _dependency? Первый и самый важный вопрос — а какого типа arg?

> Ну то есть было void Do(x arg), стало void Do() и x

Ну не обязательно void, может и что-то другое возвращать.

> Первый и самый важный вопрос — а какого типа arg?

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

… а что вообще в вашем примере зависимость?


Потому что изначально я подумал, что "зависимость" — это то, что было arg, а стало _dependency. И в этом случае то, зависимость ли оно, зависит (извините) не от того, где оно (в параметре или в филде), а от того, какого оно типа. Если это сервис, то оно всегда зависимость (не важно, как она вбрасывается), если это значение, то оно никогда не зависимость.


Так что давайте на примере хотя бы двух компонентов:


IMetadataProvider {Mapping GetMappingFor(string requestType)}
IRequestMapper {Request MapRequest(Request request, ?)}

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


IMetadataProvider.GetMappingFor, очевидно, не чистая зависимость.

> IMetadataProvider.GetMappingFor, очевидно, не чистая зависимость.

Потому что он тянет значение из какого-то хранилища, верно (этого в типе нет, но по смыслу, вроде, ясно)? А если будет так — GetMappingFor возвращает не сам маппинг, а то, как его достать (ну пусть для простоты и конкретики, это просто строка с sql запросом, чисто для примера). Запускать полученные запросы будет какой-то третий сервис, естественно, на него добавиться зависимость (очевидно, нефункциональная) в маппер. Тогда зависимость на провайдер будет функциональной или нет?
Тогда зависимость на провайдер будет функциональной или нет?

Если то, что возвращает провайдер, всегда зависит только и исключительно от того, что в него передано, провайдер, как зависимость, можно считать чистой функцией.

> Если то, что возвращает провайдер, всегда зависит только и исключительно от того, что в него передано, провайдер, как зависимость, можно считать чистой функцией.

Я бы еще добавил, что это «что-то» надо суметь провалидировать внутренними средствами (а то если лямбду возвращать всегда — то у нас тоже получатся везде формально «чистые» функции, но от самой что ни на есть грязной грязи по факту это отличаться не будет, ведь вы не можете проверить свою лямбду, не запустив ее, со всей грязью).

А по примеру — в итоге получается, что мокать вам придется только «запускатор запросов», то есть это, вобщем-то, и есть БД.
А по примеру — в итоге получается, что мокать вам придется только «запускатор запросов», то есть это, вобщем-то, и есть БД.

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

> то у вас получится хаскель

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

У чисто функциональных зависимостей в хаскеле никакой монады IO нет и не будет по определению.

В хаскеле все зависимости функционально чистые. Грязных функций там в принципе не бывает, их нельзя написать :)
Грязных функций там в принципе не бывает, их нельзя написать

Да ладно?
А как же UnsafePerformIO?
Но и без таких фокусов монада IO позволяет сохранить язык чистым, но явно обозначив зависимость от состояния окружения, совместив "чистую" программу с "грязным" результатом выполнения.
Как результат в хаскеле вопрос с моками решается тривиально — там зависимость от внешнего мира без адских хаков не спрячешь.

> А как же UnsafePerformIO?

А это и не хаскель, unsafePerformIO ломает семантику. Это особенность конкретной реализации.

> совместив «чистую» программу с «грязным» результатом выполнения.

Нету в хаскеле никакого грязного результата выполнения. Когда вы в хаскеле возвращаете IO, то никаких сайд-эффектов не происходит, они происходят при запуске ИО, которое уже к хаскелю не относится, изнутри хаскеля запустить ИО невозможно.
А это и не хаскель, unsafePerformIO ломает семантику. Это особенность конкретной реализации.

Это GHC, стандарт хаскеля де-факто, про него говорить "особенности конкретной реализации" скорее вредно чем бесполезно.


они происходят при запуске ИО

Ну вот мы и пришли к тому с чего начали — IO оказывается отличным несмываемым маркером зависимости результата работы программы от внешних данных. У шарпа такого маркера нет.

> Это GHC

А это компилятор. У языка есть определенная семантика, unsafePErformIO в нее не входит. С точки зрения хаскеля эта ф-я вообще ничего не делает.

> Ну вот мы и пришли к тому с чего начали — IO оказывается отличным несмываемым маркером зависимости результата работы программы от внешних данных.

Так результат у программы один и тот же — одно и то же ИО. А вот результат ИО — уже другое дело. Но к программе на хаскеле этот результат отношения не имеет :)
Простой пример, у вас есть один и тот же тест на некоторый модуль, вы его запускаете в двух форматах — либо заменив зависимости моками, либо не заменив.

И каким образом у вас вариант с моками оказался больше?
С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе. Похоже, вы нам что-то недоговариваете.

> И каким образом у вас вариант с моками оказался больше?

С моками вам надо настраивать моки, без моков — соответственно, не надо.

> С моками досточно имитировать непосредственные зависимости, без моков — надо построить все зависимости в графе.

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

Например, у вас есть простой однострочный тест, который ловит абсолютно все ошибки (идеал по вашим же критериям). Вот только искать, где именно ошибка, вам придется самому и тест, несмотря на высочайшую "оценку качества", не сможет здесь помочь.

> Например, у вас есть простой однострочный тест, который ловит абсолютно все ошибки (идеал по вашим же критериям). Вот только искать, где именно ошибка, вам придется самому и тест, несмотря на высочайшую «оценку качества», не сможет здесь помочь.

Тест укажет, в чем состоит ошибка (какое именно требование нарушено), а если известно, в чем ошибка, то ее локализация (в тех рамках, в которых она может быть выполнена за счет изоляции тестов) — тривиальная задача, которая даже в самых сложных случаях решается за время порядка единиц минут, обычно же — секунды.

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

Это для теста, покрывающего несколько классов с 20 зависимостями в каждом? Позвольте вам не поверить.
Допустим, ваш тест показал, что сумма контракта считается неверно. За ее расчет отвечает около сотни классов. Тривиально?


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

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

> Допустим, ваш тест показал, что сумма контракта считается неверно. За ее расчет отвечает около сотни классов. Тривиально?

А юнит-тесты вам как помогут? Если какой-то из юнит-тестов падает на сломанной зависимости, то он и в интеграционном виде на ней упадет и вы увидите ошибку там же. Если же не падает — ну тогда в случае интеграционных тестов вы эту ошибку увидите, а в случае юнит-тестов — вообще нет, она будет пропущена.

> А еще лучше — обнаружить две хорошо локализованные ошибки, что наличие юнит-тестов и позволяет сделать.

Конечно, лучше. И еще лучше, если бы не надо было совершать кучу лишних телодвижений для моков. Но такую методологию тестирования еще не придумали. Либо мы пишем простые и качественные тесты, которые, однако, чуть менее точно локализуют ошибку, либо сложные и некачественные, которые локализуют ошибку лучше.
Тестирование только на моках может приводить к «тавтологическим тестам».
Держал это в голове, но после прочтения habrahabr.ru/company/badoo/blog/336194 можно в терминах).

Тавтологичность тестов не зависит от использования моков.

Конечно, «тавтологичность» зависит от неправильного и неразумного использования моков. Меня смутило, судя по всему как и автора предыдущего комментария, напутствие использовать моки как можно чаще. Было бы правильно упомянуть в этом пункте и про подводные камни, коих не мало. Все таки статья посвящена правильному написанию unit-тестов.
Мокать надо зависимости имеющие поведение — для того чтобы тест не имел тенденции падать при изменении других модулей.

3 правило. На *них-машине оба варианта путей приведут к ошибке. Внезапно, однако!
Если уж используется java, то, по-моему, будет уместным пользоваться File.separator или path.separator для формирования путей;
6 правило. Я думаю, будет уместным упомянуть про виртуалки/контейнеры с возможностью создания снимков ФС.

Да лучше везде использовать *ниховые разделители. Винда их вполне понимает.

Тесты — это тоже код, и относиться к нему нужно как к рабочему коду.

То есть нужно писать тесты и на тесты тоже? :-)


image


И всё-равно оставили виндовые разделители путей :-D

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

Что за треш? Paths.get(".", "log", "service.log") позволит вам навсегда забыть о File.separator.

Спасибо за статью. Я с удовольствием посмеялся над пунктом три, и мне понравился последний пункт про запуск тестов не только локально.

Но в целом от такой статьи хотелось бы побольше примеров «из жизни» и, возможно, «вредных» примеров и антипаттернов для наглядности. Некоторые пункты показались довольно банальными.
Как вам мое «тестирование» — syncProj — просто и элегантно? :)

Отвратительно.

Чем аргументируешь?

Тем, что (вообще) не понятно, как это работает и должно работать.


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

Но думаю на эмоции можно отвечать эмоциями. :)

Странные у вас комментарии. Т.е. скачать исходники проекта и запустить unit testing не умеем, но высказать эмоции умеем. Да… По меньшей мере странные… :)

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


Так что нет, отвратительно.

Тут дело тоже в требованиях к тестированию — моя изначальная цель была сделать не 5 ступенчатый шаттл, а простенький unit testing. Возможно что-то не закоммичено из-за чего тест не проходит, возможно у меня английский windows, а у тебя русский и regional settings не совпадают, и он плюется messageboxом в ответ — что мол будем делать с этим тестом.

Если на работе я скажем коммичу изменения, то у меня и build компьютер проверяет что не работает, то с open source code проектом у меня нет build компьютера — все делается мануально.

Возможно я когда-нибуть забогатею с syncProj утилитой и куплю домой себе ещё компьютер для автоматических буйлдов, а также инвестирую себе деньги+ время на полную автоматизацию данного unit теста, но пока это не произошло можно и мануально запустить и самому проверить что к чему.

деньги + время + возможности влияют на требования, подход и код.

и я считаю что прежде чем выражать свои эмоции надо бы понять почему это так сделано а не иначе.
Тут дело тоже в требованиях к тестированию — моя изначальная цель была сделать не 5 ступенчатый шаттл, а простенький unit testing.

Ну так он уже есть в студии готовый. Бери и пользуйся.


то с open source code проектом у меня нет build компьютер — все делается мануально.

AppVeyor


Но вообще отсутствие билд-машины никак не влияет на подход к тестам. Я даже для собственных учебных проектов пишу тесты на том же самом движке, что и для работы, с тем же подходом. И до сих пор не видел причины изобретать велосипеды.


деньги + время + возможности влияют на требования, подход и код.

Обычные юнит-тесты на банальном xUnit пишутся не дольше, чем вы писали свой собственный раннер, а денег не стоят нисколько.


и я считаю что прежде чем выражать свои эмоции надо бы понять почему это так сделано а не иначе.

Так я выражаю эмоции от того, что и как сделано, а не почему оно так сделано.

> Ну так он уже есть в студии готовый. Бери и пользуйся.

я видел много unit testов, и более менее представляю что это и с чем едят.
Вы пишете unit testы. Я их записываю и повторяю («record & playback») — это быстрее чем написать их.

И тут дело ещё не в том что написать

assert( return == 25 )

а в том что никому кроме программиста не понятно что такое 25 — тут идет заточка на описании ошибки, а также видимости всех параметров ошибки конечному пользователю или же пользователю API.
Я их записываю и повторяю («record & playback») — это быстрее чем написать их.

Быстрее, но лучше ли?


а в том что никому кроме программиста не понятно что такое 25

Требований не существует?

> Быстрее, но лучше ли?

Сэкономленное на тестировании время можно использовать на разработку новых фич? Да и refactorинг проще делать. Compare-yes, compare-yes, compare-yes.

> Требований не существует?

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

Я лично видел написанные функции, где вместо boolean превязывался код ошибки, он в конечном итоге он не использовался. (Правда без unit testa)
Да и refactorинг проще делать. Compare-yes, compare-yes, compare-yes.

Рефакторинг — это изменение структуры кода без изменения его поведения. Иными словами, интеграционные тесты (а у вас именно они) при рефакторинге меняться не должны вовсе.


Дело в том что отпечатывание всего текстом заставляет программиста задуматься над тем, что надо сделать (обработку ошибок)

Ну так тесты — это и есть "отпечатывание всего текстом".


Требования не всегда и не все покрывают.

Тогда откуда вы знаете, что надо тестировать?

> Рефакторинг — это изменение структуры кода без изменения его поведения.

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

> Ну так тесты — это и есть «отпечатывание всего текстом».
Но заставляет задуматься что печатаешь что бы было понятно что функциональность делает.

Кстати — assert( result == 25 );
заменяется Console.WriteLine( result );
с последующим test result accept. Но естественно если напечаешь 25 — будет не понятно про что речь.

В итоге ты напечатаешь скажем так:
Console.WriteLine( "Port number: " + result );

И если в следующей итерации ты решишь поменять номер порта, то мой вариант проще и одобрить и понять что изменилось и почему.

> Тогда откуда вы знаете, что надо тестировать?

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

Это классическое определение. Если вы изменили поведение — это не рефакторинг.


Кстати — assert( result == 25 ); заменяется Console.WriteLine( result ); с последующим test result accept.

Конечно же, не заменяется.


Но естественно если напечаешь 25 — будет не понятно про что речь.

Ну так надо нормально переменные (и тесты) именовать.


И если в следующей итерации ты решишь поменять номер порта, то мой вариант проще и одобрить и понять что изменилось и почему.

О нет.


[Fact]
public void PortShouldBe25() => _sut.Port.Should().Be(25);

Вот это легко понять и поменять. И легко понять тестовый вывод (который будет "Test PortShouldBe25 failed: expected 25, got 20".


к чему клоните, не совсем понимаю.

К тому, что обычно необходимое поведение определяется требованиями.

Это классическое определение. Если вы изменили поведение — это не рефакторинг.


Да, вы кстати правы на wiki страничке так и написано. Но я теперь понял что мне не хватает термина refactoring with external changes.

[Fact]
public void PortShouldBe25() => _sut.Port.Should().Be(25);

Вот это легко понять и поменять. И легко понять тестовый вывод (который будет «Test PortShouldBe25 failed: expected 25, got 20».


Да, но фишка в том что бы пишете и тестовый код _sut.Port и результат тестирования Should().Be(25). Я пишу только тестовый код — т.е. делаю 50% меньше работы чем вы.

мне не хватает термина refactoring with external changes.

Чем это отличается от просто "доработки программы"?


Я пишу только тестовый код — т.е. делаю 50% меньше работы чем вы.

А если не писать тестовый код вообще, то будет на 100% меньше работы. Правда же, круто?


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


Как иначе вы предлагаете это сделать?

Чем это отличается от просто «доработки программы»?


В общем-то я хотел выразиться относительно большие изменения в программе — refactoring как раз подходит.

А если не писать тестовый код вообще, то будет на 100% меньше работы. Правда же, круто?


Да, до полного тестирования мне тоже ещё очень далеко. Дело в том что тестировать можно все связанное с файлами, базами данных, когда дело касается скажем 3d отрисовки или скорости выполнения, то это намного сложнее покрывать тестированием. Но syncProj позволяет тестирование — command line utility как никак.

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


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

Кстати — в syncProj идет сравнение не только по console output — там ещё идет сравнение по тому что сама программа генерирует. т.е. .sln (Solution file format) файлы и .vcxproj (xml, C++ project file format).

Альтернатива этому — это действительно прописывать все возвращаеммые значения, скажем как это сделано в premake.
В общем-то я хотел выразиться относительно большие изменения в программе — refactoring как раз подходит.

Рефакторинг — это очень конкретная вещь: изменение реализации без изменения поведения. Вы уж определитесь.


Когда я пишу тестирование, я сначала пишу что программа должна производить

Тогда вы тоже пишете "результат тестирования", и вашему "на 50% меньше" взяться неоткуда.


там ещё идет сравнение по тому что сама программа генерирует. т.е. .sln (Solution file format) файлы и .vcxproj (xml, C++ project file format).

И что? "Правильные" же все равно должны откуда-то взяться. А после того, как они есть, есть куча готовых модулей для юнит-тестирования, которые их сравнят.

И что? «Правильные» же все равно должны откуда-то взяться. А после того, как они есть, есть куча готовых модулей для юнит-тестирования, которые их сравнят.


Правильные результаты или результаты тестирования появляются тогда когда ты нажимаешь Yes в том Message boxе что ты получил и результаты спасаются отдельным файлом («одобрено программистом»).

… и откуда вы знаете, что они правильные? Они же уже сгенерены кодом.

Если это console output, то я, как программист, знаю что программа должна написать.

Если это .sln или .vcxproj — то обычно я открываю проект Визуал Студией и проверяю что бы она не плювалась, а также при закрытии solution, Visual studio не пыталась бы спасти изменения. (Обычно такое просходит если .sln содержит ошинки, и Visual Studio пытается их исправить).
Если это console output, то я, как программист, знаю что программа должна написать.

… и проверяете это глазами? Серьезно?


Если это .sln или .vcxproj — то обычно я открываю проект Визуал Студией и проверяю что бы она не плювалась, а также при закрытии solution, Visual studio не пыталась бы спасти изменения.

… и не сверяете с нужным набором файлов?


Не, это плохое тестирование, и уж точно не автоматизированное. О чем изначально речь и шла.

… и проверяете это глазами? Серьезно?


Да.
Кстати можно сделать проще:

Console.WriteLine( "Test " + result.ToString()


Что даст Test True или Test False, можно проверить визуально тоже.
Не, это плохое тестирование, и уж точно не автоматизированное. О чем изначально речь и шла.


У меня была идея что автоматизировать это можно было бы просто без message box promptа — т.е. результат не совпадает — ошибка / майл, и так далее.
Да.

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


У меня была идея что автоматизировать это можно было бы просто без message box promptа — т.е. результат не совпадает — ошибка / майл, и так далее.

Серьезно, "была идея"? Это же все много раз реализовано.

Серьезно, «была идея»? Это же все много раз реализовано.


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

Да у вас-то понятно, что не реализовано. Но есть же готовые решения.

Кстати, нашёл один random fail — уже профиксил:

Revision: 130
Author: tarmopikaro
Date: 16. syyskuuta 2017 10:51:13
Message:
Bugfix: XmlSerializer was causing field reorder by itself, causing weird fails to appear
— Modified: /ProjectTypes.cs
Modified: /tests/ProjectsToCs/TestAllInOne/out_testproj.cs.accepted

Удивительно почему XmlSerializer вообще задает fieldы случайным образом.

Из следующего коммита — TestRunTime.cs — это тот тест что я написал, остальные 11 файлов сгенерированы и просто «одобрены» мною.

Да, готовые есть, но если можно сделать проще — почему бы не сделать?
Да, готовые есть, но если можно сделать проще — почему бы не сделать?

Потому что проще как раз взять готовое.

Мне все таки кажется что проще готовый заменить на мой — но тут это возможно мое субъективное мнение. Я думаю что если скажем писать unit test на библиотеку которая делать 3d подсчёты то наверное проще было бы использовать обычный unit test, но как только библиотека станет сложнее чем 2+2 — то уже проще перейдти на мой подход. Ну а для syncProj-утилиты проблема довольно специфическая — надо было бы проверять синтакс того что генерируется, можно сделать как это делается в premake5, но мне кажется это сложнее.

Но думаю что подход syncProj очень расширяемый — и на базы данных, и на любые файловые операции.

Возможно картинки он не сможет сравнивать, но это уже другой unit test домайн.
но как только библиотека станет сложнее чем 2+2 — то уже проще перейдти на мой подход.

Аргументируйте. Начать лучше с того, что же такое "ваш подход".


Ну а для syncProj-утилиты проблема довольно специфическая — надо было бы проверять синтакс того что генерируется

А что в этом специфического?

Хмм… Давайте начнем с начала.
Итак для того что бы запустить unit test

1) нам нужен некий unit test framework и начальный effort что бы его взять в использование

2) надо написать сами unit testы.

Я предполагаю что время ушедшее на развертку (1) пункта намного меньше чем (2). Т.е. если мы сделали (1) каким то образом, то он не съест у нас много времени в отличие от (2).

На этом этапе вы со мной согласны?

Время на п.1 меньше, чем п.2, только в том случае, если берется готовый и известный фреймворк. В этом случае есть готовый инструментарий (создать проект в студии — запустить студийными средствами/создать проект в студиии — добавить nuget — запустить студийными средствами; для подключения к CI есть готовые плагины и анализаторы), есть необходимая документация и примеры.

Ну мне кажется готовый framework или свой framework особо разницы не имеет? Ну т.е. (1) может съесть время, например на допиливание своего frameworkа, но и готовый framework тоже может съёсть время — например на конфигурацию и подключение к buildам.
Ну мне кажется готовый framework или свой framework особо разницы не имеет?

Имеет. Во-первых, для любого другого человека подключение стоит намного дороже (документации-то нет), во-вторых, каждую фичу надо дорабатывать.


готовый framework тоже может съёсть время — например на конфигурацию и подключение к buildам.

Для всех мейнстрим-фреймворков это время меньше, чем время на написание того же для своего фреймворка.


Собственно, конфигурировать ни один мейстрим-фреймворк не надо. А подключение к билду, как уже говорилось, делается одним плагином.

Имеет. Во-первых, для любого другого человека подключение стоит намного дороже (документации-то нет), во-вторых, каждую фичу надо дорабатывать.


Ну все при желании запиливается и документируется.
Думаю основой вопрос в каком направлении идти.

Для всех мейнстрим-фреймворков это время меньше, чем время на написание того же для своего фреймворка.


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

Собственно если вы говорите о мейнстрим-фрейворке, то можете дать линк на хорошую документацию, и как и что пишется?
Ну все при желании запиливается и документируется.

И сколько на это надо усилий?


Да, вот это мне нравится больше всего — для «всех». Я знаю что их вагон и маленькая тележка.

Для .net — три, из которых я один никогда в своей жизни не видел.


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

… а можно просто взять работающий и освоить минут за десять.


Собственно если вы говорите о мейнстрим-фрейворке, то можете дать линк на хорошую документацию, и как и что пишется?

https://xunit.github.io/docs/getting-started-desktop.html

… а можно просто взять работающий и освоить минут за десять.


А за 5 можно и свой написать? :-)

[Fact]
public void PassingTest()
{
Assert.Equal(4, Add(2, 2));
}


Как я уже говорил — тут пишутся не только сам код, но и результат тестирования. а если я возьму более сложный пример — например 125374 + 832748 — будете на калькуляторе считать сколько получится? :-)
А за 5 можно и свой написать?

С той же функциональностью? Нельзя.


Как я уже говорил — тут пишутся не только сам код, но и результат тестирования.

Ну да, это основа юнит-тестирования. А как, простите, иначе?..


а если я возьму более сложный пример — например 125374 + 832748 — будете на калькуляторе считать сколько получится?

Ну да, а в чем проблема? Или могу взять в экселе вбить, сделать колонку с суммами, выгрузить в csv, загрузить как теорию. Наконец, могу взять AutoFixture, генерить случайные, складывать и сравнивать.

Ну да, а в чем проблема? Или могу взять в экселе вбить, сделать колонку с суммами, выгрузить в csv, загрузить как теорию. Наконец, могу взять AutoFixture, генерить случайные, складывать и сравнивать.


Хорошо. Я усложню задачу.

sourceforge.net/p/syncproj/code/HEAD/tree/tests/CsToProjects/TestPerFileConfs

для TestPerFileConfs.cs, я хочу проверить что при данной конфигурации код / скрипт сгенерирует те 12 *accepted* файлов что там лежат?

Вобьешь каждую строчку Assert.Equal?

Во-первых, с хорошей вероятностью можно посмотреть на требования и понять, как их протестировать, не сравнивая 12 файлов как текст.


Во-вторых, ничто не мешает прочитать референсные файлы и сравнить результат работы SUT с ними.

Вообще зачем я этот тест изменил — была изначально разница работы vs2010 и vs2013 — там xml format .vcxproj немного отличается — в vs2010, vs2012, нет тага <DebugInformationFormat>None</DebugInformationFormat>

тут находится тот if который я покрыл тестированием

и результат изменения можно просмотреть сравнив файлы out_TestWindows_symbols_off_vs2013.vcxproj.accepted и out_TestWindows_symbols_off_vs2010.vcxproj.accepted между собой.

Во-вторых, ничто не мешает прочитать референсные файлы и сравнить результат работы SUT с ними.


Да, но нужна логика для создания reference файлов?
Или как вы думаете их создавать.

Но вообще я понимаю что тестирование именно даного бага можно было бы сделать именно разбив оригинальный код помодульно, и протестировать конкретно каждый модуль в отдельности. Просто сейчас я имею один механизм тестирования, а так вместо одного я имел бы 3 или 4 разноуровневых unit testа.
Как проще — не знаю — надо было бы пробовать оба метода. Но если честно одноуровневый unit test на данный момент меня устраивает.

И да — тебя наверное интересует как определить результат тестирования — т.е. что я этим тест кайсом тестирую — и это не так ясно. Моя идея в том что я сам на момент разработки знаю где и что должно находится, а если кто и переберет эту утилиту, то можно по svn diff отслеживать что и в какую сторону именилось — а по коду выяснять, что и где не работает.
и результат изменения можно просмотреть сравнив файлы out_TestWindows_symbols_off_vs2013.vcxproj.accepted и out_TestWindows_symbols_off_vs2010.vcxproj.accepted между собой.

А зачем их сравнивать? Если известно, что должно генериться с тегом или без тега, значит, надо проверять наличие и отсутствие тега в выводе.


Да, но нужна логика для создания reference файлов? Или как вы думаете их создавать.

Это же от требований зависит, а не от методики тестирования.


Но если честно одноуровневый unit test на данный момент меня устраивает.

… вот только это не unit test.


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

А идея (одна из) тестов в том, чтобы по тестам можно было определить ожидаемое поведение SUT.

А зачем их сравнивать? Если известно, что должно генериться с тегом или без тега, значит, надо проверять наличие и отсутствие тега в выводе.


Тут ещё такая фишка, что да, я могу это протестировать отдельным unit testом, но кроме этого есть шанс что Visual Studio не одобрит того file formatа что я сгенерировал. Такое бывает правда редко, когда с моей точки зрения это правильный xml код, а с точки зрения Visual Studio полнейший бред. В тот момент когда я подозреваю что Visual Studio станет ругаться на мой проект, я просто открываю .vcxproj в студии и смотрю что она скажет. Т.е. мне иногда нужен не промежуточный результат тестирования, а именно конечный результат.

Конечный результат хорош тоже тем, что я не тестирую промежуточные результаты (где могут тестироваться также всякие ненужные штуки наподобии 2+2=4), а как весь код себя ведет с данной программой. Какие ошибки выдает. Понятны ли ошибки пользователю.

Если делать многоуровней код тестирования, то можно ли все unit test запустить за раз без колдовства?

Это же от требований зависит, а не от методики тестирования.


Требования, методика… — а на практике то как reference файлы сделаем? :-) Может есть предложения?

… вот только это не unit test.


Это наверное больше integration test, но не суть.

А идея (одна из) тестов в том, чтобы по тестам можно было определить ожидаемое поведение SUT.


Ну на данный момент ожидаемое поведение — это весь .vcxproj файл а также console output — но это довольно много информации за раз.

Что такое SUT?
Такое бывает правда редко, когда с моей точки зрения это правильный xml код, а с точки зрения Visual Studio полнейший бред.

А валидатор что об этом думает? А главное, если вы не понимаете, почему VS считает сгенеренный вами файл бредом, как вы можете их генерить?


(и вообще, это типичный msbuild-проект, что там за чудеса?)


где могут тестироваться также всякие ненужные штуки наподобии 2+2=4

Не надо тестировать ненужные штуки, надо тестировать нужные.


И, кстати, тут тоже хорошо видна проблема со сравнением файлов: если в проекте поменять местами два айтема, ваши тесты упадут, хотя с точки зрения поведения это валидный проект — а все потому, что вы сравниваете файлы, а не семантику.


Если делать многоуровней код тестирования, то можно ли все unit test запустить за раз без колдовства?

Да.


Требования, методика… — а на практике то как reference файлы сделаем? :-) Может есть предложения?

От задачи зависит.


Это наверное больше integration test, но не суть.

Учитывая, что мы находимся в статье про юнит-тесты — вполне себе суть.


Что такое SUT?

System under test.

А валидатор что об этом думает? А главное, если вы не понимаете, почему VS считает сгенеренный вами файл бредом, как вы можете их генерить?

(и вообще, это типичный msbuild-проект, что там за чудеса?)


Ну как я упомянул, там могут быть такие xml tagи которые не всеми Visual Studio версиями поддерживаются — как DebugInformationFormat / None например. Т.е. в 2013 году Микрософт программисты решили — давайте улучшим это так, и сделали это. Я же не виноват что vs2010 ещё в активном использовании многими проектами.

Это не msbuild проект — это генератор .vcxproj и .sln файлов. msbuild буйлдит эти проекты.

если в проекте поменять местами два айтема, ваши тесты упадут,


Не упадут. syncProj.exe unit test выдаст, что результат тестирования изменился, что будем делать дальше?

Учитывая, что мы находимся в статье про юнит-тесты — вполне себе суть.


Wiki странички не делают особой разницы между unit и integration testing.

От задачи зависит.


Кто-то утверждал, что у нас все готовое есть, и документация и описание тестирования, а как копнешься глубже оказывается что чего-то нет? :-)

А идея (одна из) тестов в том, чтобы по тестам можно было определить ожидаемое поведение SUT.


Ну в принципе ожидамое поведение — это полностью готовый .vcxproj файл. если не совпадает по линиям, то надо по линиям разбираться почему работает не верно.

Правда согласен — иногда это сложно. Как например с Xml Serializerом — order of fields при сериализации меняет порядок случайным образом. По мне это полный бред.
Ну как я упомянул, там могут быть такие xml tagи которые не всеми Visual Studio версиями поддерживаются — как DebugInformationFormat / None например.

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


Это не msbuild проект — это генератор .vcxproj и .sln файлов. msbuild буйлдит эти проекты.

Я про то, что .vcxproj, если я ничего не путаю — типичный MSBuild-проект.


Не упадут. syncProj.exe unit test выдаст, что результат тестирования изменился, что будем делать дальше?

У вас вообще нет нормального красно-зеленой семафора, у вас есть зеленый (ничего не изменилось) и не-пойми-какой (что-то изменилось); но это не важно: если требуется вмешательство разработчика для проверки, надо ли править код — это то же самое, что "упал тест". А в описанном случае это вмешательство избыточно.


Wiki странички не делают особой разницы между unit и integration testing.

Не знаю, про какие вики-странички вы говорите. Википедия их вполне разделяет.


Кто-то утверждал, что у нас все готовое есть, и документация и описание тестирования, а как копнешься глубже оказывается что чего-то нет?

Есть готовый инструментарий. А постановка задачи в инструментарий не входит.


Ну в принципе ожидамое поведение — это полностью готовый .vcxproj файл.

Осталось, глядя на тест, понять, какой же именно файл считается "полностью готовым".


если не совпадает по линиям, то надо по линиям разбираться почему работает не верно.

Вот это как раз лишняя работа, которой можно избежать.

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


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

У вас вообще нет нормального красно-зеленой семафора, у вас есть зеленый (ничего не изменилось) и не-пойми-какой (что-то изменилось); но это не важно: если требуется вмешательство разработчика для проверки, надо ли править код — это то же самое, что «упал тест».


Вообще есть — моя буйлд система (по работе) майл шлет, у паралельной группы именно светофор с отдельным дисплейем. Но syncProj сделан в свободное от работы время и у меня отдельных build компьютеров дома нет. Сейчас жду, если syncProj подберется / не подберется community, а потом буду думать о расширении.

Есть готовый инструментарий. А постановка задачи в инструментарий не входит.


В этом то и удобство syncProj тестирования, что это полная оболочка для тестирования. Может быть оптимизированная не с точки зрения отдельного build компьютера, но тем не менее. Хотя syncProj использует C# скрипты (почти как end-user frontend), не факт что ещё какой-нибуть API захочет использовать C# скрипты.

Осталось, глядя на тест, понять, какой же именно файл считается «полностью готовым».


Все полностью готовые? Т.е. в итоге конечный пользователь конфигурирует syncProj C# скрипты как он хочет, а что он хочет, зависит от него самого. В unit testе я просто делаю все возможные вариации проектов.

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


Ну обычно это не трудно, визуально воспринимается легко. Обычно если какая несостыковка — ищешь где генерируется преведущий xml tag и ставишь туда breakpoint — и исправляешь. Не сложнее чем использование калькулятора или Excelя.

Но есть и более сложные запчасти кода — например переделка .vcxproj в .cs скрипт. Включен у тебя этот define только для Debug конфигурации / x64 платформы или ещё для Release / Win32?

Там уже надо с кофе думать, как должно работать — но сейчас моя основная цель генерация, не reverse engineering. syncProj может отплюнуть какой-то .cs, а оттуда developerы могут уже и сами доковылять с документацией до конечной цели.

Но думаю без сложного кода не делается нужная функциональность.
Идея неплохая, но я иногда сравниваю с оригинальным форматом Visual Studio

Зачем?


Вообще есть — моя буйлд система (по работе) майл шлет, у паралельной группы именно светофор с отдельным дисплейем.

Это происходит в ответ на месседж-бокс вашей тестовой системы? Или вы о чем-то вообще не связанном говорите?


В этом то и удобство syncProj тестирования, что это полная оболочка для тестирования.

xunit (или mstest, или nunit) — не менее "полная оболочка для тестирования". Так в чем удобство-то?


(где в вашей "полной оболочке" интеграция с семафором в студии или решарпере?)


Все полностью готовые

Это рекурсивное определение, оно бессмысленно.


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

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


Обычно если какая несостыковка — ищешь где генерируется преведущий xml tag и ставишь туда breakpoint — и исправляешь.

(а) как найти, где генерится этот таг?
(б) как узнать, на что исправить?

Идея неплохая, но я иногда сравниваю с оригинальным форматом Visual Studio
Зачем?


Чтобы хотя бы примерно видеть, что я генерирую то, что Visual Studiя спасает. Т.е. file format идентичен. Я же для Visual Studiи его делаю.

Это происходит в ответ на месседж-бокс вашей тестовой системы? Или вы о чем-то вообще не связанном говорите?


Забудьте что я на работе делаю, syncProj на данный момент не подключен к autoбуйлдам. Возможно в будущем подключу.

xunit (или mstest, или nunit) — не менее «полная оболочка для тестирования». Так в чем удобство-то?


В том что ты не тестируешь, т.е. не пишешь тестовый код, как таковой. Просто печатаешь все на экран (console output) или спасаешь в файл, одобряешь его, и все, тест готов. 50% меньше работы.

Пример с перестановкой элементов я уже приводил: а представьте себе, что у вас было несколько сотен элементов, которые поменялись местами.


Ок, большое кол-во информации можно либо спасасть в тот же *accepted* файл, но уже в бинарном формате, либо посчитывать какой-нибуть checksum через все bufferа, и печатать их, тогда в *accepted* файл (console output), но в мой тест framework можно сделать и автоматический анализ и разпечатывать результаты на экран, после чего он тоже попадет в *accepted* file (console output). T.e. можно сделать и по-моему и по-твоему ?!

(а) как найти, где генерится этот таг?
(б) как узнать, на что исправить?


Ну например с vs2010 несовместимостью Visual Studiя выдавала ошибку стиля: не знаю такого Debug formatа: None.
Открываешь .vcxproj, ищешь None. Находишь напротив название xml taga: DebugInformationFormat. идешь в исходники и ищешь "<DebugInformationFormat" и находишь точку, где он генерируется. Ставишь breakpoint и начинаешь фиксить.

Но с Visual studiями я поменял Visual studio версию 2 или 3 раза пока не понял какая Visual studiя поддерживает и что. Использовал для этого vsver() функцию — после этого знал, как фиксить.

С syncProj немного сложнее, потому что .vcxproj file format оффициально не документирован нигде и такие утилиты как cmake, qmake, premake5 сами разбираются с этим file formatом. Это немного «reverse engineering» — как заставить Visual Studiю думать, что это она спасала этот файл, а не ты.

Думаю с точки зрения покрытия unit testингом — это не лучшая для примера утилита.
Чтобы хотя бы примерно видеть, что я генерирую то, что Visual Studiя спасает.

Это этап формирования требований (=постановки задачи). Это происходит до написания тестов.


Забудьте что я на работе делаю,

… ну так вы это не упоминайте не к месту, я и не буду вспоминать.


Просто печатаешь все на экран (console output) или спасаешь в файл, одобряешь его, и все, тест готов. 50% меньше работы.

Эмм, а откуда меньше-то? Надо просмотреть файл, понять, что он правильный, одобрить его.


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


Ок, большое кол-во информации можно либо спасасть в тот же accepted файл, но уже в бинарном формате, либо посчитывать какой-нибуть checksum через все bufferа, и печатать их, тогда в accepted файл (console output),

И как это все поможет понять, что всего лишь поменялся порядок элементов (который для задачи не критичен)?


T.e. можно сделать и по-моему и по-твоему ?!

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


Ставишь breakpoint и начинаешь фиксить.

… на что?


С syncProj немного сложнее, потому что .vcxproj file format оффициально не документирован нигде

https://blogs.msdn.microsoft.com/visualstudio/2010/05/14/a-guide-to-vcxproj-and-props-file-structure/


Я же говорю: это MSBuild. Видел один, видел все.


Думаю с точки зрения покрытия unit testингом — это не лучшая для примера утилита.

Да нет, ничем не хуже других. Даже наоборот, в чем-то лучше, потому что вход и выход чисто машино-читаемые, и нет никакого сложного сетапа.

Чтобы хотя бы примерно видеть, что я генерирую то, что Visual Studiя спасает.
Это этап формирования требований (=постановки задачи). Это происходит до написания тестов.


Ну предположим что я мог бы запустить vs2010, vs2012, vs2013, vs2015, vs2017 сгенерировать на них все возможные пермутации проектов с mfc, native c++, windows, x64, со всеми возможными опциями, но вам не кажется что это немного перебор?
Думаю что основаная проблема в том что я частично reverse engeneerю file format, по этому это невозможно оформить в виде требований. К тому же у меня меняются требования — добавлял поддержку странностей других генераторов как cmake, Gyp.

… ну так вы это не упоминайте не к месту, я и не буду вспоминать.


Ну так вы спросили знаю ли я. Знаю… :-) Но с конктекстом syncProj никак не связанно.

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


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

Например в этом файле в конце 3 filter, которые можно было соединить в два — как это написано уже в исправленной версии — там логика такая что если несколько конфигураций с одной и той же коммандой и сообщением — то syncProj должен был их соединить в одну конфигурацию.

Я знаю, что syncProj работал не так, как надо, но reverse engineering не моя прямая цель, поэтому я одобрил, как есть и пошёл дальше. Позже правда это пришлось разбирать, почему не работает, из-за случайных failов — но если бы failов не было это был бы удолетворительный результат для меня.

Пользователь получает на 6 строк больше, чем нужно — перебьётся и сам отедитирует, как ему нужно.

Думаю, не я первый и не я последний, кто захочет обойти какое-то мало важное препятствие.

И как это все поможет понять, что всего лишь поменялся порядок элементов (который для задачи не критичен)?


Ну это я думаю даже с обычным unit testом надо будет дебаггировать.

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


Взять ваш аналитический подход и напечатать «ok» / «not ok» на экране? «ok» одобрить, если придет «not ok», то тест зафайлит.

Ставишь breakpoint и начинаешь фиксить.
… на что?


Я уже описал выше, но вообще зависит от кайса. Не хватает xml тагов, или линий — добавить, слишком много — убавить?

blogs.msdn.microsoft.com/visualstudio/2010/05/14/a-guide-to-vcxproj-and-props-file-structure


это props file format. Сделай поиск на «DebugInformationFormat»? Нету? Правильно. Не описан.

Да нет, ничем не хуже других. Даже наоборот, в чем-то лучше, потому что вход и выход чисто машино-читаемые, и нет никакого сложного сетапа.


Единственное что мог бы предложить — взять и покрыть syncProj тестированием таким, каким видите вы его. Понимаю что это много работы, но хотя бы для basic вызовов.

Ну предположим что я мог бы запустить vs2010, vs2012, vs2013, vs2015, vs2017 сгенерировать на них все возможные пермутации проектов с mfc, native c++, windows, x64, со всеми возможными опциями, но вам не кажется что это немного перебор?

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


Думаю что основаная проблема в том что я частично reverse engeneerю file format, по этому это невозможно оформить в виде требований.

Не вижу проблемы. Сделали реверс, зафиксировали результаты анализа в виде требований.


Ну так вы спросили знаю ли я. Знаю

Я не спрашивал, знаете ли вы. Я говорил, что в вашей реализации адекватного семафора нет.


В принципе я согласен, что нормально ты знаешь какие результаты вылезут — но их не обязательно писать с моей точки зрения.

А если не писать, то как проверить, что результат работы совпадает?


Кроме того всегда есть желание срезать, hackнуть — т.е. если работает не совсем так, как надо или работает не так, но и не надо — то можно просто одобрить как есть и пойти дальше.

… а можно просто не тестировать, чего уж.


Я знаю, что syncProj работал не так, как надо, но reverse engineering не моя прямая цель, поэтому я одобрил, как есть и пошёл дальше. Позже правда это пришлось разбирать, почему не работает, из-за случайных failов

Вот и демонстрация того, почему так делать не надо.


Пользователь получает на 6 строк больше, чем нужно — перебьётся и сам отедитирует, как ему нужно.

При таком подходе, повторюсь, можно вообще не тестировать. Пользователь сам все поправит.


Ну это я думаю даже с обычным unit testом надо будет дебаггировать.

Нет. Если в тесте сказано "игнорировать порядок элементов", то он будет игнорировать порядок элементов.


Взять ваш аналитический подход и напечатать «ok» / «not ok» на экране? «ok» одобрить, если придет «not ok», то тест зафайлит.

Ээээ… что? И как это решит проблему того, что ваше сравнение не понимает семантики?


Я уже описал выше, но вообще зависит от кайса. Не хватает xml тагов, или линий — добавить, слишком много — убавить?

Так надо же знать, не хватает, или слишком много, или не те.


это props file format.

Упс, нет:


If you inspect the contents of a .vcxproj file (the new VC++ project file format in VS2010) in notepad or in VS editor…

Сделай поиск на «DebugInformationFormat»? Нету? Правильно. Не описан.

Правильно, потому что студия 2010-ая. Но к формату файла это отношения не имеет. DebugInformationFormat — это типичный параметр для таска/таргета, а параметры можно посмотреть в описании самого таска/таргета в .targets, и так далее.


Единственное что мог бы предложить — взять и покрыть syncProj тестированием таким, каким видите вы его.

А зачем мне это?

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

Не вижу проблемы. Сделали реверс, зафиксировали результаты анализа в виде требований.


А можно сделать проще. Не выписывать все требования, а начать с основных и по мере развития проекта добавлять новые требования. Как я и сделал.
И оставить за проектом возможность сравнивать file format по мере надобности, как это делается сейчас. Думаю какой-нибуть умный термин для этого есть. Например интеративное или эволюционное развитие.
:D

Я говорил, что в вашей реализации адекватного семафора нет.


Теперь можно поподробнее. Семафор, как я понимаю — это просто лампочки — красная или зелёная — прошло / не прошло. Или я что-то не так понимаю?

А если не писать, то как проверить, что результат работы совпадает?


В syncProj на данный момент запускается 176 тестов. Естественно я их не написал все сразу. Обычно пишется один тест за раз и сразу же тестируется. И в месте с тем, как тестируется — то результат одобряется или код продолжает исправлятся. Разве вы не так пишете тесты?

… а можно просто не тестировать, чего уж.


Лучше больше, чем нет совсем. То что .cs может генерироваться из .vcxproj — это большой плюс, но то что не самым оптимальным образом — ну и что?
Фича есть? Есть. Такой не у premake5 ни у cmake нету.
Тестируется? Да. Разботает не оптимально? Ну и?

При таком подходе, повторюсь, можно вообще не тестировать. Пользователь сам все поправит.


В смысле обратная генерация .vcxproj должна работать правильно — а как писать C# поверх syncProj — это уже зависит от программиста хорошо или нет.

Нет. Если в тесте сказано «игнорировать порядок элементов», то он будет игнорировать порядок элементов.

А зачем мне это?


Просто я подумал, что человек который пишет «Отвратительно.» действительно знает как писать unit тесты, а тут начинаешь ковырятся и идут отсылки и требованиям и к методике, и потихоньку тонешь в этом болоте. Покажите мне красивый unit test, как бы вы его стояще написали? :D

Вот и демонстрация того, почему так делать не надо.


Да нет, syncProj делает далеко не оптимизированный C# код. и это уже не первый раз я обхожу.

Ээээ… что? И как это решит проблему того, что ваше сравнение не понимает семантики?


Семантику понимает программист. Этого достаточно?
Вообще я сам всегда мечтал, что бы утилиты были не слишком глупыми, а немного помогали делать работу. Тот же компилер мог бы и автоматом пытаться добавить ';' если она отсутствует. Но пока что такого нет.

Так надо же знать, не хватает, или слишком много, или не те.


По diffy определяется. Я использую TortoiseMerge или ExamDiff. Достаточно? (Это когда нажимаешь «No»)

Правильно, потому что студия 2010-ая. Но к формату файла это отношения не имеет. DebugInformationFormat — это типичный параметр для таска/таргета, а параметры можно посмотреть в описании самого таска/таргета в .targets, и так далее.


Теперь не понял. К чему этот линк то?
А можно сделать проще. Не выписывать все требования, а начать с основных и по мере развития проекта добавлять новые требования. Как я и сделал.

Это не "проще", это так, как я и описал.


Теперь можно поподробнее. Семафор, как я понимаю — это просто лампочки — красная или зелёная — прошло / не прошло.

Да, именно так.


Разве вы не так пишете тесты?

Нет, я не так пишу тесты. Я ничего не "одобряю", я просто пишу тесты с assertions и удостоверяюсь, что они проходят.


Ну и?

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


Покажите мне красивый unit test, как бы вы его стояще написали?

[Theory]
[InlineAutoData]
public void CreateEntityMappingKey(int entityId) => _target.CreateEntityMappingKey(entityId).Should().Be("E/" + entityId);

[Theory]
[InlineAutoData]
public void CreateEntityActionMappingKey(int entityId, int actionId) => _target.CreateEntityActionMappingKey(entityId, actionId).Should().Be("E/" + entityId + "/A" + actionId);

//...

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("E")]
[InlineData("E/")]
[InlineData("E//")]
[InlineData("E/qwert")]
[InlineData("E/qwerty")]
public void ExtractTopLevelEntityIdFromFieldKey_InvalidValues(string key) => 
  _target
  .Invoking(t => _target.ExtractTopLevelEntityIdFromFieldKey(key))
  .ShouldThrow<Exception>();

Не то что бы верх идеала, но живые работающие юнит-тесты. 106 на один компонент, проходят за 0.093 секунды.


Семантику понимает программист. Этого достаточно?

Нет, конечно. Весь смысл автоматизированных тестов в том, чтобы программист работал поменьше.


По diffy определяется.

… это если у вас ожидаемый файл правильный. А откуда вы это знаете, если вы его в прошлый раз "одобрили"?


Теперь не понял. К чему этот линк то?

К тому, что, вопреки тому, что вы пишете, описание формата vcxproj гуглится сравнительно быстро.

Это не «проще», это так, как я и описал.


Удивительно, как можно говорить об одном и том же и не понимать друг друга. :-)

Теперь можно поподробнее. Семафор, как я понимаю — это просто лампочки — красная или зелёная — прошло / не прошло.
Да, именно так.


На данный момент я не коммичу не протестировав. Мне кажется так тоже можно делать?

Нет, я не так пишу тесты. Я ничего не «одобряю», я просто пишу тесты с assertions и удостоверяюсь, что они проходят.


В смысле вы не пишете 176 тестов за раз. (Тест пишется, тестируется) * N, релизится.

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


Ну т.е. там, где я вижу пол-стакана полным (протестируем), вы видите его пустым. (не будем тестировать)

[Theory]
[InlineAutoData]


Если честно, то аналогичные unit тесты видел у нас в группе когда смотрел «через плечо». Думаю, что любая проблема, может быть решена несколькими способами. Сначала вырабатываются «слова», «запчасти», C# attributы, функции нового языка для тестирования, а затем тестирование происходит на уровне нового языка.

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

Но тут вопрос ещё в том, что легко ли усвоить новый язык или нет.

У вас приведен код уже с 4мя аттрибутами. Дальше будет больше? Я оперирую с двумя — print & compare.

С точки зрения понимания нового языка для тестирования мой легче усвоить. (Если возьмем новичка, что не понимает в тестировании ничего).

Да, отсутствие документации для моего метода тестирования — это минус, но при желании могу написать. Думаю, что надо бы написать.

106 на один компонент, проходят за 0.093 секунды.


Вот скорость — это кстати тоже важный критерий. Мои тесты прогоняются порядка 4 секунд, но у меня есть тоже создание процесса (это консольное приложение как никак). Согласен, что со временем чем больше testing занимает, тем больше отнимает времени у программиста. Лучше его минимизировать. У меня есть к чему стремится. :-)

Нет, конечно. Весь смысл автоматизированных тестов в том, чтобы программист работал поменьше.


Но если тест failит — то чем ваш ваш подход уменьшает время работы программиста? Т.е. если не работает, то не работает, надо фиксить — тут никуда не отвертется.

… это если у вас ожидаемый файл правильный. А откуда вы это знаете, если вы его в прошлый раз «одобрили»?


Если одобрен — то находится в *accepted* файле. А svn diff покажет изменения с преведущего релиз тестирования (Что-где добавилось или изчезло).

К тому, что, вопреки тому, что вы пишете, описание формата vcxproj гуглится сравнительно быстро.


Да, но полное описание file formatа вы мне не нашли. Вы нашли что-то. Что-то кто угодно найдет.
На данный момент я не коммичу не протестировав. Мне кажется так тоже можно делать?

Можно. Какое отношение это имеет к семафору?


В смысле вы не пишете 176 тестов за раз. (Тест пишется, тестируется) * N, релизится.

Иногда пишу. Зависит от того, сколько требований у меня есть.


У вас приведен код уже с 4мя аттрибутами. Дальше будет больше? Я оперирую с двумя — print & compare.

Для того, чтобы писать базовые тесты, достаточно одного: [Fact]. Все остальное — дополнительная функциональность, возникающая по мере надобности.


А ваше "print&compare" — это же вообще не атрибуты, это всего лишь подход. Чем универсальнее подход, тем менее он удобен на каждый конкретный частный случай.


С точки зрения понимания нового языка для тестирования мой легче усвоить.

Это утверждение ничем не обосновано. А учитывая, что методология юнит-тестирования описана сильно больше, чем в одной книге, еще и бессмысленно.


Но если тест failит — то чем ваш ваш подход уменьшает время работы программиста?

Тем, что (а) говорит, в чем конкретно состоит ошибка, и (б) в каком именно методе она была. Это, конечно, если придерживаться чистого юнит-тестирования. Плюс меньше ложных срабатываний.


Если одобрен — то находится в accepted файле. А svn diff покажет изменения с преведущего релиз тестирования (Что-где добавилось или изчезло).

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


Да, но полное описание file formatа вы мне не нашли.

Вы, похоже, не понимаете, как устроены msbuild-проекты. Ну ладно, не суть, это не моя проблема.

Можно. Какое отношение это имеет к семафору?


Никакого. Зачем мне светофор то?

А ваше «print&compare» — это же вообще не атрибуты, это всего лишь подход. Чем универсальнее подход, тем менее он удобен на каждый конкретный частный случай.


Возможно. Возможно нет, надо пробовать. Мне нравится, что я тесты как таковые не пишу. Просто печатаю все в for loopе на экран и одобряю. Вообще без запарки.

Это утверждение ничем не обосновано. А учитывая, что методология юнит-тестирования описана сильно больше, чем в одной книге, еще и бессмысленно.


Я уже предлагал созвонится аудио / скайп. Основая идея, что онлайн / face-to-face зачастую проще и быстрее объяснить, чем сотней писем/постов. Но я думаю, что и это множество книг меня тоже не особенно вдохновляют, хотя бы потому что их множество. Напоминает трясину или болото, где все звучит хорошо, но времени уходит уйма.

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


А почему нет?

Да, но полное описание file formatа вы мне не нашли.
Вы, похоже, не понимаете, как устроены msbuild-проекты. Ну ладно, не суть, это не моя проблема.


Ну я понимаю, что понимать мне особо и не надо — зачем забивать голову ненужной информацией? :-)

Никакого. Зачем мне светофор то?

Вам, видимо, низачем. А вообще это основа юнит-тестов.


Мне нравится, что я тесты как таковые не пишу. Просто печатаю все в for loopе на экран и одобряю. Вообще без запарки.

Если вообще не тестировать, будет еще меньше запарки, представляете?


Понимаете ли, "одобрение" всегда хуже, чем машинная сверка.


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

Ну да, свое болото-то интереснее, конечно.


А почему нет?

Потому что чем меньше инструментов нужно для решения задачи, тем лучше.

Вам, видимо, низачем. А вообще это основа юнит-тестов.


Вообще в программировании много новых и просто искуственных терминов и понятий. Когда люди начинают говоть о controllerах, factory, clue, wrapperах, начинашь прислушиваться, что за странности пойдут дальше. Можете объяснить то зачем мне светофор? :-)

Ну т.е. тот что на дороге стоит, известно зачем.

Понимаете ли, «одобрение» всегда хуже, чем машинная сверка.


т.е. в первом случае вы набрали текст и спасли его в исходник. Во-втором случае вы нажали «yes» и программа спасла текст вместо вас в файл. В чем разница то? :-)

Ну да, свое болото-то интереснее, конечно.


По мне да. Не в том смысле, что ты знаешь свое болото и остальных болот не хочешь знать — а в том смысле что уже прогулялся по тем болотам и решил сделать свое.

Потому что чем меньше инструментов нужно для решения задачи, тем лучше.


Чем эффективнее используешь инструменты — тем быстее разработка.
Можете объяснить то зачем мне светофор?

Red-green-refactor. Очень просто же: написали тест, он красный, написали код, тест стал зеленый, можно двигаться дальше.


т.е. в первом случае вы набрали текст и спасли его в исходник. Во-втором случае вы нажали «yes» и программа спасла текст вместо вас в файл. В чем разница то?

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


Чем эффективнее используешь инструменты — тем быстее разработка.

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

Red-green-refactor. Очень просто же: написали тест, он красный, написали код, тест стал зеленый, можно двигаться дальше.


Ну можно и написать и тест и код одновременно?

я должен просмотреть результат (а если он большой)


Ну если это консоль — то не печатай много текста. А если это сгенерированный файл, то здесь либо diff compare на другой файл (например между соседними файлами), либо diff compare на svn history (преведущий коммит). Сравнение делает в любом случае машина, разница лишь в том, что в одном случае ты сам пишешь, а в другом машина.

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


В случае с syncProj — файлы подбираются Visual studiей, и проще иметь весь проект, чем запчасти проекта, хотя бы потому что можно протестировать с Visual Studiей.

Даже на данный момент если я буду добавлять поддержку всего того, что умеет Visual Studiя на данный момент (а это например c# код, managed c++ код), то у меня уйдут годы на поддержку всех пермутаций.

А если это весь проект, то diff compare единственный вариант. Но для других проектов возможно diff compare не лучший вариант.

Ну можно и написать и тест и код одновременно?

Если сильно захотеть, то можно. Но зачем?


Ну если это консоль — то не печатай много текста.

Ээээ. Результат работы программы — это результат работы программы, я не должен его модифицировать только для того, чтобы тестам было удобнее.


А если это сгенерированный файл, то здесь либо diff compare на другой файл (например между соседними файлами)

Так откуда же взять файл, с которым сравнивать-то? И зачем в этой конструкции что-то "одобрять", если уже есть эталонный файл.


либо diff compare на svn history (преведущий коммит).

Нету у меня версионника в тестах, и никогда не будет.


В случае с syncProj — файлы подбираются Visual studiей, и проще иметь весь проект, чем запчасти проекта, хотя бы потому что можно протестировать с Visual Studiей.

Одно другому не третье. Можно иметь в качестве результата весь проект для работы со студией, и при этом тесты на отдельные участки работы.


А если это весь проект, то diff compare единственный вариант.

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

Если сильно захотеть, то можно. Но зачем?


Мне кажется это нормальный подход, делаешь и тестируешь? Если без автоматизации, то это так и надо делать, если с автоматизацией, то можно и с светофором.

Ээээ. Результат работы программы — это результат работы программы, я не должен его модифицировать только для того, чтобы тестам было удобнее.


Ну скажем так если outputа много, то это уже отдельные файлы надо делать, визуально не проверишь мега или гига байтовые файлы.

Так откуда же взять файл, с которым сравнивать-то? И зачем в этой конструкции что-то «одобрять», если уже есть эталонный файл.


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

Это уже второе, третье и т.д. он может полуспать и сравнивать diff на преведущую версию в svn или с чем то ещё.

Но когда слишком много изменений, надо опять проснуться и разобраться в коде.

Нету у меня версионника в тестах, и никогда не будет.


По мне результат тестирования так же важен как и сам код. По крайней мере я так решил с syncProj. Но в принципе можно и без version control обходится, просто полуспать не получится. :-)

Одно другому не третье. Можно иметь в качестве результата весь проект для работы со студией, и при этом тесты на отдельные участки работы.


Можно, но нужно ли. В принципе syncProj не имеет на данный момент никакой сложной функциональности — т.е. там нет геометрических подсчётов, нет больших операцией с базой данных — поэтому зачем делать сложным то, что в своей основе просто?

Есть всегда вероятность того, что другой программист не сможет все держать в голове что и где находится или же я сам лет через 5-10 не смогу — но я и ещё не такие навороты с кодом видел.

Правда навороты — это не есть хороший код. (Особенно без тестирования)

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


Как я уже говорил, diff не обязательно делать на данные, можно просто ответ написать (на сравнение с эталонными данными) — «ок / не ок».
и сравнивать уже сам «ок».
Мне кажется это нормальный подход, делаешь и тестируешь?

Не для юнит-тестов. Юнит-тесты пишутся либо до, либо после. Нет смысла писать их одновременно.


Ну скажем так если outputа много, то это уже отдельные файлы надо делать, визуально не проверишь мега или гига байтовые файлы.

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


Первое «одобрение» — программист должен понимать, что код должен делать с точностью до линии.

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


По мне результат тестирования так же важен как и сам код.

Результат тестирования — это "прошло" или "не прошло".


В принципе syncProj не имеет на данный момент никакой сложной функциональности — т.е. там нет геометрических подсчётов, нет больших операцией с базой данных — поэтому зачем делать сложным то, что в своей основе просто?

Я, вроде бы, нигде и не предлагал что-то делать сложно.


Как я уже говорил, diff не обязательно делать на данные, можно просто ответ написать (на сравнение с эталонными данными) — «ок / не ок».
и сравнивать уже сам «ок».

Я вообще не понял, что вы имеете в виду.

Не для юнит-тестов. Юнит-тесты пишутся либо до, либо после. Нет смысла писать их одновременно.


А почему нельзя писать одновременно то?

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


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

А почему нельзя писать одновременно то?

А зачем? Зачем "до" — понятно, это TDD. Зачем "после" — тоже понятно, это тестирование написанного кода. А одновременно зачем?

Есть подозрение, что некоторые товарищи упускают из виду некоторые цели unit тестов: раз и два.

Позиция «юнит тест это только регресионное тестирование» начала устаревать еще лет 20 назад, когда Кент Бек впервые показал дядюшке Бобу TDD.
Чаще используйте заглушки (моки) вместо реальных объектов.


По моему спорно, тот же мартин фаулер писал, про два стиля написания тестов и про то, что есть факторы по которым следует выбирать, какой стиль использовать, его точка зрения мне как то ближе чем просто заявление «пишите вот так».
Не подскажете где он это писал? Интересно узнать о критериях, да и о самих стилях.

Mocks Aren't Stubs, раздел "Classical and Mockist Testing". Только это разделение не по "использовать заглушки или нет", а "что верифицировать".

Наоборот же, он в итоге пишет:

So as we see, state versus behavior verification is mostly not a big decision. The real issue is between classic and mockist TDD.

Фаулер все-таки писал немного о другом. Фаулер вводит четкое разделение моков, фейков, заглушек и так далее, и уже в зависимости от этого проводит разделение по тому, что верифицируется — состояние SUT или набор вызовов на моке.


Если вы посмотрите в статью, то там написано следующее:


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

По классификации Фаулера это стаб, а не мок, и такое использование вполне попадает в "классический", а не "мокистский" стиль тестирования.


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

> По классификации Фаулера это стаб, а не мок, и такое использование вполне попадает в «классический», а не «мокистский» стиль тестирования.

Он как раз это не разделяет:

Now I'm at the point where I can explore the second dichotomy: that between classical and mockist TDD. The big issue here is when to use a mock (or other double).

The classical TDD style is to use real objects if possible and a double if it's awkward to use the real thing. So a classical TDDer would use a real warehouse and a double for the mail service. The kind of double doesn't really matter that much.
Sign up to leave a comment.