Как тестировать серверные сервисные приложения?
От: vsb Казахстан  
Дата: 12.12.22 02:09
Оценка:
Имеется приложение. При старте запускает веб-сервер. Для работы использует СУБД. Принимает некие HTTP запросы с JSON телами и отвечает так же. Также само запрашивает некоторые другие сервисы по HTTP. Также в будущем будет работать с Rabbit MQ.

Хочется понять, как такое приложение тестировать на верхнем уровне.

Сейчас это делается юнит-тестами. Запускается несколько мок-серверов, запускается при старте теста БД, в эту БД код кладёт начальные данные, вызывает нужные эндпоинты, в конце проверяет, вызывались ли нужные эндпоинты у мок-серверов, проверяет, изменились ли данные в БД.

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

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

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

Пока вижу два подхода:

1. Собрать свой велосипед из полу-готовых инструментов. К примеру реализовать вставку в БД/верификацию по XLSX и готовить данные в экселе. Для мока использовать что-то вроде wiremock, у него там есть свои конфиги. В итоге тестировщик будет в кучке файлов возиться.

2. Сделать всё через Postman. Я им сам не пользовался, могу ошибаться, но вроде там есть возможность написания тестовых сценариев. При этом для работы с БД параллельно запустить второй сервис, который даёт универсальный HTTP-интерфейс для базы и подготавливать/проверять данные в Postman вызывая этот сервис.
Отредактировано 12.12.2022 2:39 vsb . Предыдущая версия .
Re: Как тестировать серверные сервисные приложения?
От: scf  
Дата: 12.12.22 03:50
Оценка: 18 (1) +1
Здравствуйте, vsb, Вы писали:

vsb>Сейчас это делается юнит-тестами. Запускается несколько мок-серверов, запускается при старте теста БД, в эту БД код кладёт начальные данные, вызывает нужные эндпоинты, в конце проверяет, вызывались ли нужные эндпоинты у мок-серверов, проверяет, изменились ли данные в БД.


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

Как правило, у микросервиса есть 4 пакета тестов:
— юнит-тесты на отдельные классы
— интеграционные тесты в рамках приложения, когда тестируются подсистемы или большая часть приложения, внешние зависимости заменяются на фейки тоже в рамках приложения, БД поднимается in-memory с тестовыми данными.
— приемочные тесты, это отдельное приложение, тестирующее микросервис через API в изоляции. Внешние зависимости заменяются на эмуляцию (на базе того же wiremock), БД в идеале должна наполняться через то же API, совсем в идеале — чтобы не ломать существующую базу. Например, если в системе данные разделены по юзерам, то приемочные тесты создают новых юзеров для тестирования.
— end-to-end тесты, их немного, но они нужны, чтобы проверить интеграцию между микросервисами и убедиться в работе системы в целом.

Первые две категории пишутся разработчиками, 3 и 4 — автотестировщиками, которым разработчики написали тестовый движок с эмуляторами, удобным апи, кейсами в формате текстовых файликов, интеграцией с cucumber и т.п.
Re: Как тестировать серверные сервисные приложения?
От: Sinclair Россия https://github.com/evilguest/
Дата: 15.12.22 11:51
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Имеется приложение. При старте запускает веб-сервер. Для работы использует СУБД. Принимает некие HTTP запросы с JSON телами и отвечает так же. Также само запрашивает некоторые другие сервисы по HTTP. Также в будущем будет работать с Rabbit MQ.


vsb>Хочется понять, как такое приложение тестировать на верхнем уровне.


vsb>Сейчас это делается юнит-тестами. Запускается несколько мок-серверов, запускается при старте теста БД, в эту БД код кладёт начальные данные, вызывает нужные эндпоинты, в конце проверяет, вызывались ли нужные эндпоинты у мок-серверов, проверяет, изменились ли данные в БД.

Непонятно, зачем ваши юнит тесты устроены таким странным образом.
Юнит-тест тестирует отдельный кусочек системы. Если у вас эти отдельные кусочки системы реализованы в императивном стиле с состоянием — ну, да, тестировать их сложно. За этом мы и не любим stateful-код.
Лучше всего, когда у вас система собирается из стейтлесс-кусочков.
Вот смотрите: допустим, у вас есть некий API-метод, который при вызове лезет в базу, что-то оттуда достаёт, лезет в сторонний сервис, комбинирует результаты и отдаёт клиенту.
Наиболее практичный способ юнит-тестирования — тестировать его по частям:
1. Проверяем, что для всех разрешённых значений параметров нашего API-метода строятся корректные запросы в базу.
Обратите внимание — исполнять запросы не надо. Достаточно того, что при задании параметра startDate во where добавляется documentDate >= @startDate, а без него — не добавляется.
Исполнением запроса занимается СУБД, её авторы, скорее всего, покрыли её огромным количеством тестов, чтобы убедиться, что когда во where добавляется documentDate >= @startDate, то никаких записейс documentDate < @startDate точно не вернётся. Вам не нужно за ними ничего перепроверять.

2. Проверяем, что для всех разрешённых значений параметров нашего API-метода строятся корректные запросы в сторонний сервис. И опять — исполнять эти запросы не надо. Надо проверить, что запрос порождается корректно.
3. Проверяем, что для всех интересующих нас комбинаций вариантов данных, вытащенных из СУБД и стороннего сервиса, корректно строятся те самые "композитные" результаты.
В общем-то, всё.
В зависимости от того, какую часть ответственности на себя берёт ваш код, вы можете захотеть проверить, что для всех интересующих нас вариантов "композитных" результатов порождается корректный JSON (ну или что там у нас в качестве выходного формата), а также что мы корректно разбираем веб-реквесты, превращая их в парaметры API-метода. Но, опять-таки, скорее всего вы будете пользоваться не самописанными реализациями, а каким-то фреймворком, который оттестирован гораздо лучше вашего приложения.

То есть вот примерно как выглядит код, который вы хотите тестировать:
public FooBar MyMethod(int? fooId, string barId)
{
   var q = GetDbQuery(fooId); 
   var p = GetServiceRequest(barId); 

   var result = CombineFooBar(q.ToList(), p.Execute()); 
   return result;
}

Тогда ваши юнит-тесты не вызывают никаких HTTP-методов. Они даже не вызывают напрямую метод MyMethod, которому нужна куча моков.
У вас есть серии тестов для GetDbQuery, для GetServiceRequest, и для CombineFooBar.
После того, как все эти тесты стали зелёными, а код каверадж показывает хорошее покрытие их реализаций, можно задуматься "а будет ли работать всё это вместе"?
В целом, понятное дело, совершенно не факт — вы могли неправильно понять спецификацию стороннего сервиса; в базе могло не оказаться таблицы с указанным именем; в самом методе MyMethod мы что-то где-то забыли сделать, или невовремя материализовали lazy-результат.
Но все такие вещи гораздо лучше ловить не юнит-, а интеграционными тестами.
Да, эти тесты дорогие — надо развернуть в песочнице настоящую базу, подключиться к тестовому аккаунту на стороннем сервисе, и т.п.
Но! Во-первых, вы не тратите эти усилия на обнаружение банальных ошибок вроде забытых условий в SQL-стейтментах или пропущенных колонок в отображении списка продуктов. Всё это моментально отлавливается юнит-тестами, которые легко запускать и отлаживать прямо на разработчицкой машине, безо всяких песочниц, мокапов и эмуляторов.
Во-вторых, эти тесты ловят гораздо больше реальных косяков продакшна, которые вам никакой мок не замокает. Ну там — вы взяли, создали в стороннем сервисе создали нового пользователя, и тут же приделываете к нему "метод платежа". В юнит тестах (для стейтлесс логики) у вас всё прекрасно, и с моками всё прекрасно. А у реального сервиса есть проблема с задержками распространения — и в проде ваше приложение падает на втором запросе с "user not found", потому что второй запрос приземляется на другом узле CDN или фермы. Получается, что моки тратят ваше время в обмен на иллюзию контроля.

