@WebMvcTest doesn't work properly (401 and 404 errors)

240 Views Asked by At

Note: this is solved, but it's worth being here, as I think it can help a lot of people. Also, some of the code is in spanish because that's my first language.

So, let me start by bitching. I think testing in Spring is a bit of a clusterfuck. A lot of people do the same thing differently, I'm talking different versions of JUnit, different annotations that can be used to solve the same problem, complex annotations and ways that solve the problem but are not conceptually correct. I would LOVE a video that dives deep into it, without just stopping at the basics. But that's that, let's get to the problem.

The problem I had happened when I was creating a @WebMvcTest(ClienteController.class) to test my ClienteController. I specifically wanted to do a @WebMvcTest instead of a @SpringBootTest, because this wasn't an Integration test, even though that could have solved the issues. The problems were that, first, it wasn't getting the custom security configuration correctly, and second, that it couldn't find the endpoint, even though when running the application normally, it was all ok.

So the first thing that happened was that I needed to provide a bean of type CircuitBreakerRegistry for my controller (see this for reference), that in my case, I had in my custom CircuitBreakerConfiguration. I saw that you could archive what I wanted with two annotations: @ContextConfiguration and @Import. I decided to use the first one because I just thought it was more declarative (BIG MISTAKE), so the code ended up like this:

@WebMvcTest(ClienteController.class)
@ContextConfiguration(CircuitBreakerConfiguration.class)
public class ClienteControllerTests {
    @Autowired
    MockMvc mockMvc;
    @MockBean
    ClienteService clienteService;

    @Test
    public void siNoSeAutenticaDevuelveUnauthorized() throws Exception {
        mockMvc.perform(get("/api/v1/clientes")
                .param("tipoDocumento", "0")
                .param("nroDocumento", "10000000"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    public void siSeAutenticaMalDevuelveUnauthorized() throws Exception {
        mockMvc.perform(get("/api/v1/clientes")
                .param("tipoDocumento", "0")
                .param("nroDocumento", "10000000")
                .with(httpBasic("user", "password"))
            )
            .andExpect(status().isUnauthorized());
    }

    @Test
    public void siSeAutenticaBienPasaLaRequest() throws Exception {
        //dado
        when(clienteService.getCliente(anyInt(), anyInt())).thenReturn(null);

        //entonces
        mockMvc.perform(get("/api/v1/clientes")
                .param("tipoDocumento", "0")
                .param("nroDocumento", "10000000")
                .with(httpBasic("admin", "password"))
            )
            .andExpect(status().isOk());
    }
}

Now I didn't get the error, and the first tests passed, but the final one didn't. That was because the last test is supposed to be a correct request and excpect a 200, but instead I got 401 (like in every other test). After researching, specially here since there are a lot of questions about that, I came to understand that it wasn't using my custom security configuration, but rahter the default, so I also had to include the SecurityConfig here. After trying a bit, I also added @ActiveProfiles("test") and a application-test.yml file under src/test/resources, because in SecurityConfig, I created an in memory user with a username and password defined as secrets in my application.yml. So after all of that, the annotations were as follows:

@WebMvcTest(controllers = ClienteController.class, excludeAutoConfiguration = SecurityAutoConfiguration.class)
@ActiveProfiles("test")
@ContextConfiguration(classes = {CircuitBreakerConfiguration.class, SecurityConfig.class})

This ended up solving the issue, but the tests didn't pass yet, as now I got 404 instead of 401 (meaning the security was set up correctly, but for some reason it didn't find the endpoint). After researching again, I found this post, in which the first answer says that adding @Import(ClienteController.class) would solve the issue, and that for some reason @WebMvcTest(ClienteController.class) wasn't including the ClienteController in the context correctly. I also learned that excluding the autoconfiguration didn't do anything, so it ended up being like this:

@WebMvcTest(controllers = ClienteController.class)
@ActiveProfiles("test")
@ContextConfiguration(classes = {CircuitBreakerConfiguration.class, SecurityConfig.class})
@Import(ClienteController.class)

And this finally worked, and all tests passed. So the conclusion that people reached in that post, in that answer, was that @ContextConfiguration doesn't add, but overwrites which classes are put into the context, thus the ClienteController.class part of @WebMvcTest(controllers = ClienteController.class) was overwitten. So the final version, adding some other needed configurations, is this:

@WebMvcTest(controllers = ClienteController.class)
@ActiveProfiles("test")
@Import({CircuitBreakerConfiguration.class,
    RetryConfiguration.class,
    RateLimiterConfiguration.class,
    SecurityConfig.class})

So........ ok? If there's something I want to ask is what's up with @ContextConfiguration and @Import?, as they seem to try to solve the same issue, with the difference that one overrides and one doesn't. Also, is that the only difference? When would you actilvely go for @ContextConfiguration instead of @Import?

0

There are 0 best solutions below