Spring Boot unit test with SpringSecurity and Mock - java

Currently I have several unit tests that's working correctly.
In my unit test, on init, I have included the following code:
#Mock
private UsersServices usersServices;
#InjectMocks
private UsersController usersControllers;
#Before
public void init() {
this.mvc = MockMvcBuilders.standaloneSetup(usuariosController)
.addFilter(springSecurityFilterChain)
.setControllerAdvice(new UsuariosControllerAdvice(logService)).build();
}
this worked great, but some authorizations annotations, like #PreAuthorize are ignored. (In my WebSecurityConfig, I already added the #EnableGlobalMethodSecurity(prePostEnabled = true) annotation.
So, after some time, I found the following code:
#Mock
private UsersServices usersServices;
#InjectMocks
private UsersController usersControllers;
#Autowired
private WebApplicationContext wac;
#Before
public void init() {
this.mvc = MockMvcBuilders
.webAppContextSetup(wac)
.addFilter(springSecurityFilterChain)
apply(SecurityMockMvcConfigurers.springSecurity(springSecurityFilterChain))
.build();
}
and now, the authorization annotations (#PreAuthorize) works, but the UsersServices mock don't. When I call my controller method on unit tests, the real UserServices was called, not the mock.
Here is a unit test that mock a UserServices:
when(usersServices.getUserAvatar(anyString())).thenReturn(
CompletableFuture.completedFuture(Optional.empty()));
MvcResult result = mvc.perform(
get("/api/users/avatar/{login:.+}", "mock.user")
.header("Authorization", testHelpers.buildJwtToken("USER")))
.andReturn();
mvc.perform(asyncDispatch(result))
.andExpect(status().isNotFound());
without the standaloneSetup, the real userServices.getUserAvatar is called.

This happens because your WebApplicationContext is not aware of your UsersController with mocked UsersServices. To fix this you have two options:
The first option is to use
#MockBean
private UsersServices usersServices;
instead of:
#Mock
private UsersServices usersServices;
This will add mocked bean into application context so that the Spring is aware of it and, thus, will use it instead of a real one.
The second option is to set your controller directly inside WebApplicationContext manually. This option should not be "tried at home", but can be a workaround for cases when you do not have #MockedBean because of the old spring version:
AutowireCapableBeanFactory factory = wac.getAutowireCapableBeanFactory();
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) factory;
registry.removeBeanDefinition(beanId);
//create newBeanObj through GenericBeanDefinition
registry.registerBeanDefinition(beanId, newBeanObj);

Related

Mockito Controller Test I'm getting error

