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