Comment procéder pour tester le code non injectable?

13

J'ai donc le morceau de code suivant utilisé partout dans mon système. Nous écrivons actuellement des tests unitaires rétrospectivement (mieux vaut tard que jamais mon argument), mais je ne vois pas comment cela serait testable?

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

Conceptuellement, cela devrait être applicable à n'importe quel langage, mais j'utilise PHP. Le code construit simplement un objet de requête ElasticSearch, basé sur un Searchobjet, qui à son tour est construit à partir d'un EmailAlertobjet. Ces Searchet EmailAlert« s ne sont que POPO de.

Mon problème est que je ne vois pas comment je peux simuler le SearcherFactory(qui utilise la méthode statique), ni le SearchEntityToQueryAdapter, qui a besoin des résultats SearcherFactory::getSearchDirector et de l' Searchinstance. Comment injecter quelque chose qui se construit à partir des résultats dans une méthode? Peut-être y a-t-il un modèle de conception que je ne connais pas?

Merci pour toute aide!

iLikeBreakfast
la source
@DocBrown, il est utilisé à l'intérieur de l' $this->context->addViolationappel, à l'intérieur du if.
iLikeBreakfast
1
Doit être aveugle, désolé.
Doc Brown
Donc tous les :: sont des statiques?
Ewan
Oui, en PHP, ::c'est pour les méthodes statiques.
Andy
@Ewan yes, ::appelle une méthode statique sur la classe.
iLikeBreakfast

Réponses:

11

Il y a quelques possibilités, comment se moquer des staticméthodes en PHP, la meilleure solution que j'ai utilisée est la bibliothèque AspectMock , qui peut être extraite du composeur (comment se moquer des méthodes statiques est tout à fait compréhensible dans la documentation).

Cependant, c'est un correctif de dernière minute pour un problème qui devrait être résolu d'une manière différente.

Si vous souhaitez toujours tester uniquement la couche responsable de la transformation des requêtes, il existe un moyen assez rapide de le faire.

Je suppose qu'en ce moment, la validateméthode fait partie d'une classe, la solution très rapide, qui ne vous oblige pas à transformer tous vos appels statiques en appel d'instance, consiste à créer des classes agissant en tant que proxy pour vos méthodes statiques et à injecter ces proxy dans des classes qui utilisait auparavant les méthodes statiques.

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
la source
C'est parfait! Je n'ai même pas pensé aux procurations. Merci!
iLikeBreakfast
2
Je crois que Michael Feather a appelé cela la technique "Wrap Static" dans son livre "Working Effectively with Legacy Code".
RubberDuck
1
@RubberDuck Je ne suis pas tout à fait sûr qu'il s'appelle proxy, pour être honnête. C'est ainsi que je l'ai appelé depuis aussi longtemps que je me souvienne de l'utiliser, le nom de M. Feather est probablement mieux adapté, je n'ai pas lu le livre, cependant.
Andy
1
La classe elle-même est certainement un "proxy". La technique de coupure de dépendance est appelée IIRC "wrap static". Je recommande vivement le livre. Il est plein de joyaux comme vous l'avez fourni ici.
RubberDuck
5
Si votre travail consiste à ajouter des tests unitaires au code, alors "travailler avec le code hérité" est un livre fortement recommandé. Sa définition du «code hérité» est «code sans tests unitaires», l'ensemble du livre est en fait des stratégies pour ajouter des tests unitaires au code existant non testé.
Eterm
4

Tout d'abord, je suggère de diviser cela en plusieurs méthodes:

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

Cela vous laisse dans une situation où vous pouvez envisager de rendre ces deux nouvelles méthodes publiques et de test unitaire QueryTotalet ShowMessageWhenTotalExceedsMaximumindividuellement. Une option viable ici est en fait de ne pas tester QueryTotaldu tout du tout, car vous ne testeriez essentiellement qu'ElasticSearch. La rédaction d'un test unitaire ShowMessageWhenTotalExceedsMaximumdevrait être facile et beaucoup plus logique, car elle testerait en fait votre logique métier.

Si, cependant, vous préférez tester "valider" directement, envisagez de passer la fonction de requête elle-même comme paramètre dans "valider" (avec une valeur par défaut de $this->QueryTotal), cela vous permettra de simuler la fonction de requête. Je ne sais pas si j'ai bien compris la syntaxe PHP, donc dans le cas contraire, veuillez lire ceci comme "Pseudo code":

public function validate($value, Constraint $constraint, $queryFunc=$this->QueryTotal)
{
    $totalCount =  $queryFunc($value);
    ShowMessageWhenTotalExceedsMaximum($totalCount,$constraint);
}
Doc Brown
la source
J'aime l'idée, mais je veux garder le code plus orienté objet au lieu de passer autour de méthodes comme celle-ci.
iLikeBreakfast
@iLikeBreakfast en fait, cette approche est bonne indépendamment de toute autre chose. Une méthode doit être aussi courte que possible et bien faire une chose et une chose (Uncle Bob, Clean Code ). Cela le rend plus facile à lire, à comprendre et à tester.