vsb>Проблема в том, что эти тесты очень муторно писать. Разработчики их писать ленятся, не-разработчики таким скиллом не обладают по-большому счёту.

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

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

Ну, если вы приходите в проект, в котором никогда не было никакого тестирования — то да, наверное, моки — это ваш вариант.
Хотя имхо лучше всё же сосредоточиться на покрытии интеграционными тестами; а потом рефакторить код к состоянию из предыдущего пункта, добавляя юнит-тесты по мере возможности.
Их задача, собственно — избежать запуска дорогих интеграционных тестов для отлова примитивных ошибок.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[2]: Как тестировать серверные сервисные приложения?
От: · Великобритания  
Дата: 15.12.22 22:04
Оценка: +1
Здравствуйте, Sinclair, Вы писали:

S> 1. Проверяем, что для всех разрешённых значений параметров нашего API-метода строятся корректные запросы в базу.

Вот это как-то маловато будет. Во-первых, неясно а как собственно проверить, что построенный запрос — корректный? Что нет пропущенной запятой? Во-вторых, то что запрос синтаксически корректный, не означает, что он правильный. Ну да, documentDate >= @startDate добавилось, а надо было добавить documentDate <= @startDate.
По-моему юнит-тесты для dao в общем случае бесполезны. Их лучше делать интеграционными прям с базой. Что-то вроде:
dao.saveDocument(doc1);
dao.saveDocument(doc2);
var result = dao.findDocumentsByDate(startDate);
assertThat(result).is(Set.of(doc1));

А уж потом уже можно юнит-тестировать API, что при вызове метода с такими параметрами дёргается mock dao с нужными аргументами.
avalon/3.0.0
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re: Как тестировать серверные сервисные приложения?
От: SkyDance Земля  
Дата: 16.12.22 04:30
Оценка:
vsb>Сейчас это делается юнит-тестами. Запускается несколько мок-серверов, запускается при старте теста БД, в эту БД код кладёт начальные данные, вызывает нужные эндпоинты, в конце проверяет, вызывались ли нужные эндпоинты у мок-серверов, проверяет, изменились ли данные в БД.

Это не юнит-тесты, у юнит-тестов нет БД и "эндпоинтов" (есть моки оных).

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

Поэтому поступают несколько иначе. Традиционно для программистов, решая проблему путем введения уровня абстракций. Вместо того, чтобы врукопашную писать сценарии, пишут т.н. "property based tests", ибн генераторы тестов. Тестовый фреймворк (скажем, quickcheck) генерирует те самые сценарии: последовательности входных данных. Программисты же реализуют "свойства" (инварианты). Например, "при любых комбинациях входных данных вот тот массив должен быть сортирован по возрастанию". И так далее. Я буквально пару недель назад где-то на этом же форуме описывал, где читать, какие ссылки смотреть и т.п.. один пример, но вообще надо то сообщение найти будет, и зарепостить сюда.

Самое важное свойство этих тестов — они позволяют минимизировать (shrink) сценарий, так что его потом можно будет записать как отдельный кейс и прогонять как minimal repro.
Re[3]: Как тестировать серверные сервисные приложения?
От: Sinclair Россия https://github.com/evilguest/
Дата: 16.12.22 04:30
Оценка:
Здравствуйте, ·, Вы писали:

S>> 1. Проверяем, что для всех разрешённых значений параметров нашего API-метода строятся корректные запросы в базу.

·>Вот это как-то маловато будет. Во-первых, неясно а как собственно проверить, что построенный запрос — корректный? Что нет пропущенной запятой?
Так и проверить — сравниваете SQL с заранее известной строкой.

·>Во-вторых, то что запрос синтаксически корректный, не означает, что он правильный. Ну да, documentDate >= @startDate добавилось, а надо было добавить documentDate <= @startDate.

Ну, эта проблема изоморфна любому косяку юнит-тестов. Вот вы проверяете умножение длинных чисел, и написали в юнит-тесте Assert.Equals(47, 6*7).
Шансы, что вы сделаете одну и ту же ошибку и в юнит тестах, и в коде, малы, но не равны нулю. Ну, да, такое придётся ловить при интеграционных тестах.

·>По-моему юнит-тесты для dao в общем случае бесполезны. Их лучше делать интеграционными прям с базой. Что-то вроде:

·>
·>dao.saveDocument(doc1);
·>dao.saveDocument(doc2);
·>var result = dao.findDocumentsByDate(startDate);
·>assertThat(result).is(Set.of(doc1));
·>

·>А уж потом уже можно юнит-тестировать API, что при вызове метода с такими параметрами дёргается mock dao с нужными аргументами.
Ну, можно и так, но выглядит странно. Интеграционные тесты на порядок-два дороже в запуске, в отличие от юнитов.
Вся идея юнит-тестов — избежать запуска интеграционных тестов для заведомо некорректных билдов. А вы, получается, сначала запускаете интеграционные тесты, а уже потом, если всё сработало, запускаете юниты.
Ну, и смысла в них особого нету — если ассерт выше сработал, то он проверил всю цепочку.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Как тестировать серверные сервисные приложения?
От: · Великобритания  
Дата: 16.12.22 08:23
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>·>Вот это как-то маловато будет. Во-первых, неясно а как собственно проверить, что построенный запрос — корректный? Что нет пропущенной запятой?

S>Так и проверить — сравниваете SQL с заранее известной строкой.
Откуда взять эту известную строку?

S>·>Во-вторых, то что запрос синтаксически корректный, не означает, что он правильный. Ну да, documentDate >= @startDate добавилось, а надо было добавить documentDate <= @startDate.

S>Ну, эта проблема изоморфна любому косяку юнит-тестов. Вот вы проверяете умножение длинных чисел, и написали в юнит-тесте Assert.Equals(47, 6*7).
Тут немного другая проблема. Сравнивать сформированный запрос с известной строкой — это привязываться к конкретной реализации. Текст запроса неважен, главное чтобы он выдавал те результаты, которые мы ожидаем.

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

S>·>А уж потом уже можно юнит-тестировать API, что при вызове метода с такими параметрами дёргается mock dao с нужными аргументами.

S>Ну, можно и так, но выглядит странно. Интеграционные тесты на порядок-два дороже в запуске, в отличие от юнитов.
Логично. Поэтому в dao должно быть как можно меньше логики и тестировать только интеграцию конкретно с базой.

S>Вся идея юнит-тестов — избежать запуска интеграционных тестов для заведомо некорректных билдов. А вы, получается, сначала запускаете интеграционные тесты, а уже потом, если всё сработало, запускаете юниты.

S>Ну, и смысла в них особого нету — если ассерт выше сработал, то он проверил всю цепочку.
Интеграционные тесты dao запускаем, если меняем dao.
И разрабатывать их можно параллельно и независимо.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[5]: Как тестировать серверные сервисные приложения?
От: Sinclair Россия https://github.com/evilguest/
Дата: 16.12.22 11:00
Оценка:
Здравствуйте, ·, Вы писали:

