how can I create a "quasi" MVC integration test in Spring Boot. I would like to use my real implementation of a service, but I can't mange to do it. How can I inject real implementation instead of a mock.
My classes look like this
#Controller
#RequiredArgsConstructor
public class DashboardController {
private final RolesManagerService rolesManagerService;
private final ServletRequestManagerService servletRequestManagerService;
#GetMapping({"/", "/dashboard"})
public String index(Model model, HttpServletRequest httpServletRequest) {
model.addAttribute("canAddNewOrder", rolesManagerService.canRoleAccessApplicationPart(servletRequestManagerService.getRole(httpServletRequest), ApplicationPart.CREATE_NEW_ORDER));
model.addAttribute("var", "test");
return "dashboard";
}
}
and the test
#RunWith(SpringRunner.class)
#WebMvcTest(controllers = DashboardController.class)
#AutoConfigureMockMvc
class IndexControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private UserDetailsService userDetailsService;
#MockBean
RolesManagerService rolesManagerService;
#MockBean
private ServletRequestManagerService servletRequestManagerService;
#Test
void testDashboard() throws Exception {
mockMvc.perform(get("/dashboard").with(user("admin").password("pass").roles("USER","ADMIN")))
.andExpect(status().isOk())
.andExpect(view().name("dashboard"))
.andExpect(xpath("//a").nodeCount(1))
.andExpect(model().attributeExists("canAddNewOrder"))
.andExpect(model().size(2))
.andExpect(model().attribute("var", equalTo("test")))
.andExpect(model().attribute("canAddNewOrder", equalTo(false)))
.andDo(print());
}
}
Generally WebMvcTest will not create a full Spring context with all components (Service etc) injected, but only the Controller you define. Either use a full SpringBootTest, or add something like this in your WebMvcTest class:
#TestConfiguration
static class AdditionalTestConfig {
#Bean
public RolesManagerService getService() {
return new RolesManagerService();
}
}
Related
Preamble: I'm learning Java, Spring Boot and overall... TDD with Java/Spring Boot.
Used versions:
Spring Boot 2.6.3
Java 17
Junit5
This is my controller:
#RestController
#RequestMapping("/api/v1/login")
public class LoginController {
#Autowired
private JwtAuthentication jwtAuthentication;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private JwtUserDetailService jwtUserDetailService;
#PostMapping
public ResponseEntity<?> createAuthenticationToken(#RequestBody UserEntity userEntity) throws Exception {
jwtAuthentication.authenticate(userEntity.getUsername(), userEntity.getPassword());
final UserDetails userDetails = jwtUserDetailService.loadUserByUsername(userEntity.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
}
Relevant autowired is the JwtAuthentication:
#Component
public class JwtAuthentication {
#Autowired
private AuthenticationManager authenticationManager;
private static final long serialVersionUID = -20220210203900L;
public void authenticate(String username, String password) throws BadCredentialsException {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (BadCredentialsException e) {
throw new BadCredentialsException("INVALID_CREDENTIALS", e);
}
}
}
I wrote the test for JwtAuthentication itself without issues, now I need to test the Controller.
This is my test:
#ExtendWith(SpringExtension.class)
#WebMvcTest(LoginControllerTest.class)
class LoginControllerTest {
#Autowired
private MockMvc mvc;
#Autowired
private ObjectMapper objectMapper;
#MockBean
private JwtAuthentication jwtAuthentication;
#Test
void testCanLogin() throws Exception {
UserEntity userEntity = new UserEntity();
userEntity.setUsername("username");
userEntity.setPassword("password");
mvc.perform(post("/api/v1/login/").contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userEntity))).andExpect(status().isOk());
}
}
But I get 404 instead of 200.
I read that reason is missing mocking underlying methods. That, in reality, these tests on Controller doesn't launch entire configuration (and so on). Cannot find the answer atm, here on S.O..
So, I think the solution need to be add a "simple" when in test:
#Test
void testCanLogin() throws Exception {
UserEntity userEntity = new UserEntity();
userEntity.setUsername("username");
userEntity.setPassword("password");
// I think I need some code here:
// pseudocode when(jwtAuthentication.authenticate("username", "password").then .... I DON'T KNOW HERE!
mvc.perform(post("/api/v1/login/").contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userEntity))).andExpect(status().isOk());
}
404 is because of no controllers to handle the request.
It is because you specify a wrong controller in #WebMvcTest such that the controller that you want to test is not included in the spring container. So change to the following should fix the problem :
#WebMvcTest(LoginController.class)
class LoginControllerTest {
#MockBean
private JwtAuthentication jwtAuthentication;
#MockBean
private JwtTokenUtil jwtTokenUtil;
#MockBean
private JwtUserDetailService jwtUserDetailService;
}
Also note the following points :
I remove #ExtendWith(SpringExtension.class) as #WebMvcTest already included it
#WebMvcTest will only enable the beans related to web layers (see this) which JwtTokenUtil and JwtUserDetailService does not belong to it. So you have to use #MockBean to mock them.
I am working on an assignment where a test case is already provided and I cannot change it in any sense whatsoever.
I have created a simple web project using spring boot whose controller looks like :
Controller.java
#RestController
#RequestMapping("/person")
public class Controller {
private final PersonService personService;
#Autowired
public Controller(final PersonService personService) {
this.personService = personService;
}
#PostMapping
public ResponseEntity<PersonResponse> testPerson(#RequestBody PersonRequest personRequest) {
return new ResponseEntity<>(personService.clearPastHistory((personRequest)), HttpStatus.OK);
}
}
And the Service layer is :
Service.java
#Service
public class PersonServiceImplementation implements PersonService {
#Override
public PersonResponse clearPastHistory(PersonRequest requestDTO) {
//business logic here
}
ControllerTest.java
Here, the BASE_REQUEST is a json string which acts as an input from the user.
#WebMvcTest(Controller.class)
class ControllerTest {
private static final MockHttpServletRequestBuilder REQUEST_BUILDER =
request(HttpMethod.POST, "/person")
.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.accept(APPLICATION_JSON);
#Autowired
private MockMvc mockMvc;
#Test
void callCorrectEndpoint() throws Exception {
mockMvc.perform(REQUEST_BUILDER.content(BASE_REQUEST))
.andExpect(status().isOk());
}
Now the issue is I cannot build the project without using
#MockBean
private PersonService personService;
in my test class.
and if I am using this and submitting to an online portal, the automated tests in the portal are failing with an error that Testcase was modified.
Is there a way possible to achieve this without modifying the test case at all ?
Would be highly thankful for any help.
I have a Spring boot code with Aspectj. This code has written with basic MVC architecture. Then I just try to test it with MockMVC. But when I try to test it, Aspectj doesn't interrupted. Is there a special configuration about Aspectj?
Controller:
#GetMapping("user/{userId}/todo-list")
public ResponseEntity<?> getWaitingItems(#RequestUser CurrentUser currentUser){
...handle it with service method.
}
Aspect:
#Pointcut("execution(* *(.., #RequestUser (*), ..))")
void annotatedMethod()
{
}
#Before("annotatedMethod() && #annotation(requestUser)")
public void adviseAnnotatedMethods(JoinPoint joinPoint, RequestUser requestUser)
{
...
}
Test:
#WebMvcTest(value = {Controller.class, Aspect.class})
#ActiveProfiles("test")
#ContextConfiguration(classes = {Controller.class, Aspect.class})
#RunWith(SpringJUnit4ClassRunner.class)
public class ControllerTest
{
#Autowired
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private Controller controller;
#MockBean
private Service service;
#Before
public void setUp()
{
mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.build();
}
#Test
public void getWaitingItems() throws Exception
{
mockMvc.perform(get("/user/{userId}/todo-list", 1L))
.andExpect(status().isOk());
}
}
There is no need for a #SpringBootTest if you wanna do integration tests of specific controller (web layer) + your custom Aspect logic (AOP layer).
Try something like this
#WebMvcTest(controllers = {AnyController.class})
#Import({AopAutoConfiguration.class, ExceptionAspect.class})
public class ErrorControllerAdviceTest {
AnyController.class: controller under test
AopAutoConfiguration.class: Spring Boot auto-configuration of AOP
ExceptionAspect.class: class containing AOP logic
#Aspect
#Component
public class ExceptionAspect {}
Tested with Spring Boot 2.2.1.RELEASE and JUNIT5.
I am unsure, if my solution is technically the same like #Deadpool answers
Spring #WebMvcTest will only instantiate web layer and it will not load complete application context
However, in this test, Spring Boot instantiates only the web layer rather than the whole context.
In order to test Aspectj you need to load whole application context using #SpringBootTest annotation
The #SpringBootTest annotation tells Spring Boot to look for a main configuration class (one with #SpringBootApplication, for instance) and use that to start a Spring application context
So annotate the test using #SpringBootTest annotation
#SpringBootTest
#ActiveProfiles("test")
#RunWith(SpringRunner.class)
#AutoConfigureMockMvc
public class ControllerTest {
#Autowired
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private Controller controller;
#Before
public void setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.build();
}
#Test
public void getWaitingItems() throws Exception {
mockMvc.perform(get("/user/{userId}/todo-list", 1L))
.andExpect(status().isOk());
}
}
I've been trying to figure out why my mocked findIngredientsByCategory method is returning null when I have when(controller.findIngredientsByCategory(any()).thenReturn(Collections.emptyList()). This implementation works for the findAll method works.
Below is my implementation for my unit test:
#RunWith(SpringJUnit4ClassRunner.class)
#WebMvcTest(IngredientController.class)
#ContextConfiguration(classes = {TestContext.class, WebApplicationContext.class})
#WebAppConfiguration
public class IngredientControllerTest {
#Autowired
private WebApplicationContext context;
#Autowired
private MockMvc mvc;
#MockBean
private IngredientController ingredientController;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
mvc = MockMvcBuilders.webAppContextSetup(context).build();
}
#Autowired
private ObjectMapper mapper;
private static class Behavior {
IngredientController ingredientController;
public static Behavior set(IngredientController ingredientController) {
Behavior behavior = new Behavior();
behavior.ingredientController = ingredientController;
return behavior;
}
public Behavior hasNoIngredients() {
when(ingredientController.getAllIngredients()).thenReturn(Collections.emptyList());
when(ingredientController.getIngredientsByCategory(any())).thenReturn(Collections.emptyList());
when(ingredientController.getIngredientById(anyString())).thenReturn(Optional.empty());
return this;
}
}
#Test
public void getIngredientsByCategoryNoIngredients() throws Exception {
Behavior.set(ingredientController).hasNoIngredients();
MvcResult result = mvc.perform(get("/ingredients/filter=meat"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andReturn();
String content = result.getResponse().getContentAsString();
System.out.println(content);
}
And below is the implementation for the controller:
#RestController
#RequestMapping("/ingredients")
public class IngredientController {
#Autowired
private IngredientRepository repository;
#RequestMapping(value = "/filter", method = RequestMethod.GET)
public List getIngredientsByCategory(#RequestParam("category") String category) {
return repository.findByCategory(category);
}
}
I'm not sure why the mock controller is returning null with this request, when I tell it to return an empty list. If someone could please help with this I would greatly appreciate it! Thanks.
Th request path in test is "/ingredients/filter=meat", but it should be "/ingredients/filter?category=meat". So, it seem that getIngredientsByCategory was not called.
The MockMvc actually will call the IngredientController that is bootstrapped and created by the Spring Test framework but not call the mocked IngredientController that you annotated with #MockBean, so all the stubbing that you made will not be called.
Actually, the point of #WebMvcTest is to test #RestController and its related Spring configuration is configured properly , so a real instance of IngredientController is necessary to create rather than using a mocked one. Instead , you should mock the dependencies inside IngredientController (i.e IngredientRepository).
So , the codes should looks like:
#RunWith(SpringJUnit4ClassRunner.class)
#WebMvcTest(IngredientController.class)
#ContextConfiguration(classes = {TestContext.class, WebApplicationContext.class})
#WebAppConfiguration
public class IngredientControllerTest {
#Autowired
private WebApplicationContext context;
#Autowired
private MockMvc mvc;
#MockBean
private IngredientRepository ingredientRepository;
#Test
public void fooTest(){
when(ingredientRepository.findByCategory(any()).thenReturn(Collections.emptyList())
//And use the MockMvc to send a request to the controller,
//and then assert the returned MvcResult
}
}
I am trying to run a Spring MVC Test but keep getting this exception.
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException
The exception is occuring because the autowired dependency,
#Autowired
private AccountService accountService;
is not getting injected in the test (works fine outside of the test).
Can anyone help me with this. Here is my code:
//AccountControllerITest Class
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = MockServletContext.class)
#WebAppConfiguration
public class AccountControllerITest {
private MockMvc mvc;
ObjectMapper om;
#Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}
#Test
public void getAccounts() throws Exception {
MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.get("/api/accounts"))
.andExpect(status().isOk())
.andReturn();
}
}
}
//AccountController
#RestController
#RequestMapping("/api/accounts")
public class AccountController {
#Autowired
private AccountService accountService;
#RequestMapping(method = RequestMethod.GET)
public ResponseEntity<Set<AccountInfo>> getAccounts(#RequestParam(value = "firstName", required = false) String firstName,
#RequestParam(value = "surName", required = false) String surName) {
Set<AccountInfo> accounts = accountService.getAccounts(firstName, surName);
return new ResponseEntity<>(accounts, HttpStatus.OK);
}
}
Thanks for the help!
Because you are using standalone setup: mvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();. If you create controller via new AccountController(), Spring doesn't have a chance to inject accountService as it doesn't control instances creation and wiring.
You have two options:
Switch your test to unit test and do not use SpringJUnit4ClassRunner nor MockServletContext at
all. You can use #InjectMocks to inject private accountService:
public class AccountControllerITest {
private MockMvc mvc;
ObjectMapper om;
#Mock
private AccountController accountController = new AccountController();
#InjectMocks
private AccountService accountService = new Mockito.mock(AccountService.class);
#Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.standaloneSetup(accountController).build();
}
There is also enhancement you can apply to your controller. Replace field injection with constructor injection and you can pass accountService to controller via constructor in test. This way you don't need to use #InjectMocks at all.
Use webAppContext setup:
#Autowired
private WebApplicationContext webApplicationContext;
#BeforeMethod
public void init() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
You need to configure your test to inject your autowired properties. See the following code block:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
public class AccountControllerITest {
// ... your test code
#Configuration
public static class YourConfiguration {
#Bean
AccountService accountService() {
// init and return your AccountService
}
}
}
You can init your AccountService with Mockito.