Jak przejść do testowania kodu nie do wstrzykiwania?

13

Mam więc następujący kod w całym systemie. Obecnie piszemy testy jednostkowe retrospektywnie (lepiej późno niż nigdy nie był mój argument), ale nie rozumiem, jak to byłoby możliwe do przetestowania?

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

Koncepcyjnie powinno to dotyczyć dowolnego języka, ale używam PHP. Kod po prostu tworzy obiekt zapytania ElasticSearch oparty na Searchobiekcie, który z kolei jest zbudowany z EmailAlertobiektu. To Searchi EmailAlertsą tylko POPO.

Moim problemem jest to, że nie widzę, w jaki sposób można wyśmiewać się z SearcherFactory(który wykorzystuje metodę statyczną), ani SearchEntityToQueryAdapter, który potrzebuje wyników SearcherFactory::getSearchDirector i ten Searchprzypadek. Jak wstrzyknąć coś, co powstaje na podstawie wyników w ramach metody? Może jest jakiś wzór, o którym nie wiem?

Dzięki za wszelką pomoc!

iLikeBreakfast
źródło
@DocBrown jest używany wewnątrz $this->context->addViolationpołączenia, wewnątrz if.
iLikeBreakfast
1
Przepraszam, musiał być ślepy.
Doc Brown
Więc wszystkie :: są statyką?
Ewan
Tak, w PHP ::jest to metoda statyczna.
Andy,
@Ewan tak, ::wywołuje metodę statyczną w klasie.
iLikeBreakfast

Odpowiedzi:

11

Istnieje kilka posibilitów, jak wyśmiewać staticmetody w PHP, najlepszym rozwiązaniem, którego użyłem, jest biblioteka AspectMock , którą można przeciągnąć przez kompozytora (jak wyśmiewać metody statyczne jest całkiem zrozumiałe z dokumentacji).

Jest to jednak rozwiązanie w ostatniej chwili problemu, który powinien zostać rozwiązany w inny sposób.

Jeśli nadal chcesz testować jednostkowo warstwę odpowiedzialną za przekształcanie zapytań, istnieje dość szybki sposób, jak to zrobić.

Zakładam, że teraz validatemetoda jest częścią jakiejś klasy, bardzo szybką poprawką, która nie wymaga transformacji wszystkich wywołań statycznych na wywołanie instancji, jest budowanie klas działających jako proxy dla metod statycznych i wstrzykiwanie tych proxy do klas które poprzednio stosowały metody statyczne.

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
źródło
To jest doskonałe! Nawet nie pomyślałem o serwerach proxy. Dzięki!
iLikeBreakfast
2
Wierzę, że Michael Feather nazwał to techniką „Wrap Static” w swojej książce „Efektywnie współpracując ze starszym kodem”.
RubberDuck,
1
@ RubberDuck Nie jestem do końca pewien, czy to się nazywa proxy, szczerze mówiąc. Tak mnie to nazywa tak długo, jak pamiętam, że go używam, imię pana Feather prawdopodobnie lepiej pasuje, chociaż nie czytałem książki.
Andy,
1
Sama klasa jest z pewnością „proxy”. Technika przerywania zależności nazywa się IIRC „wrap static”. Bardzo polecam książkę. Jest pełen klejnotów, takich jak tutaj.
RubberDuck,
5
Jeśli twoje zadanie polega na dodaniu testów jednostkowych do kodu, zdecydowanie zalecana jest „praca ze starszym kodem”. Jego definicja „starszego kodu” to „kod bez testów jednostkowych”, cała książka jest w rzeczywistości strategiami dodawania testów jednostkowych do istniejącego nieprzetestowanego kodu.
Eterm
4

Po pierwsze, proponuję podzielić to na osobne metody:

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

To pozostawia cię w sytuacji, w której możesz rozważyć upublicznienie tych dwóch nowych metod oraz test jednostkowy QueryTotali ShowMessageWhenTotalExceedsMaximumindywidualnie. Opłacalną opcją jest tutaj wcale nie testowanie jednostkowe QueryTotal, ponieważ zasadniczo testowałbyś tylko ElasticSearch. Napisanie testu jednostkowego ShowMessageWhenTotalExceedsMaximumpowinno być łatwe i bardziej sensowne, ponieważ faktycznie przetestowałoby logikę biznesową.

Jeśli jednak wolisz przetestować „sprawdzanie poprawności” bezpośrednio, rozważ przekazanie samej funkcji zapytania jako parametru do „sprawdzania poprawności” (z wartością domyślną $this->QueryTotal), pozwoli to na wyszydzenie funkcji zapytania. Nie jestem pewien, czy mam poprawną składnię PHP, więc jeśli nie, przeczytaj to jako „Pseudo kod”:

public function validate($value, Constraint $constraint, $queryFunc=$this->QueryTotal)
{
    $totalCount =  $queryFunc($value);
    ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint);
}
Doktor Brown
źródło
Podoba mi się ten pomysł, ale chcę, aby kod był bardziej zorientowany obiektowo, zamiast przekazywać takie metody.
iLikeBreakfast
@ iLikeBreakfast tak naprawdę to podejście jest dobre niezależnie od czegokolwiek innego. Metoda powinna być jak najkrótsza i robić jedną rzecz dobrze (Wujek Bob, Clean Code ). Ułatwia to czytanie, zrozumienie i testowanie.