·>Здравствуйте, Sinclair, Вы писали:


S>>·>Вот это как-то маловато будет. Во-первых, неясно а как собственно проверить, что построенный запрос — корректный? Что нет пропущенной запятой?

S>>Так и проверить — сравниваете SQL с заранее известной строкой.
·>Откуда взять эту известную строку?
Сесть и написать. Если вы не знаете, какой SQL порождает ваш data access layer, то это очень плохо — как вы поймёте, что он неверно работает?

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

На мой взгляд, это неудачный подход. Результаты зависят от запроса и от данных, к которым мы его применяем.
То, что корректный запрос возвращает корректный результат, обеспечивается СУБД.

·>Хуже того. Допустим, у тебя 10 таких опциональных параметров и ты накопипастил 10 известных строк, в которых только отличие по одному из параметров.

·>Теперь вдруг понадобилось в результат добавить новую колонку. Значит нужно пройтись по всем этим 10 строкам и скопипастить туда эту колонку.
Совершенно верно. Только не 10, а 2^10. Это если подходить к вопросу в лоб. Очевидно, что такие тесты можно и нужно генерализовать — в частности потому, что select clause в них везде должен быть одинаковый.
Кто мне запретит написать в expected не константу, а выражение типа selectPrefix + "from t1 join t2 on ... where ..."?
·>Или строки сравнивать не на равенство, а фрагменты. Тогда качество тестов совсем падает.
Совершенно верно. А какая альтернатива? Готовить 2^10 тестов, которые проверяют не строки, а датасеты, которые возвращаются из запроса?
Вы умрёте.

·>Логично. Поэтому в dao должно быть как можно меньше логики и тестировать только интеграцию конкретно с базой.

Ну, совсем без логики в dao нельзя. Хотя, опять же — смотря что называть dao. Скажем, как нам проверить, что для админа мы не накладываем row-level security, а для обычного сотрудника есть правило "видны документы только его отдела"?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Как тестировать серверные сервисные приложения?
От: · Великобритания  
Дата: 16.12.22 11:45
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>·>Откуда взять эту известную строку?

S>Сесть и написать.
Как написать? В руками Блокноте? А как убедиться, что хотя бы синтаксис правильный?

S>Если вы не знаете, какой SQL порождает ваш data access layer, то это очень плохо — как вы поймёте, что он неверно работает?

По тестам — выполню и посмотрю что вернулось.

S>·>Тут немного другая проблема. Сравнивать сформированный запрос с известной строкой — это привязываться к конкретной реализации. Текст запроса неважен, главное чтобы он выдавал те результаты, которые мы ожидаем.

S>На мой взгляд, это неудачный подход. Результаты зависят от запроса и от данных, к которым мы его применяем.
Да. Поэтому нужно по сценариям идти. Т.е. сразу пишем два метода — вставка данных и их получение. И тест тогда естественен — добавили данные, потом их запросили и ассертим, что запрощенные данные выглядят как надо.

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

S>То, что корректный запрос возвращает корректный результат, обеспечивается СУБД.

Но самое-то важное — нужно как-то проверить, что запрос — корректный. Как?

S>·>Хуже того. Допустим, у тебя 10 таких опциональных параметров и ты накопипастил 10 известных строк, в которых только отличие по одному из параметров.

S>·>Теперь вдруг понадобилось в результат добавить новую колонку. Значит нужно пройтись по всем этим 10 строкам и скопипастить туда эту колонку.
S>Совершенно верно. Только не 10, а 2^10. Это если подходить к вопросу в лоб. Очевидно, что такие тесты можно и нужно генерализовать — в частности потому, что select clause в них везде должен быть одинаковый.
Воот.. Т.е. не просто известную строку, но какую-то её часть... А корректность части строки уже вообще неясно как надёжно проверять.

S>Кто мне запретит написать в expected не константу, а выражение типа selectPrefix + "from t1 join t2 on ... where ..."?

Т.е. у тебя тесты уже по уши зависят от конкретной реализации. Очень всё хрупко.

S>·>Или строки сравнивать не на равенство, а фрагменты. Тогда качество тестов совсем падает.

S>Совершенно верно. А какая альтернатива? Готовить 2^10 тестов, которые проверяют не строки, а датасеты, которые возвращаются из запроса?
2^10 это не то о чём я говорил. Я говорил о том, что между тестами (не важно их сколько) вариативность обычно в небольшой части. И отделять эту часть придётся искусственно, исходя из конкретной имплементации, делая тесты ещё более хрупкими.

S>·>Логично. Поэтому в dao должно быть как можно меньше логики и тестировать только интеграцию конкретно с базой.

S>Ну, совсем без логики в dao нельзя. Хотя, опять же — смотря что называть dao. Скажем, как нам проверить, что для админа мы не накладываем row-level security, а для обычного сотрудника есть правило "видны документы только его отдела"?
"Для сотрудника X возвращаются только документы D1 и D2", "для сотрудника Y — D1, D3", "Для админа — D1, D2, D3".
А ты как предлагаешь? Грепать текст запроса на наличие определённых join, предикатов, етс?
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[7]: Как тестировать серверные сервисные приложения?
От: Sinclair Россия https://github.com/evilguest/
Дата: 16.12.22 12:10
Оценка: 18 (1)
Здравствуйте, ·, Вы писали:

·>Как написать? В руками Блокноте? А как убедиться, что хотя бы синтаксис правильный?

Ну, а сами-то как думаете?

S>>Если вы не знаете, какой SQL порождает ваш data access layer, то это очень плохо — как вы поймёте, что он неверно работает?

·>По тестам — выполню и посмотрю что вернулось.
Глазами? Или автотестом?

·>Да. Поэтому нужно по сценариям идти. Т.е. сразу пишем два метода — вставка данных и их получение. И тест тогда естественен — добавили данные, потом их запросили и ассертим, что запрощенные данные выглядят как надо.

Эмм. Вы же понимаете, что получается очень тяжёлый сценарий проверки?
Нужно же убедиться, что наличие значения у параметра startDate приводит к тому, что
а) в результате нет ни одного объекта с date < startDate
б) в результате есть все объекты с date >= startDate
в) при этом "все" означает "с другими атрибутами, соответствующими значениям остальных 9 параметров".
Чтобы случайно не получилось так, что у вас в коде ошибка, и проверяется значение совсем другого атрибута. Просто так совпало, что во вставленных данных атрибуты так удачно разлеглись, и

То есть если у вас, скажем, 10 опциональных параметров, то возможно 2^10 вариантов запроса.
Для того, чтобы их по-честному тестировать, вам нужно вставить в исходную таблицу как минимум 2*2^10 записей, и потом написать 2^10 тестов, каждый из которых состоит из довольно сложного набора ассертов про то, какие записи в результате есть, а каких — нету.

Далее — если у вас есть, скажем, параметр, описывающий способ сортировки результата, то вам ещё и придётся писать отдельную проверку того, что ключ сортировки у row(i+1) >= ключу сортировки row(i) для каждого i.
Зачем это всё нужно, когда можно просто проверить корректность формирования запроса по частям, и ничего не выполнять в СУБД?


·>Представь себе ты пишешь какую-нибудь хитрую коллекцию в памяти, с индексами и поиском. Ты же будешь проверять именно поведение этой коллекции, а не то, какие байт-коды там. Вот примерно та же ситуация с dao.

