Re[89]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 07.03.24 07:04
Оценка: 18 (1)
Здравствуйте, ·, Вы писали:
P>>·>Только если у тебя какой-то очень частый случай и ты завязываешься на какую-то конкретную реализацию — а это значит, что у тебя тесты не будут тестировать даже регрессию.
P>>Покажите пример регрессии, которая не будет обнаружена этим тестом.
·>Ну загрузится пол таблицы. Или банально твой .top(10) не появится для какой-то конкретной комбинации фильтров, т.к. для этой комбинации решили немного переписать кусочек кода и забыли засунуть .top(10).
Нет. Эта проблема возникает только в том случае, если тестируется весь кусок про генерацию запроса целиком.
Вы предлагаете тестировать этот момент косвенно, через подсчёт реального количества записей, которые вернёт ваш репозиторий, подключенный к моку базы.
Альтернатива — не в том, чтобы сгенерировать 2N комбинаций условий фильтрации, а в том, чтобы распилить функцию построения запроса на две части:
1. Сгенерировать предикатную часть запроса
2. Добавить к любому запросу .top(10).

Тестируем эти две штуки по частям, и всё:
IQueryable<User> buildQuery(UserFilter criteria, int pageSize)
    => buildWhere(criteria).addLimit(pageSize);

Вот этот addLimit мы тестируем отдельным набором тестов, чтобы посмотреть, что он будет делать для отрицательных, нулевых, и положительных значений аргумента. Трёх бесконечно дешёвых юнит-тестов нам хватит для того, чтобы убедиться в его корректной работе с произвольным запросом. Так мы убираем множитель 3 из формулы 3*2N.
Шанс "забыть засунуть .top(10)" у нас ровно в одном месте — конкретно вот в этой glue-функции, которая клеит два куска запроса.
Для того, чтобы этот шанс окончательно устранить, нам достаточно 1 (одного) теста для функции buildQuery — благодаря линейности кода, этот тест покроет нужный нам путь.
А 2^N комбинаций фильтров уезжают в функцию buildWhere, которая тестируется отдельно.
При её тестировании мы точно так же заменяем 2N тестов на 2+2N-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*24*3) получили 3+2+4+2+2+3+1 = 17 тестов. При этом нам не нужны ни моки БД, ни тестовая БД в памяти, в которую запихано большое количество записей с разными комбинациями параметров.
То есть мы имеем 17 мгновенных тестов вместо 144 медленных, и гарантию полного покрытия.
По мере роста количества возможных критериев, разница между O(N) и O(2N) будет ещё больше.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.