Wie teste ich nicht injizierbaren Code?

13

Daher wird auf meinem gesamten System der folgende Code verwendet. Wir schreiben derzeit nachträglich Unit-Tests (besser spät als nie zuvor), aber ich sehe nicht ein, wie dies testbar wäre?

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

Konzeptionell sollte dies auf jede Sprache anwendbar sein, aber ich benutze PHP. Der Code baut einfach ein ElasticSearch-Abfrageobjekt auf, das auf einem SearchObjekt basiert , das wiederum aus einem EmailAlertObjekt aufgebaut ist. Dies Searchund EmailAlertdas sind nur POPOs.

Mein Problem ist, dass ich nicht sehe, wie ich das SearcherFactory(das die statische Methode verwendet), noch das SearchEntityToQueryAdapter, das die Ergebnisse von SearcherFactory::getSearchDirector und die SearchInstanz benötigt, verspotten kann . Wie injiziere ich etwas, das aus Ergebnissen innerhalb einer Methode erstellt wird? Vielleicht gibt es ein Designmuster, das mir nicht bekannt ist?

Danke für jede Hilfe!

iLikeBreakfast
quelle
@DocBrown wird innerhalb des $this->context->addViolationAufrufs verwendet, innerhalb des if.
iLikeBreakfast
1
Muss blind gewesen sein, sorry.
Doc Brown
Also sind alle :: statisch?
Ewan
Ja, in PHP ::ist das für statische Methoden.
Andy
@Ewan Ja, ::ruft eine statische Methode für die Klasse auf.
iLikeBreakfast

Antworten:

11

Es gibt einige Möglichkeiten, wie man staticMethoden in PHP verspottet . Die beste Lösung, die ich verwendet habe, ist die AspectMock- Bibliothek, die über den Composer abgerufen werden kann (wie man statische Methoden verspottet, ist aus der Dokumentation ziemlich verständlich).

Es handelt sich jedoch um eine kurzfristige Lösung für ein Problem, das auf eine andere Weise behoben werden sollte.

Wenn Sie den Layer, der für die Transformation von Abfragen zuständig ist, dennoch einem Komponententest unterziehen möchten, können Sie dies auf recht schnelle Weise tun.

Ich gehe jetzt davon aus, dass die validateMethode Teil einer Klasse ist. Die sehr schnelle Lösung, bei der Sie nicht alle statischen Aufrufe in Instanzaufrufe umwandeln müssen, besteht darin, Klassen zu erstellen, die als Proxys für Ihre statischen Methoden fungieren, und diese Proxys in Klassen einzufügen die zuvor die statischen Methoden verwendet.

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
quelle
Dies ist perfekt! Ich habe nicht einmal an Proxies gedacht. Vielen Dank!
iLikeBreakfast
2
Ich glaube, Michael Feather hat dies in seinem Buch "Effektiv mit Legacy-Code arbeiten" als "Wrap Static" -Technik bezeichnet.
RubberDuck
1
@RubberDuck Ich bin nicht ganz sicher, ob es Proxy heißt, um ehrlich zu sein. So werde ich es genannt, solange ich mich erinnern kann, wie ich es benutzt habe. Der Name von Mr. Feather ist wahrscheinlich besser geeignet. Ich habe das Buch jedoch nicht gelesen.
Andy
1
Die Klasse selbst ist sicherlich ein "Proxy". Die Abhängigkeitsunterbrechungstechnik wird als "statische Umbruch" -IIRC bezeichnet. Ich kann das Buch nur empfehlen. Es steckt voller Juwelen, wie Sie sie hier bereitgestellt haben.
RubberDuck
5
Wenn Ihr Job das Hinzufügen von Komponententests zum Code umfasst, ist "Arbeiten mit altem Code" ein dringend empfohlenes Buch. Seine Definition von "Legacy-Code" ist "Code ohne Komponententests". Das ganze Buch enthält Strategien zum Hinzufügen von Komponententests zu vorhandenem, nicht getestetem Code.
Eterm
4

Zunächst würde ich vorschlagen, dies in separate Methoden aufzuteilen:

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

Dies versetzt Sie in eine Situation, in der Sie in Betracht ziehen können, diese beiden neuen Methoden öffentlich und Unit-Test QueryTotalund ShowMessageWhenTotalExceedsMaximumeinzeln zu machen. Eine praktikable Option besteht darin, überhaupt keinen Unit-Test durchzuführen QueryTotal, da Sie im Wesentlichen nur ElasticSearch testen würden. Das Schreiben eines Komponententests für ShowMessageWhenTotalExceedsMaximumsollte einfach und viel sinnvoller sein, da dies Ihre Geschäftslogik tatsächlich testen würde.

Wenn Sie "validieren" jedoch lieber direkt testen möchten, überlegen Sie, ob Sie die Abfragefunktion selbst als Parameter an "validieren" übergeben möchten (mit dem Standardwert " $this->QueryTotal). Auf diese Weise können Sie die Abfragefunktion ausspotten. Ich bin mir nicht sicher, ob ich die PHP-Syntax richtig verstanden habe. Falls dies nicht der Fall ist, lesen Sie dies bitte als "Pseudocode":

public function validate($value, Constraint $constraint, $queryFunc=$this->QueryTotal)
{
    $totalCount =  $queryFunc($value);
    ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint);
}
Doc Brown
quelle
Ich mag die Idee, aber ich möchte den Code objektorientierter halten, anstatt Methoden wie diese weiterzugeben.
iLikeBreakfast
@iLikeBreakfast Eigentlich ist dieser Ansatz gut, unabhängig von irgendetwas anderem. Eine Methode sollte so kurz wie möglich sein und eine Sache und eine Sache gut machen (Onkel Bob, Clean Code ). Dies erleichtert das Lesen, das Verstehen und das Testen.