Хитрая коллекция не нуждается в "моках". У неё, в некотором смысле, состояния нет. А то состояние, которое мне нужно, порождается относительно мгновенно, что и позволяет использовать её напрямую в юнит-тестах.
S>>То, что корректный запрос возвращает корректный результат, обеспечивается СУБД.
·>Но самое-то важное — нужно как-то проверить, что запрос — корректный. Как?
Я же написал, как. Что именно вам непонятно?

·>Воот.. Т.е. не просто известную строку, но какую-то её часть... А корректность части строки уже вообще неясно как надёжно проверять.

Непонятны ваши затруднения, если честно.

S>>Кто мне запретит написать в expected не константу, а выражение типа selectPrefix + "from t1 join t2 on ... where ..."?

·>Т.е. у тебя тесты уже по уши зависят от конкретной реализации. Очень всё хрупко.
Конкретной реализации чего?
У меня есть функция GetDbQuery(). Её работа и состоит в том, чтобы генерировать SQL. Именно это я и тестирую. Всё. Если я не знаю, какой SQL порождает моя функция, то как я её написал?
Если у меня SQL генерируется некоей внешней реализацией, которую протестировали до меня, то моя функция порождает не SQL, а некий AST запроса.
Тогда я тестирую не строчки, а устройство AST. А корректность порождения SQL по заданному AST мне до лампочки, т.к. её обеспечивают другие люди. И мы всегда можем заменить одну корректную реализацию AST->SQL на другую корректную реализацию, даже если она генерирует какие-то другие строчки.

Но подчеркну: у меня есть возможность писать такие unit-тесты; как раз потому, что у меня цепочка трансформаций состоит из простых функций с понятными обязанностями.
Вы предпочитаете иметь в качестве "атомов" универсальный всемогутор, который делает сразу всё:
— формирует структуру запроса в терминах какого-то data access api, навроде Hibernate Criteria API
— превращает эту структуру в SQL
— отдаёт SQL на выполнение в СУБД
— отображает полученные от СУБД результаты в ОО-представление

Конечно, такую штуку хрен протестируешь как юнит. Остаётся только унылая дорогостоящая интеграция.

·>2^10 это не то о чём я говорил. Я говорил о том, что между тестами (не важно их сколько) вариативность обычно в небольшой части. И отделять эту часть придётся искусственно, исходя из конкретной имплементации, делая тесты ещё более хрупкими.

Повторюсь: что вы предлагаете в качестве альтернативы?

·>"Для сотрудника X возвращаются только документы D1 и D2", "для сотрудника Y — D1, D3", "Для админа — D1, D2, D3".

·>А ты как предлагаешь? Грепать текст запроса на наличие определённых join, предикатов, етс?
Конечно. Потому что набор возвращённых документов — это стейт; у нас тесты начинают зависеть от того, в каком порядке выполняется инициализация и наполнение данными.
Ну, и с ростом сложности модели описать всё в виде запросов по образцу становится всё сложнее и сложнее.
Кстати, вы здесь имеете в виду запуск этих тестов с реальной БД? Или вы её мокаете в памяти?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[8]: Как тестировать серверные сервисные приложения?
От: · Великобритания  
Дата: 16.12.22 12:52
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>·>Как написать? В руками Блокноте? А как убедиться, что хотя бы синтаксис правильный?

S>Ну, а сами-то как думаете?
Что так делать вообще не надо.

S>>>Если вы не знаете, какой SQL порождает ваш data access layer, то это очень плохо — как вы поймёте, что он неверно работает?

S>·>По тестам — выполню и посмотрю что вернулось.
S>Глазами? Или автотестом?
Автотестом. Я в одном из предыдущих сообщений показал примерный код. Вставить документы через dao, выбрать и проверить, что выбралось только то что надо. Какие при этом гуляют тексты sql — вообще по барабану.

S>·>Да. Поэтому нужно по сценариям идти. Т.е. сразу пишем два метода — вставка данных и их получение. И тест тогда естественен — добавили данные, потом их запросили и ассертим, что запрощенные данные выглядят как надо.

S>Эмм. Вы же понимаете, что получается очень тяжёлый сценарий проверки?
S>Нужно же убедиться, что наличие значения у параметра startDate приводит к тому, что
S>а) в результате нет ни одного объекта с date < startDate
S>б) в результате есть все объекты с date >= startDate
S>в) при этом "все" означает "с другими атрибутами, соответствующими значениям остальных 9 параметров".
S>Чтобы случайно не получилось так, что у вас в коде ошибка, и проверяется значение совсем другого атрибута. Просто так совпало, что во вставленных данных атрибуты так удачно разлеглись, и
Что значит случайно? В тесте прописываешь начальные условия сам, на основе бизнес-требований. Например, в данном случае суём один документ на и случаи <, == и > — и смотрим возвращается ли он для трёх заданных дат.
Ты каким образом собираешься обнаружить опечатку в тексте запроса, где < вместо >? Лично у меня это самая частая ошибка — уверенно пишу условие с точностью наоборот и потом туплю почему же тест красный...

S>То есть если у вас, скажем, 10 опциональных параметров, то возможно 2^10 вариантов запроса.

Да причём тут это? А в твоём случае потребуется 2^10 текстов запроса написать ручками. Чем твои строки помогут-то?

S>Для того, чтобы их по-честному тестировать, вам нужно вставить в исходную таблицу как минимум 2*2^10 записей, и потом написать 2^10 тестов, каждый из которых состоит из довольно сложного набора ассертов про то, какие записи в результате есть, а каких — нету.

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

S>Далее — если у вас есть, скажем, параметр, описывающий способ сортировки результата, то вам ещё и придётся писать отдельную проверку того, что ключ сортировки у row(i+1) >= ключу сортировки row(i) для каждого i.

Просто ассертим, что документы выдаются в ожидаемом порядке.

S>Зачем это всё нужно, когда можно просто проверить корректность формирования запроса по частям, и ничего не выполнять в СУБД?

Затем, что нужно не корректность формирования проверять, а корректность поведения.

S>·>Представь себе ты пишешь какую-нибудь хитрую коллекцию в памяти, с индексами и поиском. Ты же будешь проверять именно поведение этой коллекции, а не то, какие байт-коды там. Вот примерно та же ситуация с dao.

S>Хитрая коллекция не нуждается в "моках". У неё, в некотором смысле, состояния нет. А то состояние, которое мне нужно, порождается относительно мгновенно, что и позволяет использовать её напрямую в юнит-тестах.
Ну да. Вот и воспринимай, что dao это такая хитрая коллекция. Обычно есть возможность писать такие тесты с in-memory dbms.

S>>>То, что корректный запрос возвращает корректный результат, обеспечивается СУБД.

S>·>Но самое-то важное — нужно как-то проверить, что запрос — корректный. Как?
S>Я же написал, как. Что именно вам непонятно?
Я не понял. Твоё преложение assert sql.contains("startDate <= @startDate"). Как проверить, банально, что оператор <= правильный? Не говоря уж об именах колонок.

S>>>Кто мне запретит написать в expected не константу, а выражение типа selectPrefix + "from t1 join t2 on ... where ..."?

S>·>Т.е. у тебя тесты уже по уши зависят от конкретной реализации. Очень всё хрупко.
S>Конкретной реализации чего?
запроса. Если я для удобства введу альяс на табличку "from table1 as t ... where t.startDate..." — все твои тесты посыпятся. Но логика-то не поменялась и поведение то же.

