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
Comment From: philwebb
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.
Comment From: eutiagocosta
I understood, how could i mock the external calls then in my real tests with the restTemplate, without the spring reloading the context?
Comment From: philwebb
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.
Comment From: SeriyBg
@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.
Comment From: surapuramakhil
@SeriyBg your workaround - mocking will be happening for all test classes right? How can we do this only for that test class ?
Comment From: prp28
@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
}