📜 ⬆️ ⬇️

ContactManager, part 3. Testing controllers using MockMvc

Hello.

Acquainted with the library MockMvc, I found the " presence of the absence " of its references to Habré. I will try to fill this gap, all the more so that our ContactManager application just needs automated testing.

So, the main topic of the lesson is to add tests for controllers to the application. As a bonus, we will do it in a fashionable, “non-xml-based” technology.

Updating the project structure

First update the library versions. SpringFramework has now been updated to version 3.2.1, and includes the coveted MockMvc, so this update is necessary. Spring Security is a little behind, but it is (almost) not a problem. The version of Hibernate also grew to 4.1.9.Final. You will find the full project file in the repository (link at the end of the article).
')
Transition to version 4 of Hibernate requires a slight refinement of the data.xml file. You need to change 3 to 4 in the package name org.springframework.orm.hibernate4 , remove the configLocation and configurationClass parameters in the configLocation and instead add the packagesToScan parameter where to transfer the list of class packages with Hibernate mapping from hibernate.cfg.xml . This file itself can be deleted, we no longer need it. As a result, the sessionfactory bin takes the form:
  <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="packagesToScan"> <list> <value>net.schastny.contactmanager.domain</value> <value>com.acme.contactmanager.domain</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">create-drop</prop> <prop key="hibernate.dialect">${jdbc.dialect}</prop> <prop key="hibernate.connection.charSet">UTF-8</prop> </props> </property> </bean> 

We will also need test resources, so we create the src/test/resources directory, copy security.xml security.xml settings into it, and create testdb.properties property file for the database. You can do without it, but again, for educational purposes, we will see how you can set properties in bins from the outside. File contents
 db.user.name=sa db.user.pass= 

Not God knows what, but as an example will do. Make a copy of log4j.xml and with resources on it all. Go to the source code.

Create a directory src/test/groovy , package com.acme.contactmanager.test
In order for the maven to find our grooves when building from the command line, we’ll add build-helper-maven-plugin to pom.xml

Spring configuration for tests

Create a TestConfig.groovy spring configuration TestConfig.groovy
 @Configuration @ComponentScan(['net.schastny.contactmanager.dao', 'com.acme.contactmanager.dao', 'net.schastny.contactmanager.web', 'net.schastny.contactmanager.service']) @PropertySource('classpath:testdb.properties') @ImportResource('classpath:security.xml') @EnableTransactionManagement class TestConfig { // ... } 

The titles of the annotations speak for themselves:

By the way, about the transaction. We have interfaces and implementations for the DAO and service classes, but in general we can confine ourselves to the implementation class alone. In this case, the annotation will need to specify that the proxyTarget is created automatically, otherwise there will be problems: @EnableTransactionManagement(proxyTargetClass = true)

Further. Bind properties from testdb.properties to class attributes using @Value
 class TestConfig { @Value('${db.user.name}') String userName @Value('${db.user.pass}') String userPass // ... } 

Add the LocalSessionFactoryBean , where we will use the properties obtained. Here we see the packagesToScan already familiar to us.
  @Bean public LocalSessionFactoryBean sessionFactory() { LocalSessionFactoryBean bean = new LocalSessionFactoryBean() bean.packagesToScan = [ 'com.acme.contactmanager.domain', 'net.schastny.contactmanager.domain'] as String[] Properties props = new Properties() props."hibernate.connection.driver_class" = "org.h2.Driver" props."hibernate.connection.url" = "jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE;DB_CLOSE_ON_EXIT=FALSE" props."hibernate.connection.username" = userName props."hibernate.connection.password" = userPass props."hibernate.dialect" = "org.hibernate.dialect.H2Dialect" props."hibernate.hbm2ddl.auto" = "create-drop" props."hibernate.temp.use_jdbc_metadata_defaults" = "false" bean.hibernateProperties = props bean } 

This hibernate.temp.use_jdbc_metadata_defaults = false thing helps when it slows down the context to retrieve metadata from the database

Finally, the “cherry on the cake” of our configuration will be HibernateTransactionManager
  @Bean public HibernateTransactionManager transactionManager() { HibernateTransactionManager txManager = new HibernateTransactionManager() txManager.autodetectDataSource = false txManager.sessionFactory = sessionFactory().object txManager } 

I repeat - the configuration of the project itself remains old, based on xml. This configuration applies only to tests.

We start testing

But it was all a saying, it was time for a fairy tale. It is logical to expect that working with MockMvc consists of 3 steps: building a mock object, sending an HTTP request to the controller and actually analyzing the results. For the first step - building a mock object - we will use a builder based on WebApplicationContext.

Create a class with the mnemonic name MockMvcTest.groovy
 @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = [ TestConfig.class ] ) class MockMvcTest { @Autowired WebApplicationContext wac MockMvc mockMvc @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).dispatchOptions(true).build() } } 