S>У меня есть функция GetDbQuery(). Её работа и состоит в том, чтобы генерировать SQL. Именно это я и тестирую. Всё. Если я не знаю, какой SQL порождает моя функция, то как я её написал?

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

S>Если у меня SQL генерируется некоей внешней реализацией, которую протестировали до меня, то моя функция порождает не SQL, а некий AST запроса.

S>Тогда я тестирую не строчки, а устройство AST. А корректность порождения SQL по заданному AST мне до лампочки, т.к. её обеспечивают другие люди. И мы всегда можем заменить одну корректную реализацию AST->SQL на другую корректную реализацию, даже если она генерирует какие-то другие строчки.


S>Но подчеркну: у меня есть возможность писать такие unit-тесты; как раз потому, что у меня цепочка трансформаций состоит из простых функций с понятными обязанностями.

S>Вы предпочитаете иметь в качестве "атомов" универсальный всемогутор, который делает сразу всё:
S>- формирует структуру запроса в терминах какого-то data access api, навроде Hibernate Criteria API
S>- превращает эту структуру в SQL
S>- отдаёт SQL на выполнение в СУБД
S>- отображает полученные от СУБД результаты в ОО-представление
Это не всемогутер, это называется dao, который ты потом будешь использовать из реального кода. Кнопочка "создать документ" у тебя в итоге дёрнет dao.saveDocument, а кнопочка "найти документ" дёрнет dao.findDocument. Так вот где-то как-то надо проверять, что сохранённый документ ищется как надо.

S>Конечно, такую штуку хрен протестируешь как юнит. Остаётся только унылая дорогостоящая интеграция.

Вот для этого и придумали dao.

S>·>2^10 это не то о чём я говорил. Я говорил о том, что между тестами (не важно их сколько) вариативность обычно в небольшой части. И отделять эту часть придётся искусственно, исходя из конкретной имплементации, делая тесты ещё более хрупкими.

S>Повторюсь: что вы предлагаете в качестве альтернативы?
S>·>"Для сотрудника X возвращаются только документы D1 и D2", "для сотрудника Y — D1, D3", "Для админа — D1, D2, D3".
S>·>А ты как предлагаешь? Грепать текст запроса на наличие определённых join, предикатов, етс?
S>Конечно. Потому что набор возвращённых документов — это стейт; у нас тесты начинают зависеть от того, в каком порядке выполняется инициализация и наполнение данными.
Это часть предусловия теста. Тест так и будет выглядеть:
dao.save(D1);
dao.save(D2);
dao.save(D3);
assert dao.getForUser(X) == Set.of(D1, D2);
assert dao.getForUser(Y) == Set.of(D1, D3);
assert dao.getForUser(Admin) == Set.of(D1, D2, D3);


S>Ну, и с ростом сложности модели описать всё в виде запросов по образцу становится всё сложнее и сложнее.

S>Кстати, вы здесь имеете в виду запуск этих тестов с реальной БД? Или вы её мокаете в памяти?
Обычно есть in-memory dbms. Как вариант, есть субд, которые умеют прикидываться синтаксисом какой-нибудь "настоящей" субд.
Т.е. гоняем "быстрые интеграционные тесты" у себя локально на h2db в памяти, потом коммитим и прогоняем в CI на "большой" бд, на случай наличия различий.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[2]: Как тестировать серверные сервисные приложения?
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 16.12.22 13:39
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>1. Проверяем, что для всех разрешённых значений параметров нашего API-метода строятся корректные запросы в базу.

S>2. Проверяем, что для всех разрешённых значений параметров нашего API-метода строятся корректные запросы в сторонний сервис. И опять — исполнять эти запросы не надо. Надо проверить, что запрос порождается корректно.

А как ты собираешься конкретно эти шаги обеспечивать?
Здесь есть большая проблема — орм или аналогичный тул большей частью не дает посмотреть, что за запрос уйдет в базу. А клиент к стороннему сервису не дает посмотреть, что за запрос уходит.
То есть, здесь понадобится проектировать какой то слой промежуточный или использовать моки "вот здесь мы бы вызвали клиент с такими параметрами"
Re: Как тестировать серверные сервисные приложения?
От: gyraboo  
Дата: 16.12.22 14:00
Оценка: 82 (1) +2
Здравствуйте, vsb, Вы писали:

vsb>Имеется приложение. При старте запускает веб-сервер. Для работы использует СУБД. Принимает некие HTTP запросы с JSON телами и отвечает так же. Также само запрашивает некоторые другие сервисы по HTTP. Также в будущем будет работать с Rabbit MQ.


vsb>Хочется понять, как такое приложение тестировать на верхнем уровне.


vsb>Сейчас это делается юнит-тестами. Запускается несколько мок-серверов, запускается при старте теста БД, в эту БД код кладёт начальные данные, вызывает нужные эндпоинты, в конце проверяет, вызывались ли нужные эндпоинты у мок-серверов, проверяет, изменились ли данные в БД.


vsb>Проблема в том, что эти тесты очень муторно писать. Разработчики их писать ленятся, не-разработчики таким скиллом не обладают по-большому счёту.


vsb>Задача мне кажется типовой и достаточно универсальной. Но не очень понимаю, как гуглить.


Согласен, мокание — это зло. Я последние пару лет пришёл к такому способу: интегротесты сами с помощью библиотеки TestContainers подымают реальное приложение сервиса и реальную БД в докере, и прогоняют все тесты без моков. Типичный интегротест выглядит так:
1. Подготовить фикстуру (тестовые данные в БД)
2. Отправить в сервис REST-запрос
3. Получить респонз и сравнить его с json-файлом ожидаемого респонза (сравнение происходит с помощью библиотеки javacrumbs.jsonunit)

Важно отметить 2 принципиальных момента:
— моков по возможности нет
— сравнение происходит "полное" json-файлов, а не "точечное" объектов DTO, поэтому часто отлавливаются такие ошибки, которые не отлавливаются "точечными" ассертами

Это позволяет писать малое количество простых и понятных тестов, покрывающих максимум кода.
И читать и поддерживать такие тесты проще, т.к. они простые и тупые как пробка, без ада моков и точечных сравнений.
Такие тесты также служат наглядным руководством к сервису, т.к. четко видно. что мы отсылаем в сервис и что ожидаем.
Ещё удобно то, что коммиты в expected json-файлы наглядко показывают, что меняется в сервисе с токи зрения конечного потребителя, что позволяет часто просто взглянув на Мерж Реквест, понять как именно поменялся респонз сервиса, без необходимости ревьюверу подымать сервис и прогонять какие-то тесты Постманом.

Также этот подход позволяет тестировать ситуации с транзакциями и блокировками, т.к. тесты работают против реальной БД и реального сервиса, с реально поднятым "настоящим" TX-манагером. На моках, на базе H2, на тестовых профилях и прочих упрощениях тестирование таких кейсов практически нереально.
Отредактировано 16.12.2022 14:09 gyraboo . Предыдущая версия . Еще …
Отредактировано 16.12.2022 14:06 gyraboo . Предыдущая версия .
Отредактировано 16.12.2022 14:05 gyraboo . Предыдущая версия .
Отредактировано 16.12.2022 14:05 gyraboo . Предыдущая версия .
Отредактировано 16.12.2022 14:03 gyraboo . Предыдущая версия .
Отредактировано 16.12.2022 14:00 gyraboo . Предыдущая версия .
Re[2]: Как тестировать серверные сервисные приложения?
От: · Великобритания  
Дата: 16.12.22 14:31
Оценка:
Здравствуйте, gyraboo, Вы писали:

