This post has been featured on This Week in Spring – October 18, 2016.
We do a lot of server side rendering at my company and we ran into a strange “problem” today testing our views. Thanks to Pivotals own Andy Wilkinson we could resolve our issue easily.
A technical problem
The testing slices in Spring Boot 1.4+ are really neat. Imagine a simple controller like this:
package ac.simons.thymeleafmockbean; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class WebController { @GetMapping(value = "/greeting") public String greeting() { return "greeting"; } } |
that returns the name of a Thymeleaf view that looks like this:
<!DOCTYPE html> <html> <head> <title>TODO supply a title</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> <div th:text="${@helloService.sayHello()}">placeholder</div> </body> </html> |
Notice the usage of a service called “helloService” inside the div!
The service is super simple, too:
package ac.simons.thymeleafmockbean; import org.springframework.stereotype.Service; @Service public class HelloService { public String sayHello() { return "Hallo, Welt"; } } |
The service is little surprising annotated with @Service
, making it a specialized component who’s bean instance can either referred to by type or by name. The name is – in the case of using the annotation – “helloService”, for reference see AnnotationBeanNameGenerator. I really didn’t spent much time thinking about naming any more the last years, it just used to work and I can use the service in my view .
Using Spring Boots test slices @WebMvcTest
and @MockBean
it can be tested like this:
package ac.simons.thymeleafmockbean; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.hasXPath; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; @RunWith(SpringRunner.class) @WebMvcTest public class WebControllerTest { @Autowired private MockMvc mvc; @MockBean(name = "helloService") private HelloService helloService; @Before public void prepareMock() { when(helloService.sayHello()).thenReturn("Hello from Mock!"); } @Test public void greetingShouldWork() throws Exception { this.mvc .perform(get("/greeting")) .andExpect(status().isOk()) .andExpect(view().name("greeting")) .andExpect(content().node(hasXPath("/html/body/div", equalTo("Hello from Mock!")))); } } |
Important thing is the usage of @MockBean(name = "helloService")
with a name! @MockBean mocks beans, here the “HelloService”. There is no instance of that service as the service is not part of the web slice. The annotation name generator doesn’t kick in and the mocking bean mechanism generates its own name which is “ac.simons.thymeleafmockbean.HelloService#0”. If you plan to use mocked beans by name and you don’t replace existing beans make sure you specify the name of the bean as you are expecting it in your application code!
In our use case, we ended up with a stack trace that read like this:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "@helloService.sayHello()" (greeting:9) at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:677) |
We actually use a service within Thymeleafs Spring Security integration inside a sec:auth attribute. If you google that stack trace you’ll end up with some solutions leading you into the wrong direction, mainly settings regarding your security configuration. For me that was a really nice example on teaching how important it is to strip away pretty much everything to come to the core of the problem which in this case was, that one bean was there but reachable by an expected name.
An architectural problem
Update: The reader may have noticed that post covers two problems. A technical problem, which is the fact that mocked beans often have a different name opposed to normal instantiated beans. This problem can be solved as the post describes, by naming them correctly.
The other problem has been mentioned by Oliver Gierke and is of architectural kind and specific to our problem we had this morning. We obviously reaching back from the view layer into a service layer. We have reasons for doing so in our use case, but as often, technical debt comes back looking for you and if it is only for finding problems like these. Oliver was so kind suggestion a solution that doesn’t involve interceptors for controllers or adding the information we need to every model in every request mapped. He suggested a view model, wrapping the service call. I adapted his gist to the example above which then looks like this:
package ac.simons.thymeleafmockbean; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; @Controller public class WebController { public static class ViewModel { private final HelloService service; public ViewModel(HelloService service) { this.service = service; } public String getGreeting() { return service.sayHello(); } } private final HelloService helloService; public WebController(HelloService helloService) { this.helloService = helloService; } // Available in all views rendered by that controller @ModelAttribute public ViewModel viewModel() { return new ViewModel(this.helloService); } @GetMapping(value = "/greeting") public String usingHelloService() { return "greeting"; } } |
You see that now the controller depends on the service, as it should. It uses the @ModelAttribute
annotation to provide an instance of the custom view model class, which wraps the service call. By declaring it as a model attribute, it’s available to every view rendered by every method of the controller declaring this.
Nice added bonus: No obvious method call in the view anymore, spot the difference:
<!DOCTYPE html> <html> <head> <title>TODO supply a title</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> <div th:text="${viewModel.greeting}">placeholder</div> </body> </html> |
And regarding the test: I can now safely remove the name of the mocked been again, as the dependency in the controller is resolved by type.
No comments yet
One Trackback/Pingback
[…] >> Spring Boot: Referencing @MockBeans by name [info.michael-simons.eu] […]
Post a Comment