Reputation: 348
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.
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.
Upvotes: 3
Views: 534
Reputation: 1554
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());
}
}
Upvotes: 3
Reputation: 348
Apparently, the problem was with this line:
given(service.create(john)).willReturn(john);
When I change the first john
(which is a User
object) to e.g. any()
, the test passes just fine.
Could somebody please shed some light for me as to why this is? Swapping john
with any()
works, but feels somewhat hacky. The controller passes the JSON deserialized john
object to its service. Is it simply that that deserialized john
is obviously not the same object as the one I create in the test class?
Upvotes: 2