[JAVA] Schreiben Sie Code, der schwer zu testen ist

Überblick

Dieses Mal möchte ich vorstellen, wie man Code schreibt, der schwer zu testen ist. Die Sprache des Codes ist Java (bitte lesen Sie andere Sprachen).

Ich habe auch einen Artikel geschrieben, der das, was ich in diesem Artikel geschrieben habe, umgestaltet. https://qiita.com/oda-kazuki/items/b66fe3d4efec822497e6

Fazit

Der Aggregationsgrad, der Bindungsgrad und der Grad der zyklischen Komplexität sind ebenfalls nachstehend zusammengefasst. https://qiita.com/oda-kazuki/items/a16b43dc624429de7db3

Schwer zu testendes Codebeispiel

Diesmal wird die folgende Verarbeitung angenommen.

Zur Vereinfachung der Beschreibung ist außerdem die externe Bibliothek der folgenden Schnittstelle zu verwenden.

public class HttpClientFactory {
    /**Gibt einen Client für die HTTP-Kommunikation zurück*/
    static Client createClient(String domain);
}

public interface Client {
    /**Holen Sie sich mit Web-API*/
    HttpResult get(String path) throws HttpException;
    /**Beitrag mit Web-API*/
    HttpResult post(String path, Json json) throws HttpException;
}

public class HttpResult {
    /**Holen Sie sich den JSON-Antworttext*/
    public Json body();
}

public class JsonMapper<T> {
    /**Serialisieren Sie das Objekt in JSON*/
    public Json serialize(T obj);
    /**Deserialisieren Sie JSON in ein Objekt*/
    public T deserialize(Json json);
}

Deshalb ist der Code schwer zu testen

Zuerst erstellen wir einen Cache. Hier ist ** [Singleton] mit Entwurfsmuster (https://ja.wikipedia.org/wiki/Singleton_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3 ) Lass es uns mit Muster ** brillant machen. Ich bin cool, Designmuster zu verwenden.

Cache.java


public class Cache {
   private static Cache instance = new Cahce();
   //Verpflichtungspunkt 1:Effektive Nutzung der Karte
   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;
   }
}

Als nächstes schreiben Sie den Prozess.

LoginService.java


public class LoginService {
    //Verpflichtungspunkt 2:Innerhalb der Klasse initialisieren
    private JsonMapper<Map<String,Object>> mapper = new JsonMapper<>();
    //Verpflichtungspunkt 1:Verwenden Sie Mitgliedsvariablen vergeblich
    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) {
        //Verpflichtungspunkt 1:Elementvariablen wiederverwenden
        this.accessToken = accessToken;
        this.account = null;
        //Verpflichtungspunkt 1:Lokale Variablen wiederverwenden
        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();
        }

        //Verpflichtungspunkt 3:Voller Verschachtelung
        for (int i = 0; i < 3; i++) {
            if (getCache().getUser() != null) {
                result = this.logout();
                if (result) {
                    try {
                        //Verpflichtungspunkt 1:Ich weiß nicht, wann der Wert in den Körper eingegeben wurde, indem ich hier nachschaue
                        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) {
                        //Verpflichtungspunkt 1:Es ist nicht leicht zu lesen, deshalb bin ich süchtig danach, so zu kommentieren
                        //ex ist getAccount, getAccountId,Mit getUser gespeichert
                        if (this.ex != null) {
                            if(this.ex.getReason() == HttpException.Reason.TIMEOUT) {
                                Thread.sleep(1);
                                continue;
                            } else {
                                //Wegen eines anderen Fehlers ausgesetzt
                                break;
                            }
                        }
                    }
                    if(this.body != null) {
                        //Beenden Sie die Verarbeitung, da der Benutzer vergeben ist
                        user = this.body;
                        break;
                    }
                } else {
                    //Unterbrochen, weil die Abmeldung fehlgeschlagen ist
                    this.body = null;
                    break;
                }
            }
            try {
                //Verpflichtungspunkt 1:TROCKEN? was ist das?
                this.getAccountId();
                if (this.body != null) {
                    this.getAccount();
                    if (this.body != null) {
                        getCache().setAccountId(this.body.get("accountId"));
                        this.getUser();
                    }
                }
            } catch (HttpException e) {
                //ex ist getAccount, getAccountId,Mit getUser gespeichert
                if (this.ex != null) {
                    if(this.ex.getReason() == HttpException.Reason.TIMEOUT) {
                        Thread.sleep(1);
                        continue;
                    } else {
                        //Wegen eines anderen Fehlers ausgesetzt
                        break;
                    }
                }
            }
            if(this.body != null) {
                //Beenden Sie die Verarbeitung, da der Benutzer vergeben ist
                user = this.body;
            }
            break;
        }
        if(user != null) {
            this.user = user;
            //Verpflichtungspunkt 4:Ich verwalte den Cache und melde mich an
            this.getCache().setUser(this.user);
        }
        return this.user;
    }

    //Verpflichtungspunkt 4:Methode anders als die ursprüngliche Rolle
    private Cache getCache() {
        //Verpflichtungspunkt 2:Beziehen Sie sich direkt auf Singleton
        return Cache.getInstance();
    } 

    //Verpflichtungspunkt 1:Eine Methode, die kein Getter mit einem Getter-ähnlichen Namen ist
    //Verpflichtungspunkt 4:Methode anders als die ursprüngliche Rolle
    private void getAccountId() throws HttpException {
        try {
            this.ex = null;
            if(getCache().getAccountId() != null) {
                //Verpflichtungspunkt 1:Verwenden Sie Mitgliedsvariablen auf verschiedene Weise
                this.body = new HashMap<>();
                this.body.put("accountId", getCache().getAccountId());
            } else {
                if(this.accessToken != null) {
                    //Verpflichtungspunkt 1:Verwenden Sie Mitgliedsvariablen auf verschiedene Weise
                    this.body = new HashMap<>();
                    this.body.set("token" , this.accessToken);
                    //Verpflichtungspunkt 2:Verwendung statischer Methoden&Rufen Sie WebAPI direkt auf
                    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;
        }
    }

    //Verpflichtungspunkt 1:Eine Methode, die kein Getter mit einem Getter-ähnlichen Namen ist
    //Verpflichtungspunkt 4:Methode anders als die ursprüngliche Rolle
    private void getAccount() throws HttpException {
        try {
            
            this.ex = null;
            //Verpflichtungspunkt 2:Verwendung statischer Methoden&Rufen Sie WebAPI direkt auf
            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;
        }
    }

    //Verpflichtungspunkt 1:Eine Methode, die kein Getter mit einem Getter-ähnlichen Namen ist
    //Verpflichtungspunkt 4:Methode anders als die ursprüngliche Rolle
    private void getUser() throws HttpException {
        try {
            this.ex = null;
            //Verpflichtungspunkt 2:Verwendung statischer Methoden&Rufen Sie WebAPI direkt auf
            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;
        }
    }

    //Verpflichtungspunkt 4:Anmeldedienst, aber Sie können sich auch abmelden
    public boolean logout() {
        this.ex = null;
        if (this.getCache().getUser() != null) {
            this.user = this.getCache().getUser();
            try {
                //Verpflichtungspunkt 2:Verwendung statischer Methoden&Rufen Sie WebAPI direkt auf
                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;
            //Verpflichtungspunkt 4:Melden Sie sich nicht nur ab, sondern löschen Sie auch den Cache
            Cache.getInstance().setUser(null);
            Cache.getInstance().setAccountId(null);
            Cache.getInstance().setToken(null);
            return true;
        }
    }
}

Ich legte etwas, das meinen Kopf flattern ließ. Wenn ich gebeten würde, dies zu testen, würde ich ein wenig weinen. Es scheint, dass ich auf eine höhere Höhe klettern kann, wenn ich die Punkte, die ich von nun an schreibe, gründlich umsetze, aber ich möchte nicht, dass jemand zu hoch klettert und es verliert, also werde ich es dieses Mal auf diesem Niveau belassen.

Klebepunkte

Dann werde ich die "Punkte erklären, die schwer zu testen sind", um die es mir besonders ging.

Verpflichtung 1. Verpflichtung zur Lesbarkeit

Der Zweck der Erstellung eines Komponententests besteht darin, zu bestätigen, dass er "gemäß den Spezifikationen" eine wichtige Voraussetzung ist. Daher ist es notwendig, das erwartete Ergebnis in Form von "Ich weiß nicht, welche Art von Verarbeitung im Code geschrieben ist" zu bestätigen. Wenn Sie dies nicht tun, fallen Sie in das Test-Anti-Muster, einfach "** den geschriebenen Code zu überprüfen und nicht die Tests durchzuführen, die Sie wirklich benötigen **".

Um den Komponententest zu bestehen, muss jedoch bekannt sein, welche Art von Verarbeitung in der Methode in gewissem Umfang durchgeführt wird. Dies liegt daran, dass die Prozesse außerhalb der Klasse, die nicht das Ziel des Tests sind, beim Verspotten erstellt werden.

Daher ist es ziemlich schwierig, einen Test nur durch Nichtlesen durchzuführen. Dieses Mal habe ich die Lesbarkeit durch die folgende Methode reduziert.

  1. ** Lokale Variablen zuerst initialisieren und wiederverwenden **
  2. ** Es gibt Variablen, bei denen ich nicht sicher bin, was sie darstellen **
  3. ** Es gibt eine Methode, die getXX schreibt, aber der Wert ist nicht getter **
  4. ** Erstellen Sie unnötige Elementvariablen und verwenden Sie sie für mehrere Zwecke **
  5. ** Ignoriere das DRY-Prinzip **
  6. ** Führen Sie nicht nur eine Prozesseinheit innerhalb einer privaten Methode aus **
  7. ** Karte verwenden, die alles enthalten kann **

Um am Ende ein wenig hinzuzufügen, geht getAccount () beispielsweise davon aus, dass Sie getAccountId () aufrufen, und dies allein schlägt fehl. Wenn Sie den Prozess richtig verstehen möchten, müssen Sie daher hier und da den Code überprüfen, was zu einer schlechten Lesbarkeit beiträgt.

Verpflichtung 2. Verpflichtung zur Abhängigkeit

Alle in diesem LoginService beschriebenen Methoden werden mit der Methode login () kombiniert. Dies hängt direkt mit der Schwierigkeit des Testens zusammen.

Um eine enge Verbindung herzustellen, haben wir Folgendes getan:

Wenn Sie diese verwenden, können Sie den Test erschweren, es sei denn, Sie machen ihn beim Testen mit "PowerMock" usw. zwangsweise zum Verspotten. Wenn es sich um eine Sprache handelt, die nicht zwangsweise in Mock konvertiert werden kann, bleibt keine andere Wahl, als die Web-API-Verbindung aufzugeben und zuzulassen. In diesem Fall schlägt der Test jedoch je nach Status fehl, da er von den Daten auf der Webserverseite abhängt. Ich bin taub. ~~ Eigentlich wollte ich Steuerkopplung usw. einbauen, aber ich hörte auf, weil es problematisch war ~~.

Verpflichtung 3. Verpflichtung zur Verschachtelung (hohe zyklische Komplexität)

Aufgrund der vergeblichen Verwendung von if-Anweisungen usw. beträgt die zyklische Komplexität etwa 20 (obwohl sie nicht richtig gemessen wird).

Dies bedeutet, dass Sie mindestens 20 Muster testen müssen. Um die richtige Route zu finden, müssen Sie sie zu einem Mock machen, als würden Sie eine Nadel einfädeln. Eine private Methode erhöht ebenfalls die Schwierigkeit. Sie können sehen, wie verrückt es wird, wenn es erstellt wird.

Verpflichtung 4. Ziel ist die Klasse Gottes (geringer Grad an Zusammenhalt)

Dieser Anmeldedienst verfügt über die gesamte für die Anmeldung erforderliche Verarbeitung, und das Finish ist in der Aggregation sehr gering. Da die Anzahl der Zeilen jetzt noch gering ist, ist es umständlich, sie als Gottklasse zu bezeichnen, aber es besteht die Möglichkeit.

Dieser Anmeldedienst hat mindestens die folgenden Rollen:

Mit diesen zusammen möchten Sie nur Ihren Anmeldefluss testen, müssen jedoch Web-API-Anforderungen, Abmeldelogik, Caching und alle Arten von Rauschen berücksichtigen. verlieren. Darüber hinaus verringert der hohe Kopplungsgrad den SAN-Wert beim Schreiben von Tests.

damit

Nächstes Mal werde ich versuchen, diesen Code ein wenig einfacher zu testen.

Recommended Posts

Schreiben Sie Code, der schwer zu testen ist
Code, der schwer zu debuggen und zu analysieren ist
Schreiben Sie einfach zu pflegenden Code (Teil 4)
Schreiben Sie einfach zu wartenden Code (Teil 3)
Schreiben wir einen Code, der einfach zu pflegen ist (Teil 2)
Wie man Code schreibt, der objektorientiertes Ruby denkt
So schreiben Sie Testcode mit Basic-Zertifizierung
Denken Sie an Testcode, der durch den Komparator-Test leicht zu verstehen ist
Wie man guten Code schreibt
Schreiben Sie zur Implementierung den Test und codieren Sie den Prozess
Java 14 neue Funktionen, mit denen Code geschrieben werden kann
[Java] Code, der schwer zu bemerken, aber furchtbar langsam ist
So schreiben Sie leicht verständlichen Code [Zusammenfassung 3]
[R Spec on Rails] So schreiben Sie Testcode für Anfänger von Anfängern
Führen Sie RSpec ein und schreiben Sie den Unit-Test-Code
Ich möchte einen Unit Test schreiben!
3 schwierige Punkte für Java Realm
[SpringBoot] So schreiben Sie einen Controller-Test
AtCoder heißt TLE und spricht darüber, wie man schönen Code schreibt
JUnit 5: Wie man Testfälle in enum schreibt
So schreiben Sie einen Komponententest für Spring Boot 2
Verwenden Sie stream, um zu überprüfen, ob SimpleDateFormat threadsicher ist
Dynamisches Schreiben iterativer Testfälle mit test / unit (Test :: Unit)
Ausführung des RSpec-Testcodes
Wie schreibe ich Rails
[Lernen / Ausgeben von Testcode]
Wie schreibe ich Docker-Compose
Wie schreibe ich Mockito
So schreiben Sie eine Migrationsdatei
Für Sie, die beklagen, dass Javas Hauptmethode statisch ist
[Integrationstestcode] So wählen Sie ein Element aus date_select aus