Как пройти тестирование неинъекционного кода?

13

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

public function validate($value, Constraint $constraint)
{
    $searchEntity = EmailAlertToSearchAdapter::adapt($value);

    $queryBuilder = SearcherFactory::getSearchDirector($searchEntity->getKeywords());
    $adapter = new SearchEntityToQueryAdapter($queryBuilder, $searchEntity);
    $query = $adapter->setupBuilder()->build();

    $totalCount = $this->advertType->count($query);

    if ($totalCount >= self::MAXIMUM_MATCHING_ADS) {
        $this->context->addViolation(
            $constraint->message
        );
    }
}

Концептуально это должно быть применимо к любому языку, но я использую PHP. Код просто создает объект запроса ElasticSearch на основе Searchобъекта, который, в свою очередь, создается из EmailAlertобъекта. Это Searchи EmailAlertесть просто ПОПО.

Моя проблема заключается в том , что я не понимаю , как я могу издеваться вне SearcherFactory(который использует статический метод), ни SearchEntityToQueryAdapter, что нужны результаты SearcherFactory::getSearchDirector и на Searchэкземпляр. Как мне внедрить что-то, что создается из результатов в методе? Может быть, есть какой-то шаблон дизайна, о котором я не знаю?

Спасибо за любую помощь!

iLikeBreakfast
источник
@DocBrown он используется внутри $this->context->addViolationвызова, внутри if.
iLikeBreakfast
1
Должно быть, был слеп, извини.
Док Браун
Так что все это статика?
Эван
Да, в PHP ::это для статических методов.
Энди
@ Иван да, ::вызывает статический метод для класса.
iLikeBreakfast

Ответы:

11

Есть некоторые возможности, как смоделировать staticметоды в PHP, лучшее решение, которое я использовал, - это библиотека AspectMock , которую можно получить через composer (как смоделировать статические методы вполне понятно из документации).

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

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

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

class EmailAlertToSearchAdapterProxy
{
    public function adapt($value)
    {
        return EmailAlertToSearchAdapter::adapt($value);
    }
}

class SearcherFactoryProxy
{
    public function getSearchDirector(array $keywords)
    {
        return SearcherFactory::getSearchDirector($keywords);
    }
}

class ClassWithValidateMethod
{
    private $emailProxy;
    private $searcherProxy;

    public function __construct(
        EmailAlertToSearchAdapterProxy $emailProxy,
        SearcherFactoryProxy $searcherProxy
    )
    {
        $this->emailProxy = $emailProxy;
        $this->searcherProxy = $searcherProxy;
    }

    public function validate($value, Constraint $constraint)
    {
        $searchEntity = $this->emailProxy->adapt($value);

        $queryBuilder = $this->searcherProxy->getSearchDirector($searchEntity->getKeywords());
        $adapter = new SearchEntityToQueryAdapter($queryBuilder, $searchEntity);
        $query = $adapter->setupBuilder()->build();

        $totalCount = $this->advertType->count($query);

        if ($totalCount >= self::MAXIMUM_MATCHING_ADS) {
            $this->context->addViolation(
                $constraint->message
            );
        }
    }
}
Энди
источник
Это потрясающе! Даже не думал о прокси. Благодарность!
iLikeBreakfast
2
Я полагаю, что Майкл Фезер в своей книге «Эффективная работа с унаследованным кодом» назвал эту технику «Wrap Static».
RubberDuck
1
@RubberDuck Я не совсем уверен, что это называется прокси, если честно. Это то, что я назвал так долго, насколько я себя помню, имя мистера Фезера, вероятно, лучше подходит, хотя я не читал книгу.
Энди
1
Сам класс, безусловно, является «прокси». Техника разрушения зависимостей называется «статическая обертка» IIRC. Я очень рекомендую книгу. Он полон драгоценных камней, как вы предоставили здесь.
RubberDuck
5
Если ваша работа заключается в добавлении модульных тестов в код, тогда настоятельно рекомендуется «работать с устаревшим кодом». Его определение «унаследованного кода» - «код без модульных тестов», вся книга на самом деле представляет собой стратегии добавления модульных тестов в существующий непроверенный код.
Eterm
4

Во-первых, я бы предложил разделить это на отдельные методы:

public function validate($value, Constraint $constraint)
{
    $totalCount = QueryTotal($value);
    ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint);
}

private function QueryTotal($value)
{
    $searchEntity = EmailAlertToSearchAdapter::adapt($value);

    $queryBuilder = SearcherFactory::getSearchDirector($searchEntity->getKeywords());
    $adapter = new SearchEntityToQueryAdapter($queryBuilder, $searchEntity);
    $query = $adapter->setupBuilder()->build();

    return $this->advertType->count($query);
}

private function ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint)
{
    if ($totalCount >= self::MAXIMUM_MATCHING_ADS) {
        $this->context->addViolation(
            $constraint->message
        );
    }
}

Это оставляет вас в ситуации, когда вы можете рассмотреть возможность сделать эти два новых метода общедоступными, а также модульное тестирование QueryTotalи ShowMessageWhenTotalExceedsMaximumиндивидуально. Жизнеспособный вариант здесь на самом деле не для модульного тестирования QueryTotalвообще, так как вы бы по существу тестировали только ElasticSearch. Написание модульного теста для ShowMessageWhenTotalExceedsMaximumдолжно быть легким и имеет гораздо больший смысл, так как это фактически проверит вашу бизнес-логику.

Однако, если вы предпочитаете тестировать «validate» напрямую, рассмотрите возможность передачи самой функции запроса в качестве параметра в «validate» (со значением по умолчанию $this->QueryTotal), это позволит вам смоделировать функцию запроса. Я не уверен, правильно ли я понял синтаксис PHP, поэтому в случае, если я этого не сделал, прочитайте это как «Псевдокод»:

public function validate($value, Constraint $constraint, $queryFunc=$this->QueryTotal)
{
    $totalCount =  $queryFunc($value);
    ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint);
}
Док Браун
источник
Мне нравится идея, но я хочу сохранить код более объектно-ориентированным, а не передавать методы, подобные этому.
iLikeBreakfast
@iLikeBreakfast на самом деле этот подход хорош независимо от всего остального. Метод должен быть как можно более коротким и хорошо выполнять одно и одно (Дядя Боб, Чистый код ). Это облегчает чтение, понимание и тестирование.