G> — сравнение происходит "полное" json-файлов, а не "точечное" объектов DTO, поэтому часто отлавливаются такие ошибки, которые не отлавливаются "точечными" ассертами

Это другая крайность. В итоге у тебя 10000 json-файлов и вдруг при добавлении какого-то поля куда-то в общее место тебе внезапно надо поменять over 9000 из них.
Да, такие тесты нужны, но их должно быть очень мало, и тестировать они должны только несколько "самых важных" сценариев. Всё остальное — точечные тесты.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[3]: Как тестировать серверные сервисные приложения?
От: gyraboo  
Дата: 16.12.22 14:54
Оценка:
Здравствуйте, ·, Вы писали:

G>> — сравнение происходит "полное" json-файлов, а не "точечное" объектов DTO, поэтому часто отлавливаются такие ошибки, которые не отлавливаются "точечными" ассертами

·>Это другая крайность. В итоге у тебя 10000 json-файлов и вдруг при добавлении какого-то поля куда-то в общее место тебе внезапно надо поменять over 9000 из них.

Тестов мало. поэтому менят надо не в 9000 тестах, а в малом кол-ве тестов.

·>Да, такие тесты нужны, но их должно быть очень мало, и тестировать они должны только несколько "самых важных" сценариев. Всё остальное — точечные тесты.


А что мешает объединять в одном таком крупном тесте сразу несколько тестируемых фич? В тестирвоании это называется попарное тестирование.
Re[4]: Как тестировать серверные сервисные приложения?
От: · Великобритания  
Дата: 16.12.22 15:13
Оценка:
Здравствуйте, gyraboo, Вы писали:

G>>> — сравнение происходит "полное" json-файлов, а не "точечное" объектов DTO, поэтому часто отлавливаются такие ошибки, которые не отлавливаются "точечными" ассертами

G>·>Это другая крайность. В итоге у тебя 10000 json-файлов и вдруг при добавлении какого-то поля куда-то в общее место тебе внезапно надо поменять over 9000 из них.
G>Тестов мало. поэтому менят надо не в 9000 тестах, а в малом кол-ве тестов.
Это значит, что тебе повезло и тебе надо тестировать мало сценариев. Какой-то небольшой проект, значит.

G>·>Да, такие тесты нужны, но их должно быть очень мало, и тестировать они должны только несколько "самых важных" сценариев. Всё остальное — точечные тесты.

G>А что мешает объединять в одном таком крупном тесте сразу несколько тестируемых фич? В тестирвоании это называется попарное тестирование.
Это опять же не масштабируется на большие проекты. Небольшое изменение будет ломает несколько мест нескольких крупных тестов и становится очень неочевидно что именно и почему сломалось и что где чинить.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[3]: Как тестировать серверные сервисные приложения?
От: Sinclair Россия https://github.com/evilguest/
Дата: 16.12.22 15:43
Оценка: 15 (1)
Здравствуйте, Pauel, Вы писали:

P>Здесь есть большая проблема — орм или аналогичный тул большей частью не дает посмотреть, что за запрос уйдет в базу.