all I'm trying to test my controller. But I'm getting error.
Wanted but not invoked: accountService.findAccount("9090");
-> at com.ontavio.bank.ControllerTests.givenId_Cash_thenReturnJson(ControllerTests.java:67)
However, there was exactly 1 interaction with this mock:
accountService.cash(
"9090",
com.ontavio.bank.model.CashTransaction#2f408960 );
#PostMapping("/cash/{accountNumber}")
public ResponseEntity<TransactionStatus> cash(#PathVariable String accountNumber, #RequestBody CashTransaction cashTransactionRequest) {
return ResponseEntity.ok(accountService.cash(accountNumber, cashTransactionRequest));
}
---
public TransactionStatus cash(String accountNumber, CashTransaction cashTransaction) {
Account account = this.findAccount(accountNumber);
cashTransaction.setType(TransactionTypes.CASH_TRANSACTION.getRelation());
account.post(cashTransaction);
accountRepository.save(accountMapper.AccountToAccountEntity(account));
return TransactionStatus.createTransactionStatus(HttpStatus.OK, "");
}
----
#SpringBootTest
#ContextConfiguration
#AutoConfigureMockMvc
class ControllerTests {
#Spy
#InjectMocks
private AccountController accountController;
#Mock
private AccountService accountService;
#Mock
private AccountMapper accountMapper;
#Mock
private AccountRepository accountRepository;
#Test
public void givenId_Cash_thenReturnJson()
throws Exception {
Account account = new Account("James Harden", "9090");
CashTransaction cashTransaction = new CashTransaction(100.0);
AccountEntity accountEntity = new AccountEntity();
accountEntity.setOwner(account.getOwner());
accountEntity.setAccountNumber(account.getAccountNumber());
TransactionStatus transactionStatus = new TransactionStatus();
transactionStatus.setStatus(HttpStatus.OK.name());
doReturn(account).when(accountService).findAccount( "9090");
doReturn(transactionStatus).when(accountService).cash("9090", depositTransaction);
doReturn(accountEntity).when(accountRepository).save(accountEntity);
ResponseEntity<TransactionStatus> result = accountController.cash( "9090", cashTransaction);
verify(accountService, times(1)).findAccount("9090");
assertEquals("OK", result.getBody().getStatus());
}
My guess is that you are missing
....
#ExtendWith(SpringExtension.class)
#SpringBootTest
#ContextConfiguration
#AutoConfigureMockMvc
class ControllerTests {
.....
above your ControllerTest class. Also i would suggest to use #Autowired instead of #Spy and #InjectMocks, and Annotate your mocked Service,Mapper and repository with #MockBean
OR (depending on your use case)
Use the annotation
#ExtendWith(MockitoExtension.class)
then you have a Mockito test which does not start the spring context. You could remove #SpringBootTest #ContextConfiguration #AutoConfigureMockMvc then. You donĀ“t need #AutoConfigureMockMvc anyways since you do not execute a http call, but call the RestControllers method directly. I dont see why you would need the spring context in your test, so I would prefer to use the Mockito Test. It is much faster since the applicationContext does not start.
Mixing Spring and Mockito testing environment works, but can make for weird circumstances. I generally try to stay in one. Either test a class and mock everything with Mockito or write an integration Test and use the Spring utilities.

Spring Boot 2.x Servlet Path is ignored in test

in my application-test.properties I have this server.servlet.context-path=/api
It works totally fine when I run the application and test it with postman. But as soon as I run my tests it swallows the part /api of the path.
So basically how it should be
localhost:8080/api/testUrl
but the controller is only available here
localhost:8080/testUrl
My Testclass head
#ExtendWith(SpringExtension.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
#AutoConfigureMockMvc
public class QaControllerIntegrationTest {
private static final String QA_URL = "/api";
#Autowired
private MockMvc mockMvc;
#MockBean
private QaService qaService;
#Autowired
private TestRestTemplate testRestTemplate;
no setup behavior implemented.
and tests (only for the sake of completeness - they would work if I remove the QA_URL)
#Test
void getQuestions() {
final ResponseEntity<List<QuestionAnswerDTO>> listResponseEntity = testRestTemplate.exchange(
QA_URL + "/questions", HttpMethod.GET, null, new ParameterizedTypeReference<>() {
});
assertThat(listResponseEntity.getStatusCode()).isEqualByComparingTo(HttpStatus.OK);
assertThat(listResponseEntity.getBody().get(0).getQuestion()).isEqualTo(QUESTION);
}
#Test
void addNewQa() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post(QA_URL + "/question")
.content(JacksonUtils.toString(questionAnswerDTO, false))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isCreated());
}
What do I miss here please?
Thank you =)
Because MockMvc isn't autoconfigured with context path and thus is unaware of it. If you want to include it, you can do:
MockMvcRequestBuilders.post(QA_URL + "/question").contextPath(QA_URL)
Notice prefix must match in order for Spring to figure out the remaining path. Typically a test shouldn't care about the context they are in therefore context path is never included.

#SpringBootTest: #MockBean not injected when multiple test classes

I want to write controller tests that also test my annotations. What I've read so far is that RestAssured one of the ways to go.
It works smoothly when I only have one controller test in place. However, when having 2 or more controller test classes in place, the #MockBeans seem to not be used properly.
Depending on the test execution order, all tests from the first test class succeed, and all others fail.
In the following test run, the PotatoControllerTest was executed first, and then the FooControllerTest.
#ExtendWith(SpringExtension.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles({"test", "httptest"})
class FooControllerTest {
#MockBean
protected FooService mockFooService;
#MockBean
protected BarService mockBarService;
#LocalServerPort
protected int port;
#BeforeEach
public void setup() {
RestAssured.port = port;
RestAssured.authentication = basic(TestSecurityConfiguration.ADMIN_USERNAME, TestSecurityConfiguration.ADMIN_PASSWORD);
RestAssured.requestSpecification = new RequestSpecBuilder()
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON)
.build();
}
#SneakyThrows
#Test
void deleteFooNotExists() {
final Foo foo = TestUtils.generateTestFoo();
Mockito.doThrow(new DoesNotExistException("missing")).when(mockFooService).delete(foo.getId(), foo.getVersion());
RestAssured.given()
.when().delete("/v1/foo/{id}/{version}", foo.getId(), foo.getVersion())
.then()
.statusCode(HttpStatus.NOT_FOUND.value());
Mockito.verify(mockFooService, times(1)).delete(foo.getId(), foo.getVersion());
}
...
}
#ExtendWith(SpringExtension.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles({"test", "httptest"})
class PotatoControllerTest {
#MockBean
protected PotatoService mockPotatoService;
#LocalServerPort
protected int port;
#BeforeEach
public void setup() {
RestAssured.port = port;
RestAssured.authentication = basic(TestSecurityConfiguration.ADMIN_USERNAME, TestSecurityConfiguration.ADMIN_PASSWORD);
RestAssured.requestSpecification = new RequestSpecBuilder()
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON)
.build();
}
...
}
Wanted but not invoked:
fooService bean.delete(
"10e76ae4-ec1b-49ce-b162-8a5c587de2a8",
"06db13f1-c4cd-435d-9693-b94c26503d40"
);
-> at com.xxx.service.FooService.delete(FooService.java:197)
Actually, there were zero interactions with this mock.
I tried to fix it with a common ControllerTestBase which configures all mocks and all other controller tests extending the base class. Which worked fine on my machine, but e.g. not in the pipeline. So I guess it is not really stable.
Why is Spring not reloading the context with the mocks? Is this the "best" way of testing my controllers?
It would be much easier and way faster to just use MockMvc.
You can just create a standalone setup for your desired controller and do additional configuration (like setting exception resolvers). Also you're able to inject your mocks easily:
#Before
public void init() {
MyController myController = new MyController(mock1, mock2, ...);
MockMvc mockMvc =
MockMvcBuilders.standaloneSetup(myController)
.setHandlerExceptionResolvers(...)
.build();
}
Afterwards you can easily call your endpoints:
MvcResult result = mockMvc.perform(
get("/someApi"))
.andExpect(status().isOk)
.andReturn();
Additional validation on the response can be done like you already know it.
Edit: As a side note - this is designed to explicitly test your web layer. If you want to go for some kind of integration test going further down in your application stack, also covering business logic, this is not the right approach.