Everything is simple to disgrace. You can already test something. Let's take a look at our controller and see that the home() method is a good candidate for the test case, where there is a simple redirect
  @RequestMapping("/") public String home() { return "redirect:/index"; } 

Actually, that's what we write
  @Test public void home() { MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/") ResultActions result = mockMvc.perform(request) result.andExpect(MockMvcResultMatchers.redirectedUrl("/index")) } 

Explanations almost not required:

The andExpect() function provides great opportunities for checking the result obtained, here are examples from Javadoc, which give a general idea of ​​its work:
 mockMvc.perform(get("/person/1")) .andExpect(status.isOk()) .andExpect(content().mimeType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.person.name").equalTo("Jason")); mockMvc.perform(post("/form")) .andExpect(status.isOk()) .andExpect(redirectedUrl("/person/1")) .andExpect(model().size(1)) .andExpect(model().attributeExists("person")) .andExpect(flash().attributeCount(1)) .andExpect(flash().attribute("message", "success!")); 

Run, works. Hurray, the first test is ready. What is next? List of contacts.
  @RequestMapping("/index") public String listContacts(Map<String, Object> map) { map.put("contact", new Contact()); map.put("contactList", contactService.listContact()); map.put("contactTypeList", contactService.listContactType()); return "contact"; } 

All the same GET request, we receive map in parameters, we fill it and we return name view. We are already able to send GET requests, it remains only to add a check of the result. We write the second test.
  @Test public void index() { ResultActions result = mockMvc.perform(MockMvcRequestBuilders.get("/index")) result.andExpect(MockMvcResultMatchers.view().name("contact")) .andExpect(MockMvcResultMatchers.model().attributeExists("contact")) .andExpect(MockMvcResultMatchers.model().attributeExists("contactList")) .andExpect(MockMvcResultMatchers.model().attributeExists("contactTypeList")) } 

Again, not a single extra line. Having executed the request, we checked the name of the view, which it returned to us, and checked that all the attributes of the model are in place. For a more detailed study of these attributes, you can get a link to the actual MvcResult object using the andReturn () function
 MvcResult mvcResult = result.andReturn() assert mvcResult.modelAndView.model.contactTypeList.size() == 3 

So far so good, but there is still a lot of work ahead. It's time to add something to our list. The controller method looks like this:
  @RequestMapping(value = "/add", method = RequestMethod.POST) public String addContact(@ModelAttribute("contact") Contact contact, BindingResult result) { contactService.addContact(contact); return "redirect:/index"; } 

Finally, the POST method and the awesome @ModelAttribute Contact contact parameter. But it is not all that bad. A quick googling request for “mockmvc Model Attribute” here gives the result . Such mapping can simply be replaced by a set of query parameters. The function of adding parameters to the query is quite expected to look like this: param(Stirng name, String... values) . We write
  @Autowired ContactService contactService @Test public void add() { //      ,    def contacts = contactService.listContact() assert !contacts //         def contactTypes = contactService.listContactType() assert contactTypes //  POST-,      mockMvc.perform(MockMvcRequestBuilders.post("/add") .param("firstname",'firstname') .param("lastname",'lastname') .param("email",'firstname.lastname@gmail.com') .param("telephone",'555-1234') .param("contacttype.id", contactTypes[0].id.toString()) .andExpect(MockMvcResultMatchers.redirectedUrl("/index")) //    contacts = contactService.listContact() //        id assert contacts assert contacts[0].id //   ,      contactService.removeContact(contacts[0].id) } 

Well, the last method remains - delete ().
  @RequestMapping("/delete/{contactId}") public String deleteContact(@PathVariable("contactId") Integer contactId) { contactService.removeContact(contactId); return "redirect:/index"; } 

Passing @PathVariable also not a problem, just add it to the URL.
  @Test public void delete() { //     def contactTypes = contactService.listContactType() assert contactTypes Contact contact = new Contact( firstname : 'firstname', lastname : 'lastname', email : 'firstname.lastname@gmail.com', telephone : '555-1234', contacttype : contactTypes[0] ) contactService.addContact(contact) assert contact.id def contacts = contactService.listContact() //   contacts.id    id  assert contact.id in contacts.id //  POST- ,   URL id   // ${contact.id} -    placeholder,  GString! mockMvc.perform(MockMvcRequestBuilders.get("/delete/${contact.id}") .andExpect(MockMvcResultMatchers.redirectedUrl("/index")) // ,    def contacts = contactService.listContact() assert !(contact.id in contacts.id) } 

That's all, all methods are covered with automatic tests, hooray! Or not cheers? An attentive reader will ask - what about security ?! Why did we connect security.xml if there is no mention of users and roles in the tests? And he will be right. In the third part of the lesson, we will add support for working with SpringSecurity.

Add authentication

It is logical to assume that MockMvc should have support for working with filters. In fact, in the builder there is a method addFilter() , in which we can pass an instance of springSecurityFilterChain . Let's change our test as follows:
  @Autowired FilterChainProxy springSecurityFilterChain @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac) .addFilter(springSecurityFilterChain) //    .dispatchOptions(true).build() } 

Now we have a rights check when accessing URLs, but we need to somehow introduce ourselves to the system. Let's try to make “feint ears” and directly set the value in SecirityContextHolder.
  List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); Authentication auth = new UsernamePasswordAuthenticationToken("user1", "1111", authorities); SecurityContextHolder.getContext().setAuthentication(auth); 

It looks plausible, let's try the add () method, which requires the ROLE_USER privilege. Babah! Did not work out
 DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /add; Attributes: [ROLE_USER] DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication. AnonymousAuthenticationToken@d4551ca6: Principal: guest; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication. WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS DEBUG: org.springframework.security.web.access.ExceptionTranslationFilter - Access is denied (user is anonymous); redirecting to authentication entry point 

Granted Authorities: ROLE_ANONYMOUS hints that our feint didn’t work with our ears. But I hasten to reassure - our guilt is not here, supporting Security in the tests has not yet been implemented . That is why in the beginning I wrote that integration with SpringSecurity is “almost no problem”. The problem is still there, but it is solved.

The SecurityRequestPostProcessors.java class will help us with this. I will not dwell on its content, just copy it to the src/test/java folder and show how it can be used for our needs.

We remove the call setAuthentication(auth) , which turned out to be useless, and in the add () method we add one line to the query construct:
  //... mockMvc.perform(MockMvcRequestBuilders.post("/add") .param("firstname",'firstname') .param("lastname",'lastname') .param("email",'firstname.lastname@gmail.com') .param("telephone",'555-1234') .param("contacttype.id", contactTypes[0].id.toString()) .with(SecurityRequestPostProcessors.userDetailsService("user1"))) //   Security .andExpect(MockMvcResultMatchers.redirectedUrl("/index")) // ... 

That is, in fact, we execute the request on behalf of user1, with all his rights. And it works great! In the log we see the desired Granted Authorities: ROLE_USER

But do not rush to remove the old test cases, they will still be useful to us. Indeed, in this form, they are testing nothing more than unauthorized access to our system. And the same methods home () and index () should work, because these URLs do not impose any restrictions on authentication. And they work!

Let's go back to the add () method. What does our application do when we try to keep a contact being unauthorized? Shows us a login page! In terms of redirect, this means redirect to /login.jsp
Therefore, we replace the verification of the result of an unauthorized request to save a contact with another one:
 result.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login.jsp")) 

In the same way, in the absence of authorization, the delete () method must also implement. And on behalf of the user “admin” the deletion works and it is correct.

And it remains to test another option - when a user with ROLE_USER rights tries to delete an entry. In this case, he should see error 403, or rather, the forward on /error403.jsp. The body of the test method for this scenario will look like this (the contact id doesn't matter in this case, just put / 1):
  mockMvc.perform(MockMvcRequestBuilders.get("/delete/1") .with(SecurityRequestPostProcessors.userDetailsService("user1"))) .andExpect(MockMvcResultMatchers.forwardedUrl("/error403.jsp")) 


That's all. As a result, we got 12 test methods, 3 for each of the 4 URLs. They check unauthorized access, access with ROLE_USER and ROLE_ADMIN rights. Along with the controllers, we tested the methods of services and DAO.

Project source code on github

Source: https://habr.com/ru/post/171911/


All Articles