Если SQL порождается ORM, то не надо тесировать порождение SQL. Надо тестировать порождение запроса для ORM — ведь именно это делает наш код.
P> А клиент к стороннему сервису не дает посмотреть, что за запрос уходит.
Смотря куда смотреть. Да, доступ к HTTP пути и телу могут и не дать — но опять-таки, ваш код же их и не формирует?
Вот, к примеру, какой-нибудь Partner Center API: https://github.com/microsoft/Partner-Center-Java/tree/master/src/main/java/com/microsoft/store/partnercenter
Чтобы к нему обратиться, нам потребуется экземпляр, скажем, ICustomerCollection, из которого мы будем что-то доставать.
При помощи, например, запроса, сформулированного в виде IQuery: https://github.com/microsoft/Partner-Center-Java/blob/master/src/main/java/com/microsoft/store/partnercenter/customers/ICustomerCollection.java#L59
Ну, либо обращаться к customers по ID — тогда мы при помощи вызова ByID (https://github.com/microsoft/Partner-Center-Java/blob/master/src/main/java/com/microsoft/store/partnercenter/customers/ICustomerCollection.java#L42) получаем ICustomerOperations.
В обоих случаях мы можем убедиться, что запрос построен корректно, без вызова удалённого сервиса, и без порождения HTTP запросов.
Нам нужно проверить, что в первом случае мы собрали уместный IQuery, а во втором — то, что построенный нами экземпляр CustomerOperations содержит нужный нам CustomerID.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[5]: Как тестировать серверные сервисные приложения?
От: gyraboo  
Дата: 16.12.22 16:00
Оценка:
Здравствуйте, ·, Вы писали:

G>>>> — сравнение происходит "полное" json-файлов, а не "точечное" объектов DTO, поэтому часто отлавливаются такие ошибки, которые не отлавливаются "точечными" ассертами

G>>·>Это другая крайность. В итоге у тебя 10000 json-файлов и вдруг при добавлении какого-то поля куда-то в общее место тебе внезапно надо поменять over 9000 из них.
G>>Тестов мало. поэтому менят надо не в 9000 тестах, а в малом кол-ве тестов.
·>Это значит, что тебе повезло и тебе надо тестировать мало сценариев. Какой-то небольшой проект, значит.

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

G>>·>Да, такие тесты нужны, но их должно быть очень мало, и тестировать они должны только несколько "самых важных" сценариев. Всё остальное — точечные тесты.

G>>А что мешает объединять в одном таком крупном тесте сразу несколько тестируемых фич? В тестирвоании это называется попарное тестирование.
·>Это опять же не масштабируется на большие проекты. Небольшое изменение будет ломает несколько мест нескольких крупных тестов и становится очень неочевидно что именно и почему сломалось и что где чинить.

Не вижу тут связи с размером проекта. Сервис может иметь сотни эндпоинтов (хотя такой микросервис уже кандидат на разделение), но дизайн контракта спроектирован так, что изменения в DTO максимально изолированы.
Re[9]: Как тестировать серверные сервисные приложения?
От: Sinclair Россия https://github.com/evilguest/
Дата: 16.12.22 16:05
Оценка:
Здравствуйте, ·, Вы писали:
·>Что так делать вообще не надо.
Ну, вам виднее. Если вы не хотите иметь возможности запускать тесты параллельно и обходиться без подключения к базе — то велком.

·>Автотестом. Я в одном из предыдущих сообщений показал примерный код. Вставить документы через dao, выбрать и проверить, что выбралось только то что надо. Какие при этом гуляют тексты sql — вообще по барабану.

Интересно, а как вы будете проверять запрос "вернуть 2 страницу из 100 документов по вот таким критериям"?


·>Что значит случайно? В тесте прописываешь начальные условия сам, на основе бизнес-требований. Например, в данном случае суём один документ на и случаи <, == и > — и смотрим возвращается ли он для трёх заданных дат.

·>Ты каким образом собираешься обнаружить опечатку в тексте запроса, где < вместо >? Лично у меня это самая частая ошибка — уверенно пишу условие с точностью наоборот и потом туплю почему же тест красный...
Смотря что именно я проверяю. Если SQL клеит мой код — то я буду прямо смотреть, что за строчка возвращается.

S>>То есть если у вас, скажем, 10 опциональных параметров, то возможно 2^10 вариантов запроса.

·>Да причём тут это? А в твоём случае потребуется 2^10 текстов запроса написать ручками. Чем твои строки помогут-то?
Строки сделают эти тесты лёгкими и независящими от внешнего стейта, так что все 2^10 выполнятся за единицы секунд.
Их легко писать и легко выполнять.
Честный тест, который проверяет, что вернулись нужные документы и не вернулись ненужные занимает гораздо больше места в коде, выполняется дольше сам по себе, и у него трудности с выполнением параллельно.
С настоящей базой хорошо гонять интеграционные тесты — их мало, они большие, накладные расходы там неважны. И ловят они совсем другие проблемы, чем опечатка вида >/<.
·>Это другая проблема и к тестированию бд отношения не имеет. Не хочу развивать далее эту тему.
Это напрямую относится к тестированию БД. Вы в вашем "юнит" тесте тестируете поведение внешних компонентов, чего делать вовсе не надо.

S>>Далее — если у вас есть, скажем, параметр, описывающий способ сортировки результата, то вам ещё и придётся писать отдельную проверку того, что ключ сортировки у row(i+1) >= ключу сортировки row(i) для каждого i.

·>Просто ассертим, что документы выдаются в ожидаемом порядке.
Покажите код этих ассертов.

S>>Зачем это всё нужно, когда можно просто проверить корректность формирования запроса по частям, и ничего не выполнять в СУБД?

·>Затем, что нужно не корректность формирования проверять, а корректность поведения.
Корректность поведения — это интеграционный тест. Я их поддерживаю обеими руками, но я против идеи называть их юнит-тестами.
При этом получить нормальное покрытие кода интеграционными тестами — это утопия.

·>Ну да. Вот и воспринимай, что dao это такая хитрая коллекция. Обычно есть возможность писать такие тесты с in-memory dbms.

Ценность таких тестов с in-memory dbms, как и с другими моками, колеблется между нулевой и отрицательной. Отрицательность появляется оттого, что такие тесты внушают ложную уверенность в ненужности интеграционных тестов с настоящей DBMS.

S>>Я же написал, как. Что именно вам непонятно?

·>Я не понял. Твоё преложение assert sql.contains("startDate <= @startDate"). Как проверить, банально, что оператор <= правильный? Не говоря уж об именах колонок.
У вас же так и написано — и имена колонок, и оператор. Вы полагаете, что сделаете одну и ту же ошибку и в тесте, и в коде?
А что тогда вам помешает перепутать код ассерта, который проверяет наличие или отсутствие документа?

S>>Конкретной реализации чего?

·>запроса. Если я для удобства введу альяс на табличку "from table1 as t ... where t.startDate..." — все твои тесты посыпятся. Но логика-то не поменялась и поведение то же.
Ценность моих тестов — в том, что они запускаются легким движением руки на машине разработчика и отрабатывают очень быстро.
Поэтому когда вы введете альяс, то у вас сразу покраснеет N тестов. Посмотрев в них, вы мгновенно поймёте, в чём дело, и почините тесты. Делов-то.

·>Да пофиг должно какой именно запрос она порождает, это не важно. Важно, что этот запрос выдаёт нужные мне результаты. Как ты собрался тестировать какие результаты выдаёт твой sql для некоторого набора данных?

Я вообще не собираюсь тестировать то, какие результаты выдаёт мой SQL. За 25 лет опыта работы с SQL я ни разу не сталкивался с тем, чтобы СУБД возвращала данные, не соответствующие запросу.
А вот ситуация, когда люди пишут запрос, не глядя в SQL, и принимают его на основании того, что "а в тестовой базе у нас никогда не бывало документов с кириллицей" или "а у нас ID всегда шли подряд", мне встречалась.

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

·>Это не всемогутер, это называется dao, который ты потом будешь использовать из реального кода. Кнопочка "создать документ" у тебя в итоге дёрнет dao.saveDocument, а кнопочка "найти документ" дёрнет dao.findDocument. Так вот где-то как-то надо проверять, что сохранённый документ ищется как надо.

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

S>>Конечно, такую штуку хрен протестируешь как юнит. Остаётся только унылая дорогостоящая интеграция.

·>Вот для этого и придумали dao.
Да? Никогда бы не подумал.

S>>Конечно. Потому что набор возвращённых документов — это стейт; у нас тесты начинают зависеть от того, в каком порядке выполняется инициализация и наполнение данными.

·>Это часть предусловия теста. Тест так и будет выглядеть:
·>
·>dao.save(D1);
·>dao.save(D2);
·>dao.save(D3);
·>assert dao.getForUser(X) == Set.of(D1, D2);
·>assert dao.getForUser(Y) == Set.of(D1, D3);
·>assert dao.getForUser(Admin) == Set.of(D1, D2, D3);
·>

Ну, надо ещё не забыть зарегистрировать этих трёх пользователей, и D1/D2/D3 — это же не константы какие-то, а сложные документы с кучей свойств.
Вы показали 1% кода теста. При этом тест "с джойнами и предикатами" можно привести целиком
И ваш тест нельзя исполнять параллельно с другими тестами — мало ли кто там ещё какие документы сохраняет.

S>>Кстати, вы здесь имеете в виду запуск этих тестов с реальной БД? Или вы её мокаете в памяти?

·>Обычно есть in-memory dbms. Как вариант, есть субд, которые умеют прикидываться синтаксисом какой-нибудь "настоящей" субд.
·>Т.е. гоняем "быстрые интеграционные тесты" у себя локально на h2db в памяти, потом коммитим и прогоняем в CI на "большой" бд, на случай наличия различий.
Как по мне, так это те самые моки, которые являются следствием неверно выбранной архитектуры.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[10]: Как тестировать серверные сервисные приложения?
От: · Великобритания  
Дата: 16.12.22 17:41
Оценка: :)
Здравствуйте, Sinclair, Вы писали:

S>·>Что так делать вообще не надо.

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

S>·>Автотестом. Я в одном из предыдущих сообщений показал примерный код. Вставить документы через dao, выбрать и проверить, что выбралось только то что надо. Какие при этом гуляют тексты sql — вообще по барабану.

S>Интересно, а как вы будете проверять запрос "вернуть 2 страницу из 100 документов по вот таким критериям"?
А что уж не из миллиарда? Кол-во документов на странице — это параметр. Вот и достаточно протестировать, из 4 документов 3 фильтруются и третий выводится на 2й странице, при заданном размере страницы в 2 документа.

S>·>Что значит случайно? В тесте прописываешь начальные условия сам, на основе бизнес-требований. Например, в данном случае суём один документ на и случаи <, == и > — и смотрим возвращается ли он для трёх заданных дат.

S>·>Ты каким образом собираешься обнаружить опечатку в тексте запроса, где < вместо >? Лично у меня это самая частая ошибка — уверенно пишу условие с точностью наоборот и потом туплю почему же тест красный...
S>Смотря что именно я проверяю. Если SQL клеит мой код — то я буду прямо смотреть, что за строчка возвращается.
Ну возвращается строчка c "<" — она правильная или нет? Я хочу видеть в тесте видеть "ищем документы не старее 2020 года" — и проверить, что возвращается документ 2020 и 2025, но не возвращается 2019. Это очевидно верно просто посмотрев на текст теста. А то что твоя строчка содержит "<" — ничего неясно и в итоге works as coded.

