Como testar o código não injetável?

13

Portanto, tenho o seguinte trecho de código em uso em todo o meu sistema. No momento, estamos escrevendo testes de unidade retrospectivamente (antes tarde do que nunca foi o meu argumento), mas não vejo como isso seria testável?

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
        );
    }
}

Conceitualmente, isso deve ser aplicável a qualquer idioma, mas estou usando PHP. O código simplesmente cria um objeto de consulta ElasticSearch, com base em um Searchobjeto, que por sua vez é criado a partir de um EmailAlertobjeto. Estes Searche EmailAlertsão apenas POPO.

Meu problema é que não vejo como zombar do SearcherFactory(que usa o método estático), nem do SearchEntityToQueryAdapter, que precisa dos resultados SearcherFactory::getSearchDirector e da Searchinstância. Como injeto algo que é construído a partir de resultados em um método? Talvez haja algum padrão de design que eu não conheça?

Obrigado por qualquer ajuda!

iLikeBreakfast
fonte
@DocBrown está sendo usado dentro da $this->context->addViolationchamada, dentro da if.
iLikeBreakfast
1
Deve ter sido cego, desculpe.
Doc Brown
Então, todos os :: são estáticos?
Ewan
Sim, no PHP ::é para métodos estáticos.
209 Andy
@ Ewan sim, ::chama um método estático na classe.
ILikeBreakfast

Respostas:

11

Existem algumas possibilidades, como zombar de staticmétodos em PHP, a melhor solução que usei é a biblioteca AspectMock , que pode ser acessada pelo compositor (como zombar de métodos estáticos é bastante compreensível na documentação).

No entanto, é uma correção de última hora para um problema que deve ser corrigido de uma maneira diferente.

Se você ainda deseja testar a camada responsável pela transformação de consultas, há uma maneira bem rápida de fazer isso.

Estou assumindo que agora o validatemétodo faz parte de alguma classe, a correção muito rápida, que não exige que você transforme todas as chamadas estáticas em chamadas de instância, é criar classes agindo como proxies para seus métodos estáticos e injetar esses proxies em classes que usava anteriormente os métodos estáticos.

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
            );
        }
    }
}
Andy
fonte
Isto é perfeito! Nem sequer pensou em proxies. Obrigado!
ILikeBreakfast #
2
Acredito que Michael Feather se referiu a isso como a técnica "Wrap Static" em seu livro "Working Effective with Legacy Code".
precisa
1
@RubberDuck Não tenho muita certeza de que seja chamado proxy, para ser sincero. É assim que eu sou chamado desde que me lembro de usá-lo. O nome do Sr. Feather provavelmente é mais adequado, mas eu não li o livro.
209 Andy
1
A classe em si é certamente um "proxy". A técnica de quebra de dependência é chamada de IIRC "wrap static". Eu recomendo o livro. Está cheio de jóias como você forneceu aqui.
precisa
5
Se seu trabalho envolve adicionar testes de unidade ao código, "trabalhar com código herdado" é um livro altamente recomendado. Sua definição de "código legado" é "código sem testes de unidade", o livro inteiro é de fato estratégias para adicionar testes de unidade ao código não testado existente.
Eterm
4

Primeiro, sugiro dividir isso em métodos separados:

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
        );
    }
}

Isso deixa você em uma situação em que você pode considerar tornar esses dois novos métodos públicos e teste de unidade QueryTotale ShowMessageWhenTotalExceedsMaximumindividualmente. Uma opção viável aqui é, na verdade, não fazer teste de unidade QueryTotal, pois você basicamente testaria apenas o ElasticSearch. Escrever um teste de unidade ShowMessageWhenTotalExceedsMaximumdeve ser fácil e faz muito mais sentido, pois na verdade testaria sua lógica de negócios.

Se, no entanto, você preferir testar "validar" diretamente, considere passar a função de consulta em si como um parâmetro para "validate" (com um valor padrão de $this->QueryTotal), isso permitirá que você zombe da função de consulta. Não tenho certeza se obtive a sintaxe do PHP correta. Caso contrário, leia isto como "Pseudo código":

public function validate($value, Constraint $constraint, $queryFunc=$this->QueryTotal)
{
    $totalCount =  $queryFunc($value);
    ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint);
}
Doc Brown
fonte
Eu gosto da ideia, mas quero manter o código mais orientado a objetos, em vez de passar métodos como esse.
ILikeBreakfast #
@iLikeBreakfast, na verdade, essa abordagem é boa, independentemente de qualquer outra coisa. Um método deve ser o mais curto possível e fazer uma coisa e uma coisa bem (tio Bob, código limpo ). Isso facilita a leitura, o entendimento e o teste.