Intro
Lorsque l’on met en place une application Spring Boot, de nombreuses briques entrent en jeu. Par exemple : la gestion des Contrôleurs REST avec la brique SPRING-WEB; la gestion de la persistance avec la brique SPRING-DATA, et la base de données sous-jacente ( MongoDB ici); n’oublions pas l’aspect authentification et autorisation via la brique SPRING-SECURITY. Et on peut imaginer d’autres vu la taille de l’écosystème SPRING!
Comment faire en sorte de tester les différentes briques Spring efficacement, en environnement mocké (test unitaire) ou non (test d’intégration).
Nous allons donc voir la technique de test slicing au travers de 2 scénarios, afin d’isoler la brique à tester. Puis nous verrons un scénario de test d’intégration où toutes les briques seront actives!
Test unitaire brique WEB : Test de la brique WEB avec @WebMvcTest. Mocking de la brique Sécurité et DATA.
Test unitaire brique DATA : Test de la brique DATA avec @DataMongoTest. Mocking de la brique WEB et Sécurité. Base locale Mongo en mémoire. Des connaissances de SPRING DATA sont bienvenues, je vous laisse regarder la doc.
Test d’intégration final : tout est démocké et lancement réel du serveur via @SpringBootTest. Base locale Mongo en mémoire.
Présentation de notre cas d’exemple
Voici le schéma de l’application que nous allons mettre en place :
- Un utilisateur authentifié et autorisé appelle un contrôleur REST (/users/123) afin d’obtenir l’adresse d’un autre utilisateur, dont l’identifiant est passé en paramètre. Ce endpoint REST est sécurisé avec SPRING SECURITY : on doit fournir un login + mot de passe en Basic Authentication (détail ci-après).
- On va utiliser Mongo pour la persistance. Nous souhaitons que l’utilisateur qui effectue la requête en Basic Authentication soit inscrit dans la base Mongo (table UTILISATEUR). Pour cela, nous mettons en place une implémentation de UserDetailsService de Spring Security;
- On va désactiver la gestion de session et la mise en place du JSESSIONID pour plus de simplicité via .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
Voici le code de la configuration Spring Security associé au vue des règles ci-dessus :
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.exceptionHandling()
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
Nous avons donc 2 collections dans Mongo :
-
UTILISATEUR (userId, email, password) : pour la gestion de l’authentification
-
CONTACT (userId, address) : stockage des adresses de chaque utilisateur, appelé par contrôleur REST
Passons maintenant dans le vif du sujet,avec l’implémentation de nos trois scénarios de tests.
Scénario 1 : Test unitaire brique WEB
Dans ce scénario, nous voulons simplement tester notre contrôleur REST, qui fait un simple appel à la base de données Mongo. Voici notre contrôleur REST :
@RestController
public class MyController {
@Autowired
private ContactService contactService;
@RequestMapping(value = "/users/{userId}", method = RequestMethod.GET)
public String test(@PathVariable("userId") String userId ) throws Exception {
return contactService.getAdress(userId);
}
Voici comment on teste cela :
@RunWith(SpringRunner.class)
@WebMvcTest(MyController.class)
public class Controller_Only_Test {
@Autowired
private MockMvc mockMvc;
@Autowired
private ApplicationContext appContext;
// need to mock this beans, will not be used anyway (we use @WithMockUser below)
@MockBean
CustomUserDetailsService service;
@MockBean
ContactService contactService;
@Test
@WithMockUser // use with mockMvc only
public void testController() throws Exception {
// given
String userId = "123";
when(contactService.getAdress(anyString())).thenReturn("myaddress");
// when + then
mockMvc.perform(get("/users/" + userId)).andExpect(content().string(containsString("myaddress")));
}
Explications :
- On utilise @WebMvcTest(MyController.class) afin d’indiquer à Spring de charger uniquement le contexte WEB;
- Nous utilisons MockMvc pour effectuer l’appel REST;
- Sachant que les services SPRING (@Service) ne sont pas chargés, il faut les mocker via l’annotation @MockBean
- MockMvc va initialiser un contexte de sécurité par défaut du fait que nous ayons déclaré la dépendance SPRING SECURITY. Nous devons donc mocker un utilisateur via @WithMockUser
Scénario 2 : Test unitaire brique DATA
Dans ce scénario, nous voulons simplement tester notre repository MONGO. Pour cela, nous devons mettre en place une base locale en mémoire grâce à cette librairie. Voici notre repository à tester :
public interface MongoContactRespository extends MongoRepository<Contact, String> {
Optional<Contact> findByUserId(String userId);
}
Voici notre test unitaire :
@RunWith(SpringRunner.class)
@DataMongoTest
public class DB_Only_Test {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
MongoContactRespository mongoContactRespository;
@Test
public void testWithFrenchIsbn() {
// given
Contact contact = new Contact();
contact.setUserId("123");
contact.setAddress("address1");
mongoTemplate.save(contact);
// when + then
Optional<Contact> address = mongoContactRespository.findByUserId("123");
assertEquals(address.get().getAddress(), "address1");
}
}
Explications:
- On utilise @DataMongoTest pour notre test; cette annotation va uniquement charger le contexte SPRING DATA.
- On injecte un MongoTemplate pour initialiser notre jeu de données;
- On injecte notre repository à tester.
Scénario 3 : Test d’intégration
Pour ce dernier scénario plus complexe, nous allons mettre en place un vrai test d’intégration qui a toutes les briques de SPRING actives : SECURITY, DATA, WEB. Ceci est rendu possible grâce à l’annotation _@SpringBootTest q_ui va charger TOUT le contexte SPRING, comme en conditions réelles!
Voici notre d’intégration :
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class Full_With_Server_Test {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate testRestTemplate;
@Autowired
private ApplicationContext appContext;
@Autowired
private WebApplicationContext webAppContext;
@Autowired
private MongoTemplate mongoTemplate;
@Before
public void setup() {
// save user who makes authentication
Utilisateur user = new Utilisateur();
user.setEmail("toto");
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String p = bCryptPasswordEncoder.encode("tutu");
user.setPassword(p);
mongoTemplate.save(user);
// save contact
Contact contact = new Contact();
contact.setAddress("adress2");
contact.setUserId("456");
mongoTemplate.save(contact);
}
@Test
public void getUserAdress() {
String adress = testRestTemplate.withBasicAuth("toto", "tutu").getForObject("http://localhost:" + port + "/users/456", String.class);
assertEquals(adress, "adress2");
}
}
Explications:
- Afin de démarrer un vrai serveur en conditions réelles il faut déclarer :
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
qui va lancer le serveur sur un port aléatoire; - Très important : Vu que
@SpringBootTest
charge le contexte SPRING par défaut, il ne faut surtout pas charger la vraie base Mongo mais la base en mémoire. Pour cela il y a plusieurs options, mais dans notre cas, j’ai choisi les profils SPRING. J’ai déclaré un profil test afin de ne pas charger la configuration réelle de MONGO, mais utiliser la base en mémoire. Voici comment :@ActiveProfiles(test)
sur la classe de test, et@Profile(test)
sur la classe de configuration Mongo; - Initialiser nos jeu de données via l’annotation
@Before
Junit et unmongoTemplate
; - Appel de notre service REST via un appel réel réseau et le TestRestTemplate. Ce dernier est idéal car il offre une méthode
withBasicAuth
pour passer l’utilisateur à authentifier via SPRING SECURITY.
Conclusion
Voici un POST riche où nous avons passés en revus de nombreux concepts fondamentaux du testing avec SPRING. Toute remarque ou commentaire est bienvenue! Comme d’habitude l’ensemble du code source est accessible sur mon github. Cheers!