Cet article concerne la refactorisation des sources publiées ci-dessous.
Écrire du code difficile à tester https://qiita.com/oda-kazuki/items/bac33094e82b0f51da41
Codons comme suit.
Faire cela rend les tests beaucoup plus faciles. Refactorisons maintenant le code précédent.
Dernière fois Comme je l'ai écrit, Login Service a joué plusieurs rôles. Tout d'abord, nous arrêterons le plus dangereux d'entre eux, "l'appel direct à l'API Web". Supprimons-le.
Mais avant cela, créez une classe d'informations de compte et d'informations utilisateur exprimées dans Map. En fait, j'aimerais ajouter divers traitements à l'intérieur, mais cette fois, il semble ne pas être utilisé en particulier, donc cela ressemble à un DTO.
@Data
public class AuthToken {
private String token;
}
@Data
public class AuthResult {
private String accountId;
}
@Data
public class Account {
//Je ne vais pas l'utiliser cette fois, mais je vais le mettre dans le sens qu'il y a quelque chose comme ça
private String accountId;
}
@Data
public class User {
//Je ne vais pas l'utiliser cette fois, mais je vais le mettre dans le sens qu'il y a quelque chose comme ça
private String name;
}
De plus, cette fois, j'utilise une bibliothèque appelée JsonMapper
(paramètre), mais c'est un peu difficile à utiliser car elle doit spécifier les génériques. Nous allons donc également créer une méthode d'usine pour cela. C'est une classe simple qui crée simplement le JsonMapper cible.
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<>();
}
}
Alors, créons d'abord quelque chose appelé HttpClient
. Limitez-y la partie qui appelle directement l'API Web.
public abstract class HttpClient {
protected final Client client;
protected final JsonMapperFactory mapperFactory;
public HttpClient(Client client, JsonMapperFactory factory) {
this.client = client;
this.mapperFactory = factory;
}
}
Maintenant, créons ʻAuthClient et ʻUserClient
qui appellent réellement l'API Web.
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));
}
}
Puis modifiez également le cache. Finalement, il sera utilisé comme un singleton, mais une fois que la description dans le modèle Singleton sera perdue.
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;
}
}
En regardant ça comme ça, je pense que c'est beaucoup de gaspillage, mais c'est une refactorisation, alors laissons les choses telles quelles.
Dependency Injection Vous êtes maintenant prêt pour la refactorisation. Mais avant cela, je présenterai brièvement DI (Dependency Injection), qui est la clé pour écrire du code testable.
Si vous voulez en savoir plus, je pense que vous devriez voir ce qui suit.
Même les singes peuvent comprendre! Injection de dépendance: injection de dépendance https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f
Cela s'appelle «injection de dépendances» en japonais. Comme son nom l'indique, il s'agit d'un modèle de conception qui place des fichiers dépendants de l'extérieur.
La DI la plus simple consiste à passer une variable membre dans le constructeur. Par exemple, les méthodes «AuthClient» et «UserClient» ci-dessus sont utilisées.
Republier
public class UserClient extends HttpClient {
public UserClient(Client client, JsonMapperFactory factory) {
super(client, factory);
}
// ︙
}
Lors du test, vous pouvez facilement Mock les fichiers dépendants. Fondamentalement, ** les classes dépendent des variables membres **. Si ce sont des données, elles peuvent être traitées comme de simples "informations", mais si cela dépend de la méthode, il peut être nécessaire de les faire simuler au moment du test.
Le modèle d'injection de dépendance facilite le remplacement de ces pièces dépendantes par moquage lors des tests.
Google Guice Pour Java, il existe de nombreuses bibliothèques DI puissantes telles que Google Guice et Spring. Utilisons Google Guice cette fois.
Maintenant, corrigeons le service de connexion que nous voulions tester cette fois. Avant cela, supposons qu'il existe un nouveau service appelé «LogoutService». (Comme c'est déjà gênant, je n'écrirai que l'interface)
public interface LogoutService {
/**Si la connexion réussit, supprimez tout le cache et renvoyez true. Renvoie false si la déconnexion échoue*/
boolean logout();
}
C'est donc le service de connexion.
public class LoginService {
@Inject //Injection de dépendance à 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;
}
}
}
C'est beaucoup plus simple. Lors de l'utilisation de cette classe, Guice l'injecte comme suit.
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);
});
//Génération d'instance
LoginService service = injector.getInstance(LoginService.class);
Notes d'utilisation de Google Guice https://qiita.com/opengl-8080/items/6fb69cd2493e149cac3a
Maintenant, écrivons un exemple de test. Étant donné que les variables membres peuvent être injectées, vous pouvez facilement tester en injectant Mock même dans le test.
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 Vous pouvez vous connecter normalement() {
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));
//Appelé/Confirmation de ne pas être appelé
verify(logoutService, never()).logout();
verify(cache, times(1)).cacheAll("token01", acount, expectedUser);
}
}
C'est comme ça. Je pense que c'est beaucoup plus facile à tester que le premier. Le nombre total de tests peut ne pas changer beaucoup car il y a plus de classes, mais ** la facilité de création de tests pour chaque classe devrait être beaucoup plus facile qu'elle ne l'était au début **.
J'ai omis cette fois, mais en réalité, cet ʻexecuteLogin` pourrait être rendu encore plus facile. Par exemple, lorsque vous obtenez un jeton d'accès, vous obtiendrez généralement des informations utilisateur, etc. à la fin, supprimez donc la classe de service qui effectue le traitement suivant, transmettez le jeton d'accès et obtenez les informations sur le compte et l'utilisateur. La création d'une classe de service à obtenir ** permettra à LoginService de se concentrer uniquement sur les tentatives et la gestion du cache **.
AuthResult auth = authClient.aurhotize(token);
//Je lis le cache ici, je dois donc réfléchir à la manière de le laisser dans le service de connexion et de supprimer tout ce processus.
Account account = cache.getAccount();
if (account == null) {
account = authClient.fetchAccount(auth.getAccountId());
}
User user = userClient.fetchUser(account);
if (user == null) {
return null;
}
Certaines personnes peuvent s'inquiéter du fait que "** Si vous divisez trop la classe, vous ne saurez pas ce qui se passe **", mais si vous vous souciez du nom et de la structure du package, il n'y a pas de problème. En fait, j'ai moi-même créé un système avec plus de 1000 classes, mais je ne savais même pas quoi mettre dans quel paquet.
Ceci est également omis, mais en créant cette JsonMapperFactory, il sera plus facile de tester ʻUserClient et ʻAuthClient
cette fois. Cela est dû au fait que ** Client et JsonMapperFactory peuvent être simulés ** dans le test à ce moment-là.
Ce client et JsonMapper sont des bibliothèques externes (avec un paramètre appelé), vous n'avez donc pas besoin de les tester. Donc, si vous pouvez facilement le convertir en Mock, ce sera très facile à tester.
Ainsi, encapsuler la génération de bibliothèques externes est un ** second atout ** pour écrire du code testable. Vous n'êtes pas obligé de tout faire, mais si c'est générique comme JsonMapper cette fois, il est très utile de créer une classe d'usine.
Une autre chose importante est la lisibilité du code. Comme je l'ai écrit dans Écrire du code difficile à tester, il est très important de comprendre correctement le processus afin de tester correctement. Par conséquent, je pense qu'il vaut mieux garder la partie de conclusion que j'ai écrite au début.