Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring REST controller behaves differently in unit tests

The problem

I am new to Spring and trying to write some unit tests for my REST controller. Testing manually with httpie or curl works well, however, with @WebMvcTest, strange things happen.

Here is what happens when I PUT a new user by curl:

$ curl -v -H'Content-Type: application/json' -d@- localhost:8080/api/users <john_smith.json                                                                                                                                  
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /api/users HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.69.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 102
> 
* upload completely sent off: 102 out of 102 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 18 Apr 2020 22:29:43 GMT
< 
* Connection #0 to host localhost left intact
{"id":1,"firstName":"John","lastName":"Smith","email":"[email protected]","password":"l33tp4ss"}

As you can see, the Content-Type header is there in the response and the body is indeed the new User.

Below is how I am trying to test the same automatically:

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserService service;

    private final User john = new User("John", "Smith",
                                       "[email protected]",
                                       "s3curep4ss");

    @Test
    public void givenNoUser_whenCreateUser_thenOk()
    throws Exception
    {
        given(service.create(john)).willReturn(john);

        mvc.perform(post("/users")
                    .contentType(APPLICATION_JSON)
                    .content(objectToJsonBytes(john)))
        .andExpect(status().isOk())
        .andExpect(content().contentType(APPLICATION_JSON))
        .andExpect(jsonPath("$.id", is(0)))
        .andDo(document("user"));
    }

}

But what I get is this:

$ mvn test
[...]
MockHttpServletRequest:                                                                                                                
      HTTP Method = POST    
      Request URI = /users                   
       Parameters = {}                                                                                                                 
          Headers = [Content-Type:"application/json", Content-Length:"103"]                                                                                                                                                                                     
             Body = {"id":0,"firstName":"John","lastName":"Smith","email":"[email protected]","password":"s3curep4ss"}
    Session Attrs = {}                                                                                                                                                                                                                                                        

Handler:                
             Type = webshop.controller.UserController
           Method = webshop.controller.UserController#create(Base)                                                                     

Async:                                        
    Async started = false                                                                                                              
     Async result = null                      

Resolved Exception:                       
             Type = null                                           

ModelAndView:                                                                                                                                                                                                                                                                 
        View name = null                                                                                                                                                                                                                                                      
             View = null                                                                                                                                                                                                                                                      
            Model = null                                                                                                               

FlashMap:                                                                                                                                                                                                                                                                     
       Attributes = null                                           

MockHttpServletResponse:    
           Status = 200                      
    Error message = null                                                                                                               
          Headers = []                                                                                                                                                                                                                                                        
     Content type = null                                                                                                               
             Body =                                                                                                                                                                                                                                                           
    Forwarded URL = null                                                                                                               
   Redirected URL = null
          Cookies = []                               
[ERROR] Tests run: 6, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 11.271 s <<< FAILURE! - in webshop.UserControllerTest          
[ERROR] givenNoUser_whenCreateUser_thenOk  Time elapsed: 0.376 s  <<< FAILURE!                                                         
java.lang.AssertionError: Content type not set
        at webshop.UserControllerTest.givenNoUser_whenCreateUser_thenOk(UserControllerTest.java:70)

What is happening? Where is the body from the MockHttpServletResponse? I must be missing something, as it seems to act completely differently.


Other code in case it is needed

My generic controller class:

public class GenericController<T extends Base>
implements IGenericController<T> {

    @Autowired
    private IGenericService<T> service;

    @Override
    @PostMapping(consumes = APPLICATION_JSON_VALUE,
                 produces = APPLICATION_JSON_VALUE)
    public T create(@Valid @RequestBody T entity)
    {
        return service.create(entity);
    }

    /* ... Other RequestMethods ... */

}

The actual User controller:

@RestController
@RequestMapping(path="/users")
public class UserController extends GenericController<User> { }

UPDATE 2020-04-22
As suggested, I took generics out of the equation, but it did not help.

like image 393
bp99 Avatar asked Aug 26 '26 13:08

bp99


1 Answers

Seems like the @WebMvcTest annotation is configuring a UserService bean that is using the real implementation, and your bean is somehow ignored.

We can try to create the UserService bean differently

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@Import(UserControllerTest.Config.class)
public class UserControllerTest {

    @TestConfiguration
    static class Config {

        @Primary
        @Bean
        UserService mockedUserService() {
            UserService service = Mockito.mock(UserService.class);
            given(service.create(john)).willReturn(UserControllerTest.john());
            return service;
        }
    }

    static User john() {
        return new User("John", "Smith", "[email protected]", "s3curep4ss");
    }

    ...
}

You could also move the stubbing to a @Before method in your tests

@Configuration
public class CommonTestConfig {
   @Primary
   @Bean
   UserService mockedUserService() {
      return Mockito.mock(UserService.class)
   }
}

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@Import(CommonTestConfig.class)
public class Test1 {
   @Autowired
   private UserService userService;

   @Before
   public void setup() {
      given(userService.create(any())).willReturn(user1());
   }
}

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@Import(CommonTestConfig.class)
public class Test2 {
   @Autowired
   private UserService userService;

   @Before
   public void setup() {
      given(userService.create(any())).willReturn(user2());
   }
}
like image 194
Ahmed Sayed Avatar answered Aug 28 '26 03:08

Ahmed Sayed



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!