S>·>Да причём тут это? А в твоём случае потребуется 2^10 текстов запроса написать ручками. Чем твои строки помогут-то?

S>Строки сделают эти тесты лёгкими и независящими от внешнего стейта, так что все 2^10 выполнятся за единицы секунд.
S>Их легко писать и легко выполнять.
Ок, можно писать тесты для какого-то нетривиального случая хитрого query-builder, но избежать тестов с субд ты не можешь.

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

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

S>С настоящей базой хорошо гонять интеграционные тесты — их мало, они большие, накладные расходы там неважны. И ловят они совсем другие проблемы, чем опечатка вида >/<.

Ты так и не рассказал как ты будешь ловить эту опечатку без выполнения запроса. Если тупо сравнивать строки, ты эту опечатку просто скопипастишь из кода в тест. Т.к. интерпретация sql в голове, по крайней мере у меня, всегда идёт с ошибками. Я считаю что этот запрос делает, что надо, но на самом деле он делает ровно противоположное.

S>·>Это другая проблема и к тестированию бд отношения не имеет. Не хочу развивать далее эту тему.

S>Это напрямую относится к тестированию БД. Вы в вашем "юнит" тесте тестируете поведение внешних компонентов, чего делать вовсе не надо.
Сколько конкретно комбинаций нужно тестировать и как тестировать нужные комбинации минимум тестов — это другой вопрос.

S>>>Далее — если у вас есть, скажем, параметр, описывающий способ сортировки результата, то вам ещё и придётся писать отдельную проверку того, что ключ сортировки у row(i+1) >= ключу сортировки row(i) для каждого i.

S>·>Просто ассертим, что документы выдаются в ожидаемом порядке.
S>Покажите код этих ассертов.
assert dao.getDocListByDate() == List.of(d1, d3, d2)
assert dao.getDocListByName() == List.of(d1, d2, d3)
или типа того.

S>·>Затем, что нужно не корректность формирования проверять, а корректность поведения.

S>Корректность поведения — это интеграционный тест. Я их поддерживаю обеими руками, но я против идеи называть их юнит-тестами.
Я их не разывал юнит-тестами. Я говорил, что юнит-тесты для dao не нужны и даже немножечко вредны.

S>При этом получить нормальное покрытие кода интеграционными тестами — это утопия.

Не всего кода, а dao конкретно.

S>·>Ну да. Вот и воспринимай, что dao это такая хитрая коллекция. Обычно есть возможность писать такие тесты с in-memory dbms.

S>Ценность таких тестов с in-memory dbms, как и с другими моками, колеблется между нулевой и отрицательной. Отрицательность появляется оттого, что такие тесты внушают ложную уверенность в ненужности интеграционных тестов с настоящей DBMS.
Я же говорю, ровно эти же тесты гоняются потом с настоящей dbms по ночам в CI. А in-memory для быстрого девелопинга локально.

S>>>Я же написал, как. Что именно вам непонятно?

S>·>Я не понял. Твоё преложение assert sql.contains("startDate <= @startDate"). Как проверить, банально, что оператор <= правильный? Не говоря уж об именах колонок.
S>У вас же так и написано — и имена колонок, и оператор. Вы полагаете, что сделаете одну и ту же ошибку и в тесте, и в коде?
Почему нет-то? Ибо "<" и в тесте и в коде выглядит одинаково.

S>А что тогда вам помешает перепутать код ассерта, который проверяет наличие или отсутствие документа?

Это сложнее. Это можно красиво в тесте оформить:
var doc19 = newDoc("2019");
var doc20 = newDoc("2020");
var doc25 = newDoc("2025");
dao.save(doc19);
dao.save(doc20);
dao.save(doc25);
assert dao.findNewDocuments("2020") == Set.of(doc20, doc25);

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

S>>>Конкретной реализации чего?

S>·>запроса. Если я для удобства введу альяс на табличку "from table1 as t ... where t.startDate..." — все твои тесты посыпятся. Но логика-то не поменялась и поведение то же.
S>Ценность моих тестов — в том, что они запускаются легким движением руки на машине разработчика и отрабатывают очень быстро.
S>Поэтому когда вы введете альяс, то у вас сразу покраснеет N тестов. Посмотрев в них, вы мгновенно поймёте, в чём дело, и почините тесты. Делов-то.
Это называется хрупкие тесты.

S>·>Да пофиг должно какой именно запрос она порождает, это не важно. Важно, что этот запрос выдаёт нужные мне результаты. Как ты собрался тестировать какие результаты выдаёт твой sql для некоторого набора данных?

S>Я вообще не собираюсь тестировать то, какие результаты выдаёт мой SQL. За 25 лет опыта работы с SQL я ни разу не сталкивался с тем, чтобы СУБД возвращала данные, не соответствующие запросу.
Да повторяю, проблема не в том, что субд возвращает несоответствующие данные, а в том, что запрос несоответствующий.

S>А вот ситуация, когда люди пишут запрос, не глядя в SQL, и принимают его на основании того, что "а в тестовой базе у нас никогда не бывало документов с кириллицей" или "а у нас ID всегда шли подряд", мне встречалась.

S>То, что у меня готовое приложение выдаёт то, чего ожидает пользователь, я проверяю интеграционным тестом. Который выполняется, естественно, уже после того, как я убедился, что отдельные компоненты ведут себя корректно.
Через готовое приложение тестить запросы ещё сложнее и медленнее.
Поэтому я и говорю об интеграционных тестах конкретно dao — интеграция ЯП и СУБД.

S>·>Это не всемогутер, это называется dao, который ты потом будешь использовать из реального кода. Кнопочка "создать документ" у тебя в итоге дёрнет dao.saveDocument, а кнопочка "найти документ" дёрнет dao.findDocument. Так вот где-то как-то надо проверять, что сохранённый документ ищется как надо.

S>А что значит "как надо"? Вы почему-то настаиваете, что тестировать генерацию запроса, его исполнение, и материализацию результата нужно только в пачке. Почему?
Потому что мы видим в тесте что мы кладём в базу и что извлекается ровно то что должно извлечься согласно критериям запроса.

S>>>Конечно. Потому что набор возвращённых документов — это стейт; у нас тесты начинают зависеть от того, в каком порядке выполняется инициализация и наполнение данными.

S>·>Это часть предусловия теста. Тест так и будет выглядеть:
S>·>
S>·>dao.save(D1);
S>·>dao.save(D2);
S>·>dao.save(D3);
S>·>assert dao.getForUser(X) == Set.of(D1, D2);
S>·>assert dao.getForUser(Y) == Set.of(D1, D3);
S>·>assert dao.getForUser(Admin) == Set.of(D1, D2, D3);
S>·>

S>Ну, надо ещё не забыть зарегистрировать этих трёх пользователей, и D1/D2/D3 — это же не константы какие-то, а сложные документы с кучей свойств.
Которые ты так же будешь описывать в любом случае, для твоих интеграционных тестов.

S>Вы показали 1% кода теста. При этом тест "с джойнами и предикатами" можно привести целиком

Ну добавятся ещё userDao.save(X...Y...Admin). Можно иметь некий общий setUp метод, который развернёт общее для большинства тестов.

S>И ваш тест нельзя исполнять параллельно с другими тестами — мало ли кто там ещё какие документы сохраняет.

В случае in-memory — элементарно. Впрочем бывают и другие способы изоляции, зависит от предметной области.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.