New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolving of mocked beans not working in Thymeleaf views #7160

Closed
michael-simons opened this Issue Oct 13, 2016 · 6 comments

Comments

Projects
None yet
3 participants
@michael-simons
Contributor

michael-simons commented Oct 13, 2016

Hey everybody,

the following problem arises with

Spring Boot 1.4.1
and the dependencies brought in via the fitting spring-boot-starter-thymeleaf
on Java 8u102

We want to use beans inside a Thymeleaf template using ${@bean.foobar()}, more specifically inside a sec:authorize attribute, which works basically fine. The problem comes into play with mocked beans during tests.

Running those templates in a test configured with @WebMvcTest and the service mocked away with @MockBean doesn't work.

I have prepared the following example, see thymeleafmockbean.zip:

Having this template

<!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>

for this controller

@Controller
public class WebController {
    @GetMapping(value = "/usingHelloService")
    public String usingHelloService() {
        return "usingHelloService";
    }
}

and testing it like this

@RunWith(SpringRunner.class)
@WebMvcTest
@AutoConfigureMockMvc(print = MockMvcPrint.NONE)
public class WebControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private HelloService helloService;

    @Before
    public void prepareMock() {
        when(helloService.sayHello()).thenReturn("Hallo, Welt");
    }

    @Test
    public void thisCausesAnError() throws Exception {
        this.mvc
                .perform(get("/usingHelloService"))
                .andExpect(status().isOk())
                .andExpect(view().name("usingHelloService"))
                .andExpect(content().node(hasXPath("/html/body/div", equalTo("Hallo, Welt"))))
                .andDo(MockMvcResultHandlers.print());
    }
}

results in the following error:

2016-10-13 10:41:49.651 ERROR 2738 --- [           main] org.thymeleaf.TemplateEngine             : [THYMELEAF][main] Exception processing template "usingHelloService": Exception evaluating SpringEL expression: "@helloService.sayHello()" (usingHelloService:9)
Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 5.632 sec <<< FAILURE! - in ac.simons.thymeleafmockbean.WebControllerTest
thisCausesAnError(ac.simons.thymeleafmockbean.WebControllerTest)  Time elapsed: 0.275 sec  <<< ERROR!
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "@helloService.sayHello()" (usingHelloService:9)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:677)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1180)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:284)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1076)
    at org.springframework.context.expression.BeanFactoryResolver.resolve(BeanFactoryResolver.java:45)
    at org.springframework.expression.spel.ast.BeanReference.getValueInternal(BeanReference.java:55)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:51)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:87)
    at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:120)
    at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:267)
    at org.thymeleaf.spring4.expression.SpelVariableExpressionEvaluator.evaluate(SpelVariableExpressionEvaluator.java:139)
    at org.thymeleaf.standard.expression.VariableExpression.executeVariable(VariableExpression.java:154)
    at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:59)
    at org.thymeleaf.standard.expression.Expression.execute(Expression.java:103)
    at org.thymeleaf.standard.expression.Expression.execute(Expression.java:133)
    at org.thymeleaf.standard.expression.Expression.execute(Expression.java:120)
    at org.thymeleaf.standard.processor.attr.AbstractStandardTextChildModifierAttrProcessor.getText(AbstractStandardTextChildModifierAttrProcessor.java:68)
    at org.thymeleaf.processor.attr.AbstractTextChildModifierAttrProcessor.getModifiedChildren(AbstractTextChildModifierAttrProcessor.java:59)
    at org.thymeleaf.processor.attr.AbstractChildrenModifierAttrProcessor.processAttribute(AbstractChildrenModifierAttrProcessor.java:59)
    at org.thymeleaf.processor.attr.AbstractAttrProcessor.doProcess(AbstractAttrProcessor.java:87)
    at org.thymeleaf.processor.AbstractProcessor.process(AbstractProcessor.java:212)
    at org.thymeleaf.dom.Node.applyNextProcessor(Node.java:1017)
    at org.thymeleaf.dom.Node.processNode(Node.java:972)
    at org.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:695)
    at org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:668)
    at org.thymeleaf.dom.Node.processNode(Node.java:990)
    at org.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:695)
    at org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:668)
    at org.thymeleaf.dom.Node.processNode(Node.java:990)
    at org.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:695)
    at org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:668)
    at org.thymeleaf.dom.Node.processNode(Node.java:990)
    at org.thymeleaf.dom.Document.process(Document.java:93)
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1155)
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1060)
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1011)
    at org.thymeleaf.spring4.view.ThymeleafView.renderFragment(ThymeleafView.java:335)
    at org.thymeleaf.spring4.view.ThymeleafView.render(ThymeleafView.java:190)
    at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1257)
    at org.springframework.test.web.servlet.TestDispatcherServlet.render(TestDispatcherServlet.java:105)
    at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1037)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:980)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:65)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:89)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:155)
    at ac.simons.thymeleafmockbean.WebControllerTest.thisCausesAnError(WebControllerTest.java:51)

My expectation is, that the mocked bean is accessible from the Thymeleaf template like any other bean.

If I use "the real thing" in the test set up like this:

