¿Cómo hacer para probar el código no inyectable?

13

Así que tengo el siguiente código en uso en todo mi sistema. Actualmente estamos escribiendo pruebas unitarias retrospectivamente (mejor tarde que nunca fue mi argumento), pero no veo cómo esto sería comprobable.

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

Conceptualmente, esto debería ser aplicable a cualquier lenguaje, pero estoy usando PHP. El código simplemente crea un objeto de consulta ElasticSearch, basado en un Searchobjeto, que a su vez está construido a partir de un EmailAlertobjeto. Estos Searchy EmailAlert's son solo POPO's.

Mi problema es que no veo cómo puedo burlarme del SearcherFactory(que usa el método estático), ni del SearchEntityToQueryAdapter, que necesita los resultados SearcherFactory::getSearchDirector y la Searchinstancia. ¿Cómo se inyecta algo que se genera a partir de resultados dentro de un método? ¿Tal vez hay algún patrón de diseño que no conozco?

¡Gracias por cualquier ayuda!

iLikeB Breakfast
fuente
@DocBrown se está utilizando dentro de la $this->context->addViolationllamada, dentro del if.
iLikeBreakfast
1
Debe haber sido ciego, lo siento.
Doc Brown
Entonces, ¿todos los :: son estáticos?
Ewan
Sí, en PHP ::es para métodos estáticos.
Andy
@Ewan sí, ::llama a un método estático en la clase.
iLikeBreakfast

Respuestas:

11

Hay algunas posibilidades, cómo burlarse de los staticmétodos en PHP, la mejor solución que he usado es la biblioteca AspectMock , que se puede extraer a través del compositor (cómo burlarse de los métodos estáticos es bastante comprensible de la documentación).

Sin embargo, es una solución de último minuto para un problema que debería solucionarse de una manera diferente.

Si aún desea probar la capa responsable de la transformación de las consultas, hay una forma bastante rápida de hacerlo.

Supongo que en este momento el validatemétodo es parte de alguna clase, la solución muy rápida, que no requiere que transforme todas sus llamadas estáticas a llamada de instancia, es construir clases que actúen como proxies para sus métodos estáticos e inyectar estos proxies en clases que anteriormente usaba los 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
fuente
¡Esto es perfecto! Ni siquiera pensé en proxies. ¡Gracias!
iLikeBreakfast
2
Creo que Michael Feather se refirió a esto como la técnica "Wrap Static" en su libro "Working Effectively with Legacy Code".
RubberDuck
1
@RubberDuck No estoy completamente seguro de que se llame proxy, para ser honesto. Así es como lo he llamado desde que tengo memoria de haberlo usado, el nombre del Sr. Feather probablemente sea más adecuado, aunque no he leído el libro.
Andy
1
La clase misma es ciertamente un "proxy". La técnica de ruptura de dependencia se llama IIRC "wrap static". Recomiendo mucho el libro. Está lleno de gemas como las que has proporcionado aquí.
RubberDuck
55
Si su trabajo implica agregar pruebas unitarias al código, entonces "trabajar con código heredado" es un libro muy recomendable. Su definición de "código heredado" es "código sin pruebas unitarias", el libro completo es, de hecho, estrategias para agregar pruebas unitarias al código no probado existente.
Eterm
4

Primero, sugeriría dividir esto en 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
        );
    }
}

Esto lo deja en una situación en la que puede considerar hacer públicos esos dos nuevos métodos y realizar pruebas unitarias QueryTotale ShowMessageWhenTotalExceedsMaximumindividuales. Una opción viable aquí es en realidad no hacer una prueba unitaria QueryTotal, ya que esencialmente probaría solo ElasticSearch. Escribir una prueba unitaria ShowMessageWhenTotalExceedsMaximumdebería ser fácil y tiene mucho más sentido, ya que en realidad pondría a prueba la lógica de su negocio.

Sin embargo, si prefiere probar "validar" directamente, considere pasar la función de consulta como un parámetro a "validar" (con un valor predeterminado de $this->QueryTotal), esto le permitirá simular la función de consulta. No estoy seguro de tener la sintaxis PHP correcta, por lo que en caso de que no lo haya hecho, léalo como "Pseudocódigo":

public function validate($value, Constraint $constraint, $queryFunc=$this->QueryTotal)
{
    $totalCount =  $queryFunc($value);
    ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint);
}
Doc Brown
fuente
Me gusta la idea, pero quiero mantener el código más orientado a objetos en lugar de pasar métodos como este.
iLikeBreakfast
@iLikeB Breakfast en realidad este enfoque es bueno independientemente de cualquier otra cosa. Un método debe ser lo más breve posible y hacer una cosa y una cosa bien (tío Bob, código limpio ). Esto hace que sea más fácil de leer, más fácil de entender y más fácil de probar.