I have the following components in my application code. On application startup, I want to call some init methods. However, during testing, I want to do nothing when these methods are called.
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class ManagementService {
@Autowired
private Temp temp;
@EventListener(ApplicationReadyEvent.class)
public void startup() {
log.info("Startup on application ready.");
temp.print();
}
}
@Slf4j
@Component
public class Temp {
public void print() {
log.info("Inside temp");
}
}
I used @BeforeEach to stub the methods like so:
@SpringBootTest
public abstract class SpringIntegrationBaseTest {
@SpyBean
protected ManagementService service;
@SpyBean
protected Temp temp;
@BeforeEach
public void preventConnection() throws SQLException {
Mockito.doNothing().when(temp).print();
}
}
However, I noticed that the logs from the first unit test shows:
2024-10-23 10:24:16.675 INFO 23628 [ main] o.a.c.l.i.Jdk14Logger : Started ManagementServiceTests in 3.382 seconds (process running for 4.907)
2024-10-23 10:24:16.694 INFO 23628 [ main] c.c.s.ManagementService : Startup on application ready.
2024-10-23 10:24:16.696 INFO 23628 [ main] c.c.s.Temp : Inside temp
and on the second unit test it correctly does nothing for print().
2024-10-23 10:36:36.799 INFO 17496 [ main] o.a.c.l.i.Jdk14Logger : Started RestControllerTest in 0.821 seconds (process running for 5.924)
2024-10-23 10:36:36.801 INFO 17496 [ main] c.c.s.ManagementService : Startup on application ready.
On further inspection, I noticed that the stubbing is only done directly before the test, and when application context initialized, the print() method is still called because the stubbing is not done yet, but on the next test, the previous stubbing is carried over.
I think it would look something like this:
application context initialize --> Event Listener called --> BeforeEach stubbing --> Test
I would like something like this:
??? --> stubbing --> ??? --> Event Listener called --> ??? --> Test
where ??? is any intermediate step
I found that I can use @MockBean to just mock Temp and do nothing for all methods by default, however, during testing I need to use the actual behavior of some methods which is why I would like to use @SpyBean to not have to stub all methods used in testing.
Edit: I have also tried using a @PostConstruct inside the SpringIntegrationBaseTest like so:
@PostConstruct
public void init() throws Exception {
log.info("postconstruct init");
Mockito.doNothing().when(temp).print();
}
However, this also executes AFTER the event listener annotated method.
I managed to come up with a passable answer (imo). At first my thought process was to delay the EventListener by having the ManagementService listen to some other custom event i.e. StubInjectionComplete.class
and have a class listening to ApplicationReadyEvent that will intercept and inject stubs in between, before publishing the above custom event.
However, I decided to just use a simple java class to hold my stubbing that I can just run later and using @TestConfiguration, I inject the injector with stubbing as a callable (runnable is not able to throw checked exceptions):
public class StubInjector {
private final Callable injected;
public StubInjector(Callable injected) {
this.injected = injected;
}
public void run() throws Exception {
this.injected.call();
}
}
@TestConfiguration
@Slf4j
public class StubConfiguration {
@Bean
public StubInjector getInjector(Temp temp) {
return new StubInjector(() -> {
log.info("Injected stubs");
Mockito.doNothing().when(temp).print();
return null;
});
}
}
And I modified my existing code and unit test like so:
@SpringBootTest
@ActiveProfiles("test") //<--
@Import(StubConfiguration.class) //<--
public abstract class SpringIntegrationBaseTest {
...
}
public class ManagementService {
@Autowired
private StubInjector injector;
...
@EventListener(ApplicationReadyEvent.class)
public void startup() throws Exception {
log.info("Startup on application ready.");
injector.run(); //<--
...
}
...
}
@Configuration
public class MiscConfig {
@Bean
@Profile("!test")
public StubInjector stubInjector() {
return new StubInjector(()-> null);
}
}
Now, on regular runs, it does nothing besides an extra call to an empty callable, but on unit tests, I inject some stubbing before my regular code is executed on ApplicationReadyEvent.
Edit:
An alternative I just tried was to create another class specifically for all the Scheduled and EventListener annotations. In this class, I autowire the original ManagementService class, and create scheduled and eventlistener methods to call the actual methods in the management service class. During testing, I just use @Profile to turn off all the @Scheduled and @EventListener methods in the new class