Spring Boot: Referencing @MockBeans by name

Testing Thymeleaf Views with Spring Boots @WebMvcTest and @MockBean
October 13, 2016 by Michael

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
  1. Java Web Weekly, Issue 147 | Baeldung on October 20, 2016 at 1:20 PM

    […] >> Spring Boot: Referencing @MockBeans by name [info.michael-simons.eu] […]

Post a Comment

Your email is never published. We need your name and email address only for verifying a legitimate comment. For more information, a copy of your saved data or a request to delete any data under this address, please send a short notice to michael@simons.ac from the address you used to comment on this entry.
By entering and submitting a comment, wether with or without name or email address, you'll agree that all data you have entered including your IP address will be checked and stored for a limited time by Automattic Inc., 60 29th Street #343, San Francisco, CA 94110-4929, USA. only for the purpose of avoiding spam. You can deny further storage of your data by sending an email to support@wordpress.com, with subject “Deletion of Data stored by Akismet”.
Required fields are marked *