Re[19]: Что такое Dependency Rejection
От: Pauel Беларусь http://blogs.rsdn.org/ikemefula
Дата: 20.12.23 21:03
Оценка: +1
Здравствуйте, Буравчик, Вы писали:

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

P>>1. интеграционные тесты с какой то базой
P>>2. разного сорта моки

Б>Что значит дизайн такой? Это логика приложения такая, как ее по другому задизайнить?


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

P>>для п.2 для разработчиков очень трудно не скатиться в тавтологические тесты вида "метод вызывает другой метод"


Б>С моками можно по-разному тестировать. Правильно — в моках возвращаемое значение фиксировать, а не тестировать факт вызовы.


Похоже, что моки про которые вы говорите, это фаулеровские стабы

Читайте фаулера

Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive


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

P>>Это ничего не меняет — вашем дизайне вариантов нету, и наличие ветвлений их не увеличит.


Б>Не понял, что понимается под вариантами. И почему ветвления не влияют.


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

P>>Проблема в самих зависимостях, их много, и в том, что ваш дизайн мутабельный

P>>Попробуйте отказаться от мутабельного дизайна. Как минимум, три эффекта из четырех легко отделяются. Так что можно упростить до безобразия.

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


Гнаться за иммутабельностью не сильно нужно, главное отделить зависимости, эффекты

Б>Как выделить эффекты, можно продемонстрировать?


У вас их 4 — чтение из бд, вызов сервиса, запись в бд, вызов кафки

Общий подход — эффекты преобразовать в вызовы интерфейса, инстанц которого приходит с параметрами

Потом преобразуете метод в класс, v1,v2,v3 уходят в члены класса

class Logic(v1, v2, v3):  // параметры-значения в конструкторе
  def run(load, load2, commit): // эффекты-зависимости здесь, эдакий аналог импорта
     p = ctx.load(self.v1) 

     if что-то(p):  
       p2 = ctx.load2(self.v2) // load2 - абстракция так себе, должно быть хорошее имя
       p = update(p, p2)

     if что-то(p):   
       ctx.commit(self.v3)
      
     return p


Теперь в тестах вы можете создать n троек значений, m результатов операций
И покроется все одной таблицей тестов n x m

И вам не надо мокать репозитории, запись в кафку, хттп вызов сервиса итд

def do_logic(v1,v2,v3):
  l = Logic(v1,v2,v3)
  p = l.run(
    load = ..., repo  // здесь и ниже хватит простой лямбды
    load2 = ...., svc
    commit = ..., repo+kafka // как вариант, для кафки можно протащить доп. имя, например event
  );

  Return serialize(p)


Теперь do-logic стал линейным, его покроет любой интеграционный код, который в любом случае должен быть

Любители моков правда очень обрадуются, умножат(sic!) Количество тестов за счет проверок "commit вызывается после двух load"
Потом небольшое изменение кода, сломает все такие тесты, даже если результат p у нас гарантированно сохранится

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

do_logic(v1, v2, v3):
  p = repo.get(v1)

  if что-то(p):  
    arg = repo.get(v2) // появился доп параметр, который надо вытащить из бд
    p2 = external_service.request(v2, arg)
    p = update(p, p2)

...


Теперь ваш мок repo.get должен считать вызовы — что его вызвали первый раз с v1, второй раз с v2, и это важно, т.к на втором разе вы должны вернуть кое что другое
И это именно то, чего делать не стоит — в сложной логике ваш мок репозитория будет чудовищем, которое мало будет походить на реальную часть системы
Чем более унивесальная зависимость, тем труднее её мокать, и тем выше шансы на ошибку

Если не хотите возиться с интерфейсом — можно вытащить repo.get и подобное в методы рядом с do_logic. Идея на самом деле так себе, этот подход стоит применять если у вас не сильно много зависимостей
do_logic(v1, v2, v3):
  p = self.load(v1)

  if что-то(p):  
    p2 = self.load2(v2)
    p = update(p, p2)

  if что-то(p):   
    self.commit(v3)

  result = serialize(p) 
  return result


Выглядит компактно — только штука в том, что вам придется мокать сам класс, ну хотя бы через наследника под тесты или сетать все методы и надеятся что ничего не пропустите.
А вот зависимости как параметры вы можете понасоздвать или нагенерить на все случаи жизни
1 ошибки всех сортов
2 пустой результат
3 куча вариантов ответа
4 куча сочетаний того и этого
Отредактировано 21.12.2023 8:57 Pauel . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.