0

I have a REST controller in a Spring boot application, simplyfied:

@RestController
@RequestMapping("/api")
public class MyRestController {
    @Autowired
    private Environment env;

    private String property1;

    @PostConstruct
    private void init() {
        this.property1 = env.getProperty("myproperties.property_1");
    }

    @GetMapping("/mydata")
    public String getMyData() {     
        System.out.println("property1: " + this.property1);
        ...
    }

In application.yml I have defined the property similar to:

myproperties:
    property_1: value_1

When I use the REST controller, it works as expected, the value value_1 is read, and in the GET method present.

Now I wanted to test it with a unit test, similar too:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyApp.class)
public class MyRestControllerTest {
    @Autowired
    private MappingJackson2HttpMessageConverter jacksonMessageConverter;    

    @Autowired
    private PageableHandlerMethodArgumentResolver pageableArgumentResolver; 

    @Autowired
    private ExceptionTranslator exceptionTranslator;    

    private MockMvc restMyRestControllerMockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        final MyRestController myRestController = new MyRestController();

        this.restMyRestControllerMockMvc = MockMvcBuilders.standaloneSetup(myRestController)
                .setCustomArgumentResolvers(pageableArgumentResolver).setControllerAdvice(exceptionTranslator)
                .setConversionService(createFormattingConversionService()).setMessageConverters(jacksonMessageConverter)
                .build();
    }

    @Test
    public void getMyDataTest() throws Exception {
        restMyRestControllerMockMvc.perform(get("/api/mydata"))
            .andExpect(status().isOk());
    }

When the method in test is executed, the value of the property property1 is null.

Why is that?

The code above is partially generated by JHipster, I'm not sure if this is a optimal solution, just reused it.

Thanks!

neblaz
  • 683
  • 10
  • 32
  • You're creating the controller outside Spring, irregardless of the rest of the MockMVC setup. what if you autowire the WebApplicationContext and create it through `webAppContextSetup` factory method? – Darren Forsythe Feb 26 '18 at 21:00
  • Could you please provide code snippet for it? – neblaz Feb 26 '18 at 21:07
  • https://gist.github.com/Flaw101/c74755bc5e23903e826fc837ec07d2da – Darren Forsythe Feb 26 '18 at 21:12
  • When I use your suggestion, it says: The method setCustomArgumentResolvers(PageableHandlerMethodArgumentResolver) is undefined for the type DefaultMockMvcBuilder – neblaz Feb 27 '18 at 06:33
  • Also for: The method setConversionService(FormattingConversionService) is undefined for the type DefaultMockMvcBuilder. – neblaz Feb 27 '18 at 06:41
  • Not sure what this is for and if this is necessary, as this is generated by JHipster: .setCustomArgumentResolvers(pageableArgumentResolver).setControllerAdvice(exceptionTranslator) .setConversionService(createFormattingConversionService()).setMessageConverters(jacksonMessageConverter) – neblaz Feb 27 '18 at 06:42
  • Sorry, you ca can the rest of the builder arguments as it should load everything else. – Darren Forsythe Feb 27 '18 at 09:08

3 Answers3

1

MockMvcBuilders.standaloneSetup not loads SpringContext so properties data are not available. You can verify this by using @Value("${myproperties.property_1}") annotation directly inside MyRestControllerTest - it will return "value_1" value (but inside MyRestController - will return null).

Please change it to MockMvcBuilders.webAppContextSetup and inject WebApplicationContext. (Eventually you can inject Environment bean into MyRestController by it constructor, but in my opinion this is Spring hacking.)

Warning: also remember that (in Maven layout project) application.yml need to be copied to src/test/resources.

Code example:

@RestController
@RequestMapping("/api")
public class MyRestController {

    @Autowired
    private Environment env;

    private String envProperty;

    @Value("${myproperties.property_1}")
    private String valueProperty;

    @PostConstruct
    private void init() {
        this.envProperty = env.getProperty("myproperties.property_1");
    }

    @GetMapping("/mydata")
    public String getMyData() {
        System.out.println("envProperty: " + this.envProperty);
        System.out.println("valueProperty: " + this.valueProperty);
        return "";
    }

    @GetMapping("/myproblem")
    public String getMyProblem() {
        throw new IllegalArgumentException();
    }

}

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyApp.class)
public class MyRestControllerTest {

    private MockMvc restMyRestControllerMockMvc;

    @Autowired
    private WebApplicationContext context;

    @Before
    public void setup() {
        final MyRestController myRestController = new MyRestController();
//        this.restMyRestControllerMockMvc = MockMvcBuilders.standaloneSetup(myRestController)
//                .build();
        this.restMyRestControllerMockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
    }

