Здравствуйте, ·, Вы писали:
P>>·>Только если у тебя какой-то очень частый случай и ты завязываешься на какую-то конкретную реализацию — а это значит, что у тебя тесты не будут тестировать даже регрессию.
P>>Покажите пример регрессии, которая не будет обнаружена этим тестом.
·>Ну загрузится пол таблицы. Или банально твой .top(10) не появится для какой-то конкретной комбинации фильтров, т.к. для этой комбинации решили немного переписать кусочек кода и забыли засунуть .top(10).
Нет. Эта проблема возникает только в том случае, если тестируется весь кусок про генерацию запроса целиком.
Вы предлагаете тестировать этот момент
косвенно, через подсчёт реального количества записей, которые вернёт ваш репозиторий, подключенный к моку базы.
Альтернатива — не в том, чтобы сгенерировать 2
N комбинаций условий фильтрации, а в том, чтобы распилить функцию построения запроса на две части:
1. Сгенерировать предикатную часть запроса
2. Добавить к любому запросу .top(10).
Тестируем эти две штуки по частям, и всё:
IQueryable<User> buildQuery(UserFilter criteria, int pageSize)
=> buildWhere(criteria).addLimit(pageSize);
Вот этот addLimit мы тестируем отдельным набором тестов, чтобы посмотреть, что он будет делать для отрицательных, нулевых, и положительных значений аргумента. Трёх бесконечно дешёвых юнит-тестов нам хватит для того, чтобы убедиться в его корректной работе с
произвольным запросом. Так мы убираем множитель 3 из формулы 3*2
N.
Шанс "забыть засунуть .top(10)" у нас ровно в одном месте — конкретно вот в этой glue-функции, которая клеит два куска запроса.
Для того, чтобы этот шанс окончательно устранить, нам достаточно 1 (одного) теста для функции buildQuery — благодаря линейности кода, этот тест покроет нужный нам путь.
А 2^N комбинаций фильтров уезжают в функцию buildWhere, которая тестируется отдельно.
При её тестировании мы точно так же заменяем 2
N тестов на 2+2
N-1 — режем на части примерно таким образом:
IQueryable<User> buildWhere(UserFilter criteria)
{
var q = db.Users;
if (criteria.Name != null)
q = q.Where(u=>u.Name.Contains(criteria.Name));
...
return q;
}
теперь нам достаточно двух тестов — в одном criteria.Name == null, в другом — не-null. Test Coverage нам покажет, что все строчки покрыты, а сам тест убедится, что у нас там — ровно нужный предикат — Сontains, BeginsWith, EndsWith или что там у нас требовалось по ТЗ.
Со следующим критерием всё будет точно так же — мы расщепляем код на линейно независимые куски, каждый из которых тестируется по отдельности. Если есть боязнь того, что нубас напишет некорректную комбинацию if-ов, вроде такой:
var q = db.Users;
if (criteria.Name != null)
q = q.Where(u => u.Name.Contains(criteria.Name))
else
if (criteria.MinLastLoginTimestamp.HasValue)
q = q.Where(u => u.LastLoginTimeStamp >= criteria.MinLastLoginTimestamp.Value);
...
То пилим функцию на несколько кусков:
IQueryable<User> buildWhere(UserFilter criteria)
=> db.Users
.FilterName(criteria.Name)
.FilterLastLogin(criteria.MinLastLoginTimeStamp, criteria.MaxLastLoginTimeStamp)
.FilterDomain(criteria.Domain)
.FilterGroupMembership(criteria.Groups)
Эта функция — строго линейна, она проверяется ровно одним тестом, который убеждается, что мы не забыли собрать все компоненты предиката.
А каждая из запчастей проверяется двумя или четырьмя тестами для всех нужных нам комбинаций её значений.
Всё, мы вместо 144 тестов (3*2
4*3) получили 3+2+4+2+2+3+1 = 17 тестов. При этом нам не нужны ни моки БД, ни тестовая БД в памяти, в которую запихано большое количество записей с разными комбинациями параметров.
То есть мы имеем 17 мгновенных тестов вместо 144 медленных, и гарантию полного покрытия.
По мере роста количества возможных критериев, разница между O(N) и O(2
N) будет ещё больше.