Come fare per testare il codice non iniettabile?


13

Quindi ho il seguente codice in uso in tutto il mio sistema. Attualmente stiamo scrivendo i test unitari in modo retrospettivo (meglio tardi che mai è stato il mio argomento), ma non vedo come questo sarebbe testabile?

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

Concettualmente questo dovrebbe essere applicabile a qualsiasi lingua, ma sto usando PHP. Il codice crea semplicemente un oggetto query ElasticSearch, basato su un Searchoggetto, che a sua volta viene creato da un EmailAlertoggetto. Questi Searche EmailAlert'solo POPO.

Il mio problema è che non vedo come posso deridere la SearcherFactory(che utilizza il metodo statico), né il SearchEntityToQueryAdapter, che ha bisogno i risultati SearcherFactory::getSearchDirector e l' Searchistanza. Come iniettare qualcosa che viene creato dai risultati all'interno di un metodo? Forse c'è qualche modello di design di cui non sono a conoscenza?

Grazie per qualsiasi aiuto!


@DocBrown viene utilizzato all'interno della $this->context->addViolationchiamata, all'interno di if.
iLikeBreakfast

1
Deve essere stato cieco, scusa.
Doc Brown,

Quindi tutti i :: sono statici?
Ewan,

Sì, in PHP ::è per metodi statici.
Andy,

@Ewan sì, ::chiama un metodo statico sulla classe.
iLikeBreakfast

Risposte:


11

Ci sono alcune possibilità, come deridere i staticmetodi in PHP, la migliore soluzione che ho usato è la libreria AspectMock , che può essere estratta dal compositore (come deridere i metodi statici è abbastanza comprensibile dalla documentazione).

Tuttavia, è una soluzione dell'ultimo minuto per un problema che dovrebbe essere risolto in modo diverso.

Se vuoi ancora testare l'unità del layer responsabile della trasformazione delle query, c'è un modo abbastanza veloce per farlo.

Sto assumendo in questo momento che il validatemetodo fa parte di alcune classi, la soluzione molto rapida, che non richiede di trasformare tutte le chiamate statiche in chiamate di istanza, è quella di creare classi che fungono da proxy per i metodi statici e iniettare questi proxy in classi che in precedenza utilizzava i metodi statici.

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

Questo è perfetto! Non ho nemmeno pensato ai proxy. Grazie!
iLikeBreakfast

2
Credo che Michael Feather abbia definito questa tecnica "Wrap Static" nel suo libro "Lavorare efficacemente con il codice legacy".
RubberDuck,

1
@RubberDuck Non sono del tutto sicuro che si chiami proxy, a dire il vero. Questo è come lo chiamo da quando ricordo di averlo usato, il nome di Mr. Feather è probabilmente più adatto, ma non ho letto il libro.
Andy,

1
La classe stessa è certamente un "proxy". La tecnica di rottura delle dipendenze si chiama IIRC "a capo statico". Consiglio vivamente il libro. È pieno di gemme come hai fornito qui.
RubberDuck,

5
Se il tuo lavoro prevede l'aggiunta di unit test al codice, allora "lavorare con il codice legacy" è un libro fortemente raccomandato. La sua definizione di "codice legacy" è "codice senza unit test", l'intero libro è in realtà strategie per l'aggiunta di unit test al codice non testato esistente.
Eterm,

4

Innanzitutto, suggerirei di dividere questo in metodi separati:

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

Questo ti lascia in una situazione in cui puoi prendere in considerazione la possibilità di rendere pubblici questi due nuovi metodi, unit test QueryTotale ShowMessageWhenTotalExceedsMaximumindividualmente. Un'opzione praticabile qui in realtà non è affatto il test unitario QueryTotal, dal momento che essenzialmente testeresti solo ElasticSearch. Scrivere un unit test per ShowMessageWhenTotalExceedsMaximumdovrebbe essere facile e ha molto più senso, dal momento che testerebbe la tua logica aziendale.

Se, tuttavia, si preferisce testare "convalida" direttamente, considerare di passare la funzione di query stessa come parametro in "convalida" (con un valore predefinito di $this->QueryTotal), ciò consentirà di simulare la funzione di query. Non sono sicuro di aver corretto la sintassi di PHP, quindi, nel caso non lo sapessi, ti preghiamo di leggere questo come "codice pseudo":

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

Mi piace l'idea, ma voglio mantenere il codice più orientato agli oggetti invece di passare metodi come questo.
iLikeBreakfast

@iLikeBreakfast in realtà questo approccio è buono indipendentemente da qualsiasi altra cosa. Un metodo dovrebbe essere il più breve possibile e fare una cosa e una cosa bene (zio Bob, codice pulito ). Ciò semplifica la lettura, la comprensione e la verifica.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.