    @Test
    public void getMyDataTest() throws Exception {
        restMyRestControllerMockMvc.perform(get("/api/mydata"));
    }

    @Test
    public void getMyProblemTest() throws Exception {
        restMyRestControllerMockMvc.perform(get("/api/myproblem"))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isConflict());
    }

}

@ControllerAdvice
public class ControllerAdvicer {

    @ResponseStatus(HttpStatus.CONFLICT)
    @ExceptionHandler(IllegalArgumentException.class)
    public String assertionException(final IllegalArgumentException e) {
        return "xxx";
    }

}
kasopey
  • 355
  • 4
  • 17
  • Yes, because I thought that would have been the mistake. – neblaz Feb 26 '18 at 21:08
  • 1
    SpringBoot will load the application.yml from the `src/main/resources`. Its not required to duplicate it. – Darren Forsythe Feb 26 '18 at 21:13
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/low-quality-posts/18942404) – Kirill Simonov Feb 26 '18 at 21:21
  • @Kirill Simonov - Thx for your advices, but unfortunately I can't add comments at now (under not-my-post). – kasopey Feb 26 '18 at 21:32
  • @kasopey I understand that. But since your post looks more like a comment than an answer, it appears in the Low Quality Posts queue. This is also the reason why you get downvotes (by the way, this one is not mine). So I suggest you to wait a little - you only need to get 6 points to be able to comment everywhere ;) – Kirill Simonov Feb 26 '18 at 21:44
  • @kasopey - Not to nitpick, but it would serve you well to make your answer look a bit more like an answer (without starting it off with a question or putting "Edit" in it). You can use the `>` symbol to create a yellow text block to make "notes" stand out. It also helps to briefly explain what the code does to help others to learn. The reason I bring this up is that others who are reviewing your answer are still voting to delete it (as indicated by the comment by jww). – NightOwl888 Feb 27 '18 at 01:35
  • @kasopey: Could you also explain, why to use @AutoConfigureMockMvc? – neblaz Mar 02 '18 at 07:53
  • @kasopey: You say, you "@Autowired" the MockMvc, but then in method setup() it is reassigned via MockMvcBuilders.webAppContextSetup(...), so what was done via "@Autowired" is then overwritten? – neblaz Mar 05 '18 at 11:34
  • Good point @neblaz. It was sense-less - my mistake (too many copy-paste during testing different solutions of your problem). Answer updated. – kasopey Mar 06 '18 at 20:05
1

Use @Value annotation to read values from your app.yml

@RestController
@RequestMapping("/api")
public class MyRestController {
    @Autowired
    private Environment env;

    @Value("${myproperties.property_1}")
    private String property1;

    @GetMapping("/mydata")
    public String getMyData() {     
        System.out.println("property1: " + this.property1);
        ...
    }

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html

Mehtrick
  • 518
  • 3
  • 12
  • This suggestion is just another way for reading values from .yml files, but still I have to use webAppContextSetup? – neblaz Feb 27 '18 at 06:58
  • No it is just basic spring. Don't need other things. – Mehtrick Feb 27 '18 at 08:34
  • Just to avoid misunderstanding: If I use @Value instead of Environment, I will have the Property values available when running the test method? No usage of MockMvcBuilders.webAppContextSetup(...) is required, can use MockMvcBuilders.standaloneSetup(...)? – neblaz Feb 27 '18 at 10:38
0

I marked kasopey answer as correct, as it contains a complete answer, although in parts the answers of the other responders are also correct.

But still I would like to know what those line are for:

.setCustomArgumentResolvers(pageableArgumentResolver)
.setControllerAdvice(exceptionTranslator)
.setConversionService(createFormattingConversionService())
.setMessageConverters(jacksonMessageConverter)

because with your solution to use

MockMvcBuilders.webAppContextSetup(context)

those methods are not available. How to achive the same, if necessary?

The missing method in my sample code looks like this:

... Create a FormattingConversionService which use ISO date format, instead of the localized one.
public static FormattingConversionService createFormattingConversionService() {
    DefaultFormattingConversionService dfcs = new DefaultFormattingConversionService ();
    DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
    registrar.setUseIsoFormat(true);
    registrar.registerFormatters(dfcs);
    return dfcs;
}

And again, the most part of the code is generated by JHipster, which is quite convenient, but not always clear why and what for this is.

neblaz
  • 683
  • 10
  • 32
  • I'm just guessing, but if all web context is created you have already all yours controller stuff: @ControlerAdvice components, message convert beans, etc. – kasopey Mar 06 '18 at 20:17