[JAVA] Écrire du code difficile à tester

Aperçu

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

Conclusion

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

Exemple de code difficile à tester

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

C'est pourquoi le code est difficile à tester

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.

Points de blocage

Ensuite, j'expliquerai les «points difficiles à tester» sur lesquels j'ai été particulière.

Engagement 1. Engagement de lisibilité

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.

  1. ** Initialisez et réutilisez d'abord les variables locales **
  2. ** Il y a des variables dont je ne sais pas trop ce que cela représente **
  3. ** Il existe une méthode qui écrit getXX mais la valeur n'est pas getter **
  4. ** Créez des variables membres inutiles et réutilisez-les à des fins multiples **
  5. ** Ignorez le principe DRY **
  6. ** Ne complétez pas une unité de traitement uniquement dans une méthode privée **
  7. ** Utilisez une carte qui peut contenir n'importe quoi **

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é.

Engagement 2. Engagement envers la dépendance

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 ~~.

Engagement 3. Engagement à imbriquer (complexité cyclique élevée)

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éé.

Engagement 4. Vise la classe Dieu (faible degré de cohésion)

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.

alors

La prochaine fois, j'essaierai de rendre ce code un peu plus facile à tester.

Recommended Posts

Écrire du code difficile à tester
Code difficile à déboguer et à analyser
Écrire du code facile à maintenir (partie 4)
Écrire du code facile à maintenir (partie 3)
Écrivons un code facile à maintenir (Partie 2) Nom
Comment écrire du code qui pense Ruby orienté objet
Comment écrire du code de test avec la certification de base
Pensez à un code de test facile à comprendre grâce au test de Comparator
Comment écrire du bon code
Pour implémenter, écrivez le test puis codez le processus
Nouvelles fonctionnalités de Java 14 pouvant être utilisées pour écrire du code
[Java] Code difficile à remarquer mais terriblement lent
Comment rédiger un code facile à comprendre [Résumé 3]
[R Spec on Rails] Comment écrire du code de test pour les débutants par les débutants
Introduire RSpec et écrire le code de test unitaire
Je veux écrire un test unitaire!
3 points difficiles à gérer Java Realm
[SpringBoot] Comment écrire un test de contrôleur
AtCoder s'appelle TLE et explique comment écrire du beau code
JUnit 5: Comment écrire des cas de test dans enum
Comment écrire un test unitaire pour Spring Boot 2
Utilisez stream pour vérifier que SimpleDateFormat est thread unsafe
Comment écrire dynamiquement des cas de test itératifs à l'aide de test / unit (Test :: Unit)
Exécution du code de test RSpec
Comment écrire des rails
[Tester l'apprentissage / la sortie du code]
Comment écrire docker-compose
Comment écrire Mockito
Comment écrire un fichier de migration
A vous qui regrettez que la méthode principale de Java soit statique
[Code de test d'intégration] Comment sélectionner un élément dans date_select