@RunWith(SpringRunner.class)
@WebMvcTest(includeFilters = @Filter(classes = HelloService.class, type = FilterType.ASSIGNABLE_TYPE))
@AutoConfigureMockMvc(print = MockMvcPrint.NONE)
public class WebControllerTestNotUsingMockBean {

    @Autowired
    private MockMvc mvc;

    @Test
    public void usingTheRealServiceItWorks() throws Exception {
        this.mvc
                .perform(get("/usingHelloService"))
                .andExpect(status().isOk())
                .andExpect(view().name("usingHelloService"))
                .andExpect(content().node(hasXPath("/html/body/div", equalTo("Hallo, Welt"))))
                .andDo(MockMvcResultHandlers.print());
    }
}

the view works as expected.

I have attached a demo project that should demonstrate the error with the above code.
I'm not entirely sure if this is a problem on your site or on Thymeleaf. Maybe @danielfernandez has an idea as well.

Thanks for looking at this.

@wilkinsona

This comment has been minimized.

Show comment
Hide comment
@wilkinsona

wilkinsona Oct 13, 2016

Member

Thanks for the very thorough bug report, @michael-simons.

It looks like the use of @MockBean has changed the name of the bean. Looking in the bean factory I can see a bean named ac.simons.thymeleafmockbean.HelloService#0. That's at odds with the @MockBean javadoc for the name attribute:

The name of the bean to register or replace. If not specified the name will either be generated or, if the mock replaces an existing bean, the existing name will be used.

A workaround is to explicitly set the name in WebControllerTest:

@MockBean(name="helloService")
private HelloService helloService;
Member

wilkinsona commented Oct 13, 2016

Thanks for the very thorough bug report, @michael-simons.

It looks like the use of @MockBean has changed the name of the bean. Looking in the bean factory I can see a bean named ac.simons.thymeleafmockbean.HelloService#0. That's at odds with the @MockBean javadoc for the name attribute:

The name of the bean to register or replace. If not specified the name will either be generated or, if the mock replaces an existing bean, the existing name will be used.

A workaround is to explicitly set the name in WebControllerTest:

@MockBean(name="helloService")
private HelloService helloService;
@wilkinsona

This comment has been minimized.

Show comment
Hide comment
@wilkinsona

wilkinsona Oct 13, 2016

Member

It looks like the use of @MockBean has changed the name of the bean.

I was wrong. WebControllerTest doesn't contain a HelloService bean (unlike WebControllerTestNotUsingMockBean it's not using a custom include filter) so there's no existing bean definition to use to name the mocked bean. As a result, we fall back to using a name generated by DefaultBeanNameGenerator.

What I described above as a workaround is actually the solution. In the absence of an existing bean to provide the name, you need to use the name attribute on @MockBean if the bean's name is important.

Member

wilkinsona commented Oct 13, 2016

It looks like the use of @MockBean has changed the name of the bean.

I was wrong. WebControllerTest doesn't contain a HelloService bean (unlike WebControllerTestNotUsingMockBean it's not using a custom include filter) so there's no existing bean definition to use to name the mocked bean. As a result, we fall back to using a name generated by DefaultBeanNameGenerator.

What I described above as a workaround is actually the solution. In the absence of an existing bean to provide the name, you need to use the name attribute on @MockBean if the bean's name is important.

@michael-simons

This comment has been minimized.

Show comment
Hide comment
@michael-simons

michael-simons Oct 13, 2016

Contributor

Perfect @wilkinsona, thank you! I didn't thought about a naming problem at all, so sorry if I didn't ask into this direction.

I might write a short blog post about it, I think that topic could be interesting to others as well, if this is ok for you.

Contributor

michael-simons commented Oct 13, 2016

Perfect @wilkinsona, thank you! I didn't thought about a naming problem at all, so sorry if I didn't ask into this direction.

I might write a short blog post about it, I think that topic could be interesting to others as well, if this is ok for you.

@michael-simons

This comment has been minimized.

Show comment
Hide comment
@michael-simons

michael-simons Oct 13, 2016

Contributor

Just one more feedback: That also solves our root problem, using one service in a sec:authorize attribute.

Contributor

michael-simons commented Oct 13, 2016

Just one more feedback: That also solves our root problem, using one service in a sec:authorize attribute.

@wilkinsona

This comment has been minimized.

Show comment
Hide comment
@wilkinsona

wilkinsona Oct 13, 2016

Member

Sorry, I missed your comment earlier about possibly writing a blog post. I'm very pleased to see that you did so. For anyone who's interested, here it is

Member

wilkinsona commented Oct 13, 2016

Sorry, I missed your comment earlier about possibly writing a blog post. I'm very pleased to see that you did so. For anyone who's interested, here it is

@michael-simons

This comment has been minimized.

Show comment
Hide comment
@michael-simons

michael-simons Oct 13, 2016

Contributor

I've added Olivers feedback to the post as well, solving the architectural problem causing our trouble in the start :)

Contributor

michael-simons commented Oct 13, 2016

I've added Olivers feedback to the post as well, solving the architectural problem causing our trouble in the start :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment