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
Lassen Sie uns wie folgt codieren.
Dies erleichtert das Testen erheblich. Lassen Sie uns nun den vorherigen Code umgestalten.
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.
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;
}
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<>();
}
}
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));
}
}
Ä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);
}
// ︙
}
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.
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
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 **.
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.
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.
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.