I'm having trouble running my tests, when using the @MockBean annotation to mock a behavior, Spring reloads the context. As the example below:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = WalletPaymentApplication.class, properties = { "spring.cloud.config.enabled:true",
"management.security.enabled=false" })
@RabbitListenerTest
@Transactional
@ActiveProfiles("test")
public abstract class WalletPaymentApplicationTests {
public static final String JSON_MEDIA_TYPE = "application/json;charset=UTF-8";
@Autowired
protected EntityManager em;
@MockBean
protected RestTemplate template;
protected MockMvc mockMvc;
@Autowired
protected WebApplicationContext wac;
@SpyBean
protected DomainEventPublisher domainEventPublisher;
}
public class PaymentControllerTest extends WalletPaymentApplicationTests {
@MockBean
private PaymentService service;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void newPayment() throws Exception {
when(service.newPayment(any(PaymentCommand.class)))
.thenReturn(CompletableFuture.supplyAsync(() -> new PaymentResumeData(any(MerchantData.class),
any(CashbackData.class), any(BigDecimal.class), anyListOf(SummaryCardData.class))));
String payload = "{\"paymentId\": \"4548888888888888\","
+ "\"customerId\": \"100\","
+ "\"merchantSubKey\": \"4df4447hjh8g-4d4g5f-vdgfg\","
+ "\"amount\": 650.00,"
+ "\"taxId\": \"frfr6-d4g4v7-4b8f\"}";
MockHttpServletRequestBuilder builder = post("/payment").contentType(MediaType.parseMediaType(JSON_MEDIA_TYPE))
.content(payload);
mockMvc.perform(builder).andExpect(request().asyncStarted()).andExpect(status().isOk());
}
public class PaymentServiceTest extends WalletPaymentApplicationTests {
@Test
public void newPayment() throws Exception {
String date = formatarData(new Date(), "yyyy-MM-dd HH:mm:ss");
when(template.getForObject(configureUrl(customerUrl), CustomerRows.class, "100"))
.thenReturn(fixture.fixtureCustomer());
when(template.getForObject(merchantUrl, MerchantRows.class, "wevf-f5vgb-5f", date))
.thenReturn(fixture.fixtureMerchant());
when(template.getForObject(configureUrl(cardUrl), CardRows.class, "100"))
.thenReturn(fixture.fixtureCard());
PaymentResumeData resume = service.newPayment(new PaymentCommand("12378000000000000", "100",
"wevf-f5vgb-5f", 3000L, "85640827807")).get();
Payment payment = repository.getPaymentById(new PaymentId("12378000000000000"))
.orElseThrow(() -> new PaymentNotFoundException());
assertEquals(BigDecimal.valueOf(150.60), resume.getBalance());
assertEquals("promocode", resume.getCashback().getType());
}
For test scenario, the spring recharges the context 3 times.
If i remove the annotation, spring starts the context only once
The Spring test framework will cache an ApplicationContext whenever possible between test runs. In order to be cached, the context must have an exactly equivalent configuration. Whenever you use @MockBean, you are by definition changing the context configuration.
The PaymentServiceTest extends WalletPaymentApplicationTests and inherits a @MockBean of RestTemplate (the config is WalletPaymentApplication + mock RestTemplate).
The PaymentControllerTest also extends WalletPaymentApplicationTests, but it defines an additional PaymentService @MockBean (the config is WalletPaymentApplication + mock RestTemplate + mock PaymentService).
This additional mock means that the same context can't be cached. In PaymentServiceTest the PaymentService is real, in PaymentControllerTest it's a mock. The two contexts contain different beans.
I understood, how could i mock the external calls then in my real tests with the restTemplate, without the spring reloading the context?
I generally use MockRestServiceServer or WireMock for that kind of thing. I gave a talk that covered some testing options at Spring One last year that might be helpful. You can watch a recording here.
@eutiagocosta @philwebb I had a similar issue, and I have overcome it by doing the following. I'm having in the test sources a separate configuration for bean mocking:
@Configration
public class MockConfig {
@Bean
public ExternalService externalService() {
return mock(ExternalService.class);
}
}
And in the test class I'm just using @Autowire for the ExternalService:
@Autowire
private ExternalService externalService;
This is the workaround, but it does the job for me.
@SeriyBg your workaround - mocking will be happening for all test classes right? How can we do this only for that test class ?
@surapuramakhil Yes, for only particular test class, you can create mock object and inject it on demand by using ReflectionTestutils in test class.
ExternalService externalService = Mockito.mock(ExternalService.class);
@Test
void testMockBeanWorksWithInjection (){
ReflectionTestUtils.setField(<AutowiredClassWhichHasMockField>, "externalService", externalService);
// Rest of test code
}
Most helpful comment
@eutiagocosta @philwebb I had a similar issue, and I have overcome it by doing the following. I'm having in the test sources a separate configuration for bean mocking:
And in the test class I'm just using
@Autowirefor theExternalService:This is the workaround, but it does the job for me.