Cette fois, je voudrais vous présenter comment écrire du code difficile à tester unitaire. La langue du code est Java (veuillez lire les autres langues).
J'ai également écrit un article qui refactorise ce que j'ai écrit dans cet article. https://qiita.com/oda-kazuki/items/b66fe3d4efec822497e6
Le degré d'agrégation, le degré de liaison et le degré de complexité cyclique sont également résumés ci-dessous. https://qiita.com/oda-kazuki/items/a16b43dc624429de7db3
Cette fois, le traitement suivant est supposé.
De plus, afin de simplifier la description, la bibliothèque externe de l'interface suivante doit être utilisée.
public class HttpClientFactory {
    /**Renvoie un client pour la communication HTTP*/
    static Client createClient(String domain);
}
public interface Client {
    /**Obtenez avec l'API Web*/
    HttpResult get(String path) throws HttpException;
    /**Publier avec l'API Web*/
    HttpResult post(String path, Json json) throws HttpException;
}
public class HttpResult {
    /**Obtenir le corps de la réponse JSON*/
    public Json body();
}
public class JsonMapper<T> {
    /**Sérialiser l'objet en JSON*/
    public Json serialize(T obj);
    /**Désérialiser JSON en un objet*/
    public T deserialize(Json json);
}
Commençons par créer un cache. Voici ** [Singleton] utilisant un modèle de conception (https://ja.wikipedia.org/wiki/Singleton_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3 ) Faisons-le brillamment avec motif **. Je suis cool d'utiliser des modèles de conception.
Cache.java
public class Cache {
   private static Cache instance = new Cahce();
   //Point d'engagement 1:Utilisation efficace de la carte
   private Map<String,Object> user;
   private String token;
   private String accountId;
   private Cache() {
   }   
   public static Cache getInstance() {
       return instance;
   }
   public void setUser(Map<String,Object> u) {
       this.user = u;
   }
   public Map<String, Object> getUser() {
       return this.user;
   }
   public void setToken(String a) {
       this.token = a;
   }
   public String getToken() {
       return this.token;
   }
   public String setAccountId(String accountId) {
       this.accountId = accountId;
   }
   public String getAccountId() {
       return this.accountId;
   }
}
Ensuite, écrivez le processus.
LoginService.java
public class LoginService {
    //Point d'engagement 2:Initialiser à l'intérieur de la classe
    private JsonMapper<Map<String,Object>> mapper = new JsonMapper<>();
    //Point d'engagement 1:Utiliser des variables membres en vain
    private HttpException ex = null;
    private Map<String,Object> user = null;
    private Map<String,Object> body = null;
    private String accessToken = null;
    private Json json = null;
    public Map<String,Object> login(String accessToken) {
        //Point d'engagement 1:Réutiliser les variables de membre
        this.accessToken = accessToken;
        this.account = null;
        //Point d'engagement 1:Réutiliser les variables locales
        Map<String,Object> user = null;
        this.ex = null;
        boolean result = true;
        this.json = null;
        this.body = null;
        if(this.accessToken != null) {
            getCache().setToken(accessToken);
        } else {
            this.accessToken = getCache().getToken();
        }
        //Point d'engagement 3:Plein de nidification
        for (int i = 0; i < 3; i++) {
            if (getCache().getUser() != null) {
                result = this.logout();
                if (result) {
                    try {
                        //Point d'engagement 1:Je ne sais pas quand la valeur a été entrée dans le corps juste en regardant ici
                        this.getAccountId();
                        if (this.body != null) {
                            this.getAccount();
                            this.account = this.body;
                            if (this.body != null) {
                                getCache().setAccountId(this.body.get("accountId"));
                                this.getUser();
                            }
                        }
                    } catch (HttpException e) {
                        //Point d'engagement 1:Ce n'est pas facile à lire, donc je suis accro aux commentaires comme ça
                        //ex est getAccount, getAccountId,Stocké avec getUser
                        if (this.ex != null) {
                            if(this.ex.getReason() == HttpException.Reason.TIMEOUT) {
                                Thread.sleep(1);
                                continue;
                            } else {
                                //Suspendu en raison d'une autre erreur
                                break;
                            }
                        }
                    }
                    if(this.body != null) {
                        //Arrêter le traitement car l'utilisateur est pris
                        user = this.body;
                        break;
                    }
                } else {
                    //Interrompu car la déconnexion a échoué
                    this.body = null;
                    break;
                }
            }
            try {
                //Point d'engagement 1:SEC? Qu'est-ce que c'est?
                this.getAccountId();
                if (this.body != null) {
                    this.getAccount();
                    if (this.body != null) {
                        getCache().setAccountId(this.body.get("accountId"));
                        this.getUser();
                    }
                }
            } catch (HttpException e) {
                //ex est getAccount, getAccountId,Stocké avec getUser
                if (this.ex != null) {
                    if(this.ex.getReason() == HttpException.Reason.TIMEOUT) {
                        Thread.sleep(1);
                        continue;
                    } else {
                        //Suspendu en raison d'une autre erreur
                        break;
                    }
                }
            }
            if(this.body != null) {
                //Arrêter le traitement car l'utilisateur est pris
                user = this.body;
            }
            break;
        }
        if(user != null) {
            this.user = user;
            //Point d'engagement 4:Je gère le cache et je me connecte
            this.getCache().setUser(this.user);
        }
        return this.user;
    }
    //Point d'engagement 4:Méthode différente du rôle d'origine
    private Cache getCache() {
        //Point d'engagement 2:Se référer directement à singleton
        return Cache.getInstance();
    } 
    //Point d'engagement 1:Une méthode qui n'est pas un getter avec un nom de type getter
    //Point d'engagement 4:Une méthode différente du rôle d'origine
    private void getAccountId() throws HttpException {
        try {
            this.ex = null;
            if(getCache().getAccountId() != null) {
                //Point d'engagement 1:Réutiliser les variables membres de différentes manières
                this.body = new HashMap<>();
                this.body.put("accountId", getCache().getAccountId());
            } else {
                if(this.accessToken != null) {
                    //Point d'engagement 1:Réutiliser les variables membres de différentes manières
                    this.body = new HashMap<>();
                    this.body.set("token" , this.accessToken);
                    //Point d'engagement 2:Utilisation de méthodes statiques&Appelez WebAPI directement
                    this.json = Http.createClient("https://account.example.com").post("/auth", this.mapper.serialize(body)).body();
                    this.body = this.mapper.deserialize(this.json);
                }
            }
        } catch (HttpException e) {
            this.body = null;
            this.ex = e;
            throw e;
        }
    }
    //Point d'engagement 1:Une méthode qui n'est pas un getter avec un nom de type getter
    //Point d'engagement 4:Méthode différente du rôle d'origine
    private void getAccount() throws HttpException {
        try {
            
            this.ex = null;
            //Point d'engagement 2:Utilisation de méthodes statiques&Appelez WebAPI directement
            this.json = Http.createClient("https://account.example.com").get("/accounts/" + this.body.get("accountId")).body();
            this.body = this.mapper.deserialize(this.json);
        } catch (HttpException e) {
            this.body = null;
            this.ex = e;
            throw e;
        }
    }
    //Point d'engagement 1:Une méthode qui n'est pas un getter avec un nom de type getter
    //Point d'engagement 4:Méthode différente du rôle d'origine
    private void getUser() throws HttpException {
        try {
            this.ex = null;
            //Point d'engagement 2:Utilisation de méthodes statiques&Appelez WebAPI directement
            this.json = Http.createClient("https://example.com").post("/users", this.mapper.serialize(this.body)).body();
            this.body = this.mapper.deserialize(this.json);
        } catch (HttpException e) {
            this.body = null;
            this.ex = e;
            throw e;
        }
    }
    //Point d'engagement 4:Service de connexion, mais vous pouvez également vous déconnecter
    public boolean logout() {
        this.ex = null;
        if (this.getCache().getUser() != null) {
            this.user = this.getCache().getUser();
            try {
                //Point d'engagement 2:Utilisation de méthodes statiques&Appelez WebAPI directement
                Json json = Http.createClient("https://example.com").post("/logout", this.mapper.serialize(this.user)).body();
            } catch (HttpException e) {
                this.ex = e;
            }
        }
        if (this.ex != null) {
            this.user = null;
            return false;
        } else {
            this.user = null;
            //Point d'engagement 4:Non seulement vous déconnectez, mais supprimez également le cache
            Cache.getInstance().setUser(null);
            Cache.getInstance().setAccountId(null);
            Cache.getInstance().setToken(null);
            return true;
        }
    }
}
J'ai mis quelque chose qui m'a fait trembler la tête. Je pleurerais un peu si on me disait de tester ça. Il semble que je puisse monter à une hauteur plus élevée si je mets en œuvre à fond les points que j'écrirai à partir de maintenant, mais je ne veux pas que quiconque monte trop haut et le perde, alors je vais le laisser à ce niveau cette fois.
Ensuite, j'expliquerai les «points difficiles à tester» sur lesquels j'ai été particulière.
Le but de la création d'un test unitaire est de confirmer qu'il est «selon les spécifications» comme une prémisse majeure. Par conséquent, il est nécessaire de confirmer le résultat attendu sous la forme "Je ne sais pas quel type de traitement est écrit dans le code". Si vous ne le faites pas, vous tomberez dans l'anti-pattern de test de simplement "** vérifier le code écrit et ne pas faire les tests dont vous avez vraiment besoin **".
Cependant, pour réussir le test unitaire, il est nécessaire de savoir quel type de traitement est effectué dans une certaine mesure dans la méthode. En effet, les processus en dehors de la classe qui ne sont pas la cible du test sont créés tout en étant moqués.
Par conséquent, il est assez difficile de faire un test simplement en le rendant illisible. Cette fois, j'ai réduit la lisibilité par la méthode suivante.
Pour ajouter un peu à la fin, par exemple, getAccount () suppose que vous appelez getAccountId (), et le faire seul échouera. Par conséquent, si vous voulez bien comprendre le processus, vous devez vérifier le code ici et là, ce qui contribue à une mauvaise lisibilité.
Toutes les méthodes décrites dans ce LoginService sont combinées avec la méthode login (). Ceci est directement lié à la difficulté des tests.
Pour créer un lien étroit, nous avons fait ce qui suit:
En utilisant ces derniers, vous pouvez rendre le test difficile à moins que vous ne le fassiez de force Mock en utilisant "PowerMock", etc. lors du test. S'il s'agit d'un langage qui ne peut pas être converti de force en Mock, il n'y a pas d'autre choix que d'abandonner et d'autoriser la connexion à l'API Web. Toutefois, dans ce cas, le test échouera en fonction de l'état car il dépend des données côté serveur Web. Je suis engourdie. ~~ En fait, je voulais inclure le couplage de contrôle, etc., mais j'ai arrêté parce que c'était gênant ~~.
En raison de l'utilisation des instructions if etc. en vain, la complexité cyclique est d'environ 20 (bien qu'elle ne soit pas mesurée correctement).
Cela signifie que vous devez tester au moins 20 modèles, et pour obtenir le bon itinéraire, vous devrez en faire un simulacre comme si vous enfiliez une aiguille. Avoir une méthode privée ajoute également à la difficulté. Vous pouvez voir comment cela devient fou quand il est créé.
Ce service de connexion a tout le traitement requis pour la connexion par lui-même, et la finition est très faible en agrégation. Étant donné que le nombre de lignes est encore petit maintenant, il est difficile de l'appeler une classe divine, mais il y a une possibilité.
Ce service de connexion a au moins les rôles suivants:
Ensemble, vous souhaitez simplement ** tester votre flux de connexion **, mais vous devez prendre en compte les requêtes d'API Web, la logique de déconnexion, la mise en cache et toutes sortes de ** bruit **. perdre. De plus, le degré élevé de couplage réduira la valeur SAN lors de l'écriture des tests.
La prochaine fois, j'essaierai de rendre ce code un peu plus facile à tester.
Recommended Posts