Can't mock repository when testing with mockmvc

I have this quite simple controller class and a simple (jpa) repository.
What I want to do is to test it's api but mock it's repository and let it return an object or not depending on the test case.
My problem now is that I don't know how to do that.
I know how to mock a repository and inject it to a controller/service class with #Mock / #InjectMocks / when().return()
But I fail when I want to do the same after doing a request with MockMvc.
Any help is highly appreciated
The controller
import java.util.Optional;
#RestController
#Slf4j
public class ReceiptController implements ReceiptsApi {
#Autowired
private ReceiptRepository receiptRepository;
#Autowired
private ErrorResponseExceptionFactory exceptionFactory;
#Autowired
private ApiErrorResponseFactory errorResponseFactory;
#Override
public Receipt getReceipt(Long id) {
Optional<ReceiptEntity> result = receiptRepository.findById(id);
if (result.isEmpty()) {
throw invalid("id");
}
ReceiptEntity receipt = result.get();
return Receipt.builder().id(receipt.getId()).purchaseId(receipt.getPurchaseId()).payload(receipt.getHtml()).build();
}
private ErrorResponseException invalid(String paramName) {
return exceptionFactory.errorResponse(
errorResponseFactory.create(HttpStatus.NOT_FOUND.value(), "NOT_VALID", String.format("receipt with id %s not found.", paramName))
);
}
}
And it's test class
#WebMvcTest(ReceiptController.class)
#RestClientTest
public class ReceiptControllerTest {
#InjectMocks
private ReceiptController receiptController;
#Mock
private ReceiptRepository receiptRepository;
#Mock
private ErrorResponseExceptionFactory exceptionFactory;
#Mock
private ApiErrorResponseFactory errorResponseFactory;
private MockMvc mvc;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mvc = MockMvcBuilders.standaloneSetup(
new ReceiptController())
.build();
}
#Test
public void getReceiptNotFoundByRequest() throws Exception {
mvc.perform(MockMvcRequestBuilders
.get("/receipt/1")
.header("host", "localhost:1348")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
//TODO: Finish this test
#Test
public void getReceiptFoundByRequest() throws Exception {
ReceiptEntity receipt1 = ReceiptEntity.builder().id(99999L).purchaseId(432L).html("<html>").build();
when(receiptRepository.findById(1L)).thenReturn(Optional.of(ReceiptEntity.builder().id(1L).purchaseId(42L).html("<html></html>").build()));
ResultActions result = mvc.perform(get("/receipt/1")
.header("host", "localhost:1348")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
Within your setUp() method, you're using Mockito to mock the beans annotated with #Mock and inject them in a new instance of ReceiptController, which is then put into the field annotated with #InjectMocks.
On the next line, you're setting up MockMvc with a new instance of ReceiptController (you use new ReceiptController()), rather than using the instance that Mockito created. That means that all fields within that instance will be null.
This pretty much boils down exactly to Why is my Spring #Autowired field null.
To solve this, you could pass receiptController to MockMvc. In that case, you could also remove the #WebMvcTest and #RestClientTest as you're not using them.
Alternatively, you could setup your test with #RunWith(SpringRunner.class), and properly set up a Spring test by using #Autowired in stead of #InjectMocks and #MockBean in stead of #Mock. In that case, you don't need a setUp() method at all, as MockMvc could be autowired by Spring.
#WebMvcTest and MockMvc allows you to do integration testing, not unit testing.
They allow you to boot an actual Spring application (using a random port) and test the actual controller class with its dependencies. This means that the variables you declared at the top of your test are not actually used in your current setup.
If you wish to unit-test your controller, you can remove #WebMvcTest, create mock dependencies (like you did) and call your methods directly instead of using MockMvc.
If you really wish to write an integration test, you need to mock your external dependencies (the database for example). You can configure spring to use an embedded database (H2 for example) in the test environment, so that you do not affect your real database.
See an example here : https://www.baeldung.com/spring-testing-separate-data-source

Spring testing annotations

I've been working on a personal project very recently and looking at my test file I realized I have some regarding spring annotations:
#RunWith(MockitoJUnitRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
public class BookingServicesTests {
private MockMvc mvc;
#Mock
private BookingRepository bookingRepository;
#InjectMocks
private BookingResource bookingController;
#Before
public void setup() {
JacksonTester.initFields(this, new ObjectMapper());
mvc = MockMvcBuilders
.standaloneSetup(bookingController)
.setControllerAdvice(new ConflictExceptionController())
.build();
}
...
}
So the thing is that #SpringBootTest is made to test your application using real HTTP methods. But in my setup method I included a MockMvcBuilders statement, which is a standalone test (no server and no application context).
My question is:
Are those elements incompatible?
One element obfuscate the other? This is: by using MockMvcBuilder can I get rid of #SpringBootTest?
Thanks
Use one or the other, not both. You are only allowed one JUnit's #runwith() and the value you pass in, whether it be SpringRunner.class or MockitoJUnitRunner.class, has very different behaviors.
So the code you posted is incorrect as #SpringBootTest will try to load the application context when your test class is "running with MockitoJUnitRunner". Therefore #SpringBootTest should be used along with #runWith(SpringRunner.class), as such
#RunWith(SpringRunner.class)
#WebMvcTest(BookingResource.class) // multiple controller class could go here
#AutoConfigureMockMvc
public class BookingServicesTests {
#Autowired
private MockMvc mvc;
#MockBean
private BookingRepository bookingRepository;
...
}
Notice how I replace #SpringBootTest() with #WebMvcTest(). This is because #WebMvcTest() only scans components that are #Controller and loads configuration for the web layer, whereas #SpringBootTest() does so for the entire application.
Or what you did with Mockito without Spring:
#RunWith(MockitoJUnitRunner.class)
public class BookingServicesTests {
private MockMvc mvc;
#Mock
private BookingRepository bookingRepository;
#InjectMocks
private BookingResource bookingController;
...
}

Categories

Resources