[JAVA] Testbarer Code

Überblick

In diesem Artikel geht es um die Umgestaltung der unten aufgeführten Quellen.

Schreiben Sie Code, der schwer zu testen ist https://qiita.com/oda-kazuki/items/bac33094e82b0f51da41

Testbare Code-Schlussfolgerungen

Lassen Sie uns wie folgt codieren.

Dies erleichtert das Testen erheblich. Lassen Sie uns nun den vorherigen Code umgestalten.

Erhöhen Sie den Aggregationsgrad

Letztes Mal Wie ich schrieb, spielte der Anmeldedienst mehrere Rollen. Zunächst werden wir den schädlichsten von ihnen stoppen, den "direkten Aufruf der Web-API". Lass es uns rausnehmen.

Modell erstellen

Erstellen Sie zuvor jedoch eine Klasse von Kontoinformationen und Benutzerinformationen, die in Map ausgedrückt werden. Eigentlich möchte ich verschiedene Verarbeitungsfunktionen hinzufügen, aber diesmal scheint es nicht besonders verwendet zu werden, so dass es sich wie ein DTO anfühlt.

@Data 
public class AuthToken {
   private String token;
}

@Data 
public class AuthResult {
   private String accountId;
}

@Data
public class Account {
   //Ich werde es diesmal nicht benutzen, aber ich werde es in dem Sinne ausdrücken, dass es so etwas gibt
   private String accountId;
}

@Data
public class User {
   //Ich werde es diesmal nicht benutzen, aber ich werde es in dem Sinne ausdrücken, dass es so etwas gibt
   private String name;
}

Erstellen einer Factory-Methode

Auch dieses Mal verwende ich eine Bibliothek namens "JsonMapper" (Einstellung), aber die Verwendung ist etwas schwierig, da die Generika angegeben werden müssen. Deshalb werden wir auch eine Factory-Methode dafür erstellen. Es ist eine einfache Klasse, die einfach den Ziel-JsonMapper erstellt.

public class JsonMapperFactory {
    public JsonMapper<Account> createAccountMapper() {
        return new JsonMapper<>();
    }

    public JsonMapper<AuthToken> createAuthMapper() {
        return new JsonMapper<>();
    }

    public JsonMapper<AuthResult> createAuthResultMapper() {
        return new JsonMapper<>();
    }

    public JsonMapper<User> createUserMapper() {
        return new JsonMapper<>();
    }    
}

Sperren Sie die Web-API

Erstellen wir also zuerst etwas namens "HttpClient". Beschränken Sie den Teil, der die Web-API direkt aufruft.

public abstract class HttpClient {
    protected final Client client;
    protected final JsonMapperFactory mapperFactory;

    public HttpClient(Client client, JsonMapperFactory factory) {
        this.client = client;
        this.mapperFactory = factory;
    }
}

Erstellen wir nun "AuthClient" und "UserClient", die die Web-API tatsächlich aufrufen.

public class AuthClient extends HttpClient {
    public AuthClient(Client client, JsonMapperFactory factory) {
        super(client, factory);
    }

    public AuthResult authorize(String accessToken) throws HttpException {
        JsonMapper<AuthToken> tokenMapper = factory.createAuthMapper();
        AuthToken token = new AuthToken(accessToken);
        Json json = client.post("/auth" , tokenMapper.serialize(token)).body();

        JsonMapper<AuthResult> resultMapper = factory.createAuthResultMapper();
        return resultMapper.deserialize(json);
    }

    public Account fetchAccount(String accountId) throws HttpException {
        Json json = client.get("/accounts/" + accountId).body();

        JsonMapper<Account> accountMapper = factory.createAccountMapper();
        return accountMapper.deserialize(json);
    }
}

public class UserClient extends HttpClient {
    public UserClient(Client client, JsonMapperFactory factory) {
        super(client, factory);
    }

    public User fetchUser(Account account) throws HttpException {
        JsonMapper<Account> accountMapper = factory.createAccountMapper();
        Json json = client.post("/users" , accountMapper.serialize(account)).body();
        
        JsonMapper<User> userMapper = factory.createUserMapper();
        return userMapper.deserialize(json);
    }

    public void logout(User user) throws HttpException {
        JsonMapper<User> userMapper = factory.createUserMapper();
        client.post("/logout", userMapper.serialize(user));
    }
}

Cache-Klasse korrigieren

Ändern Sie dann auch den Cache. Schließlich wird es als einzelne Tonne verwendet, aber sobald die Beschreibung im Einzeltonnenmuster verloren geht.

public class Cache {
    private User loggedInUser;
    @Getter @Setter
    private String accessToken;
    @Getter 
    private Account loggedInAccount;

    public Cache() {
        this.reset();
    }

    public boolean isLoggedIn() {
        return loggedInUser != null;
    }

    public String cacheToken(String accessToken) {
        if (accessToken != null) {
            this.accessToken = accessToken;
        }
        return this.accessToken;
    }

    public void cacheAll(String accessToken, Account account, User user) {
        this.accessToken = accessToken;
        this.loggedInAccount = account;
        this.loggedInUser = user;
    }

    public void reset() {
        this.accessToken = null;
        this.loggedInAccount = null;
        this.loggedInUser = null;
    }
}

Wenn ich es so betrachte, denke ich, dass es viel Verschwendung ist, aber es ist ein Refactoring, also lassen wir es so wie es ist.

Dependency Injection Jetzt können Sie umgestalten. Vorher werde ich jedoch kurz DI (Dependency Injection) vorstellen, den Schlüssel zum Schreiben von testbarem Code.

Wenn Sie mehr wissen möchten, sollten Sie Folgendes sehen.

Sogar Affen können verstehen! Abhängigkeitsinjektion: Abhängigkeitsinjektion https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f

Dies wird auf Japanisch als "Abhängigkeitsinjektion" bezeichnet. Wie der Name schon sagt, handelt es sich um ein Entwurfsmuster, das abhängige Dateien von außen einfügt.

Der einfachste DI besteht darin, eine Mitgliedsvariable im Konstruktor zu übergeben. Beispielsweise werden die obigen Methoden "AuthClient" und "UserClient" verwendet.

Erneut veröffentlichen


public class UserClient extends HttpClient {
    public UserClient(Client client, JsonMapperFactory factory) {
        super(client, factory);
    }
    // ︙
}

Was ist gut daran?

Beim Testen können Sie die abhängigen Dateien leicht verspotten. Grundsätzlich hängen ** Klassen von Mitgliedsvariablen ** ab. Wenn es sich um Daten handelt, können diese nur als "Informationen" behandelt werden. Wenn dies jedoch von der Methode abhängt, muss sie möglicherweise zum Zeitpunkt des Tests verspottet werden.

Das Abhängigkeitsinjektionsmuster macht es einfach, diese abhängigen Teile beim Testen durch Verspotten zu ersetzen.

Google Guice Für Java gibt es viele leistungsstarke DI-Bibliotheken wie Google Guice und Spring. Verwenden wir diesmal Google Guice.

Anmeldedienst korrigieren

Korrigieren wir nun den Anmeldedienst, den wir dieses Mal testen wollten. Angenommen, es gibt zuvor einen neuen Dienst namens "LogoutService". (Da es bereits problematisch ist, werde ich nur die Schnittstelle schreiben)

public interface LogoutService {
    /**Wenn die Anmeldung erfolgreich ist, löschen Sie den gesamten Cache und geben Sie true zurück. Gibt false zurück, wenn die Abmeldung fehlschlägt*/
    boolean logout();
}

Es ist also ein Anmeldedienst.

public class LoginService {
   @Inject //Abhängigkeitsinjektion in Guice
   private Cache cache;
   @Inject 
   private AuthClient authClient;
   @Inject 
   private UserClient userClient;
   @Inject
   private LogoutService logoutService;

   public User login(String authToken) {
       if(cache.isLoggedIn()) {
           if(!logoutService.logout()) {
               return null;
           }
       }
       String token = cache.cacheToken(authToken);
       return this.executeLogin(token, 0);
   }

   private User executeLogin(String token, int retryCount) {
       if (retryCount >= 3) {
           return null;
       }
       try {
          AuthResult auth = authClient.aurhotize(token);
          
          Account account = cache.getAccount();
          if (account == null) {
              account = authClient.fetchAccount(auth.getAccountId());
          }

          User user = userClient.fetchUser(account);
          if (user == null) {
              return null;
          }

          cache.cacheAll(token, account, user);
          return user;
       } catch (HttpException e) {
          if (e.getReason() == HttpException.Reason.TIMEOUT) {
              return this.executeLogin(authToken, ++retryCount);
          }
          return null;
       }
   }
}

Es ist viel einfacher. Bei Verwendung dieser Klasse injiziert Guice sie wie folgt.

Injector injector = Guice.createInjector(() -> {
    bind(Cache.class).toProvider(() -> {
        return new Cache();
    }).asEagerSingleton();

    bind(AuthClient.class).toProvider(() -> {
        return new AuthClient(HttpClientFactory.createClient("https://account.example.com"), new JsonMapperFactory());
    }).asEagerSingleton();

    bind(UserClient.class).toProvider(() -> {
        return new UserClient(HttpClientFactory.createClient("https://example.com"), new JsonMapperFactory());
    }).asEagerSingleton();

    bind(LogoutService.class).to(LogoutServiceImpl.class);
});

//Instanzgenerierung
LoginService service = injector.getInstance(LoginService.class);

Verwendungshinweise zu Google Guice https://qiita.com/opengl-8080/items/6fb69cd2493e149cac3a

Aktueller Test

Lassen Sie uns nun ein Testbeispiel schreiben. Da Mitgliedsvariablen injiziert werden können, können Sie Mock auch im Test problemlos testen, indem Sie Mock injizieren.

class LoginServiceTest {
   @Mock
   Cache cache;
   @Mock 
   AuthClient authClient;
   @Mock 
   UserClient userClient;
   @Mock 
   LogoutService logoutService;

   Injector injector;

   @Before 
   public void setup() {
      injector = Guice.createInjector(() -> {
          bind(Cache.class).toInstance(this.cache);
          bind(AuthClient.class).toInstance(this.authClient);
          bind(UserClient.class).toInstance(this.userClient);
          bind(LogoutService.class).toInstance(this.logoutService);
      });
   }

   @Test 
public void Sie können sich normal anmelden() {
       Account account = new Account("accountId");
       User expectedUser = new User("name001");
       LoginService test = injector.getInstance(LoginService.class);

       when(cache.isLoggedIn()).thenReturn(false);
       when(cache.cacheToken("token01")).thenReturn("token01");
       when(authClient.authorize("token01")).thenReturn(new AuthResult("account01"));
       when(cache.getAccount()).thenReturn(null);
       when(authClient.fetchAccount("account01")).thenReturn(account);
       when(userClient.fetchUser(account)).thenReturn(expectedUser);
       
       User actualUser = test.login("token01");
       Assert.that(actualUser, is(expectedUser));
       //Namens/Bestätigung, nicht angerufen zu werden
       verify(logoutService, never()).logout();
       verify(cache, times(1)).cacheAll("token01", acount, expectedUser);
   }
}

Es ist so. Ich denke, es ist viel einfacher zu testen als der erste. Die Gesamtzahl der Tests ändert sich möglicherweise nicht wesentlich, da mehr Klassen vorhanden sind. ** Die einfache Erstellung von Tests für jede Klasse sollte jedoch viel einfacher sein als zu Beginn **.

Um das Testen zu vereinfachen

Ich habe diesmal weggelassen, aber in Wirklichkeit könnte dieses "executeLogin" noch einfacher gemacht werden. Wenn Sie beispielsweise ein Zugriffstoken erhalten, erhalten Sie am Ende normalerweise Benutzerinformationen usw. Entfernen Sie daher die Serviceklasse, die die folgende Verarbeitung ausführt, übergeben Sie das Zugriffstoken und rufen Sie die Konto- und Benutzerinformationen ab Durch das Erstellen einer Serviceklasse zum Abrufen ** kann sich LoginService ausschließlich auf Wiederholungsversuche und Cache-Verwaltung ** konzentrieren.

AuthResult auth = authClient.aurhotize(token);

//Ich lese den Cache hier, daher muss ich überlegen, wie ich dies im Anmeldedienst belassen und den gesamten Prozess entfernen kann.
Account account = cache.getAccount();
if (account == null) {
    account = authClient.fetchAccount(auth.getAccountId());
}

User user = userClient.fetchUser(account);
if (user == null) {
    return null;
}

Einige Leute sind vielleicht besorgt, dass "** Wenn Sie die Klasse zu stark aufteilen, wissen Sie nicht, was los ist **", aber wenn Sie sich für den Namen und die Paketstruktur interessieren, gibt es kein Problem. Tatsächlich habe ich selbst ein System mit mehr als 1000 Klassen erstellt, aber ich wusste nicht einmal, was ich in welches Paket packen sollte.

Was war übrigens JsonMapperFactory?

Dies wird ebenfalls weggelassen, aber durch das Erstellen dieser JsonMapperFactory ist es diesmal einfacher, "UserClient" und "AuthClient" zu testen. Dies liegt daran, dass ** Client und JsonMapperFactory zu diesem Zeitpunkt im Test verspottet werden können **.

Dieser Client und JsonMapper sind externe Bibliotheken (Einstellungen), sodass Sie sie nicht testen müssen. Wenn Sie es also einfach in Mock konvertieren können, ist es sehr einfach zu testen.

Das Umschließen der Generierung externer Bibliotheken ist daher ein ** zweiter Trumpf ** für das Schreiben von testbarem Code. Sie müssen nicht alles tun, aber wenn es diesmal generisch wie JsonMapper ist, ist es sehr nützlich, eine Factory-Klasse zu erstellen.

Schließlich

Ein weiterer wichtiger Punkt ist die Lesbarkeit des Codes. Wie ich in Schreiben von Code, der schwer zu testen ist geschrieben habe, ist es sehr wichtig, den Prozess richtig zu verstehen, um richtig zu testen. Daher denke ich, dass es besser ist, den Schlussfolgerungsteil beizubehalten, den ich am Anfang geschrieben habe.

Recommended Posts

Testbarer Code
Führen Sie Java-Code skriptweise aus
Java-Code-TIPPS
Java-Beispielcode 02
Java-Beispielcode 03
Codeüberprüfungspunkte
Java-Beispielcode 01
Java-Zeichencode
Code schreiben Ruby