I have an application that listens for Kafka messages using #KafkaListener inside of a #Component. Now I'd like to make an integration test with a Kafka test container (which spins up Kafka in the background). In my test I want to verify that the listener method was called and finished, however when I use #SpyBean in my test I get:
No bean found for definition [SpyDefinition#7a939c9e name = '', typeToSpy = com.demo.kafka.MessageListener, reset = AFTER]
I'm using Kotling, important classes:
Class to test
#Component
class MessageListener(private val someRepository: SomeRepository){
#KafkaListener
fun listen(records: List<ConsumerRecord<String, String>>) {
// do something with someRepository
}
}
Base test class
#ExtendWith(SpringExtension::class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class KafkaContainerTests {
// some functionality to spin up kafka testcontainer
}
Test class
class MessageListenerTest #Autowired constructor(
private val someRepository: SomeRepository
) : KafkaContainerTests() {
#SpyBean
private lateinit var messageListenerSpy: MessageListener
private var messageListenerLatch = CountDownLatch(1)
#BeforeAll
fun setupLatch() {
logger.debug("setting up latch")
doAnswer {
it.callRealMethod()
messageListenerLatch.count
}.whenever(messageListenerSpy).listen(any())
}
#Test
fun testListener(){
sendKafkaMessage(someValidKafkaMessage)
// assert that the listen method is being called & finished
assertTrue(messageListenerLatch.await(20, TimeUnit.SECONDS))
// and assert someRepository is called
}
}
The reason I am confused is that when I add the MessageListener to the #Autowired constructor of the MessageListenerTest it does get injected successfully.
Why is the test unable to find the bean when using #SpyBean?
It works fine for me with Java:
#SpringBootTest
class So58184716ApplicationTests {
#SpyBean
private Listener listener;
#Test
void test(#Autowired KafkaTemplate<String, String> template) throws InterruptedException {
template.send("so58184716", "foo");
CountDownLatch latch = new CountDownLatch(1);
willAnswer(inv -> {
inv.callRealMethod();
latch.countDown();
return null;
}).given(this.listener).listen("foo");
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
verify(this.listener).listen("foo");
}
}
#SpringBootApplication
public class So58184716Application {
public static void main(String[] args) {
SpringApplication.run(So58184716Application.class, args);
}
#Bean
public NewTopic topic() {
return TopicBuilder.name("so58184716").partitions(1).replicas(1).build();
}
}
#Component
class Listener {
#KafkaListener(id = "so58184716", topics = "so58184716")
public void listen(String in) {
System.out.println(in);
}
}
Related
I have the following publishing class.
#Component
public class Publisher {
#Autowired
private MessageChannel publishingChannel;
#Override
public void publish(Event event) {
publishingChannel.send(event);
}
}
I have the following test class.
#RunWith(SpringRunner.class)
#SpringBootTest
public class PublisherTest {
private final List<Event> events = new ArrayList<>();
#Autowired
private Publisher publisher;
#Test
public void testPublish() {
Event testEvent = new Event("some_id_number");
publisher.publish(testEvent);
Awaitility.await()
.atMost(2, TimeUnit.SECONDS)
.until(() -> !this.events.isEmpty());
}
#ServiceActivator(inputChannel = "publishingChannel")
public void publishEventListener(Event event) {
this.events.add(event);
}
}
The message channel bean is instantiated elsewhere. The publisher runs as expected and an event is publishing to the channel, however the service activator is never invoked. What am I missing here?
Turns out you need to move the service activator to a separate test component class (#TestComponent prevents this from being injected outside the test context).
#TestComponent
public class TestListener {
public final List<Object> results = new ArrayList<>();
#ServiceActivator(inputChannel = "messageChannel")
public void listener(Event event) {
Object id = event.getHeaders().get("myId");
results.add(id);
}
}
Then you can bring this listener into your test. Make sure you use #Import to bring your service activator class into the test context.
#SpringBootTest
#Import(TestListener.class)
class PublisherTest {
#Autowired
private Publisher publisher;
#Autowired
private TestListener testListener;
#Test
void testPublish() {
this.publisher.publish(new Event().addHeader("myId", 1));
Awaitility.await()
.atMost(2, TimeUnit.SECONDS)
.until(() -> !this.testListeners.results.isEmpty());
}
}
The test passes after making these changes. Figured this out with a demo app and applied it to a production testing issue.
I am having an issue when trying to integration test my JMS listener with Mockito and MockRestServiceServer. Even if I'm using the correct Mockito.when annotations, they are coming up as null, and the MockRestServiceServer is acting as if it isn't being called. If I switch instead to test against the myService component that the jms listener calls, the mocks and the MockRestServiceServer calls are working as expected, which is puzzling. I am connecting to an embedded ActiveMQ broker for the test and I am using Spring Boot 2.2.8.RELEASE and JDK 8.x if that helps.
Here is the JMS Listener Class
#Component
public class MyJmsListener {
#Autowired
private MyService myService;
#JmsListener(
destination = "${jms.queue}",
containerFactory = "myJmsListenerContainerFactory"
)
public void receive(Message<String> message) {
myService.process(message);
}
}
Here is the JMS Listener Test Class
#RunWith(SpringRunner.class)
#SpringBootTest
#ActiveProfiles("test")
public class JmsListenerTest {
...
#MockBean
private AuthorizationService authorizationService;
...
#Autowired
private MockRestServiceServer mockRestServiceServer;
#Autowired
private JmsTemplate listenerTestJmsTemplate;
#Value("${jms.queue}")
private String testDestination;
...
#Test
public void testListener() throws IOException, URISyntaxException, InterruptedException {
//ARRANGE
String payloadPath = "classpath:payloads/listenerPayload.json";
String payload = new String(Files.readAllBytes(ResourceUtils.getFile(payloadPath).toPath()));
String testAuth = "auth";
Mockito.when(authorizationService.generateTicket(Mockito.any(Headers.class), Mockito.eq("9130353887051456")))
.thenReturn(testAuth);
String extPayloadPath = "classpath:payloads/revokeCancelAutoRenewRequestApi.json";
String extPayload = new String(Files.readAllBytes(ResourceUtils.getFile(extPayloadPath).toPath()));
mockRestServiceServer.expect(ExpectedCount.once(), MockRestRequestMatchers.requestTo(new URI("/test/v3/subscriptions/400367048/something")))
.andExpect(MockRestRequestMatchers.content().string(extPayload))
.andExpect(MockRestRequestMatchers.header(HttpHeaders.AUTHORIZATION, testAuth))
.andRespond(MockRestResponseCreators.withStatus(HttpStatus.OK));
//ACT
listenerTestJmsTemplate.convertAndSend(testDestination, payload);
//ASSERT
mockRestServiceServer.verify();
Assert.assertTrue(JmsListenerWrapperConfiguration.latch.await(5, TimeUnit.SECONDS));
}
...
}
I have a JmsListenerWrapperConfiguration that will allow me to wrap the countdown latch into the jms listener.
#Configuration
#Profile("test")
public class JmsListenerWrapperConfiguration {
public static final CountDownLatch latch = new CountDownLatch(1);
#Bean
public JmsTemplate listenerTestjmsTemplate(ActiveMQConnectionFactory activeMQConnectionFactory){
JmsTemplate jmsTemplate = new JmsTemplate(activeMQConnectionFactory);
return jmsTemplate;
}
/**
* Wrap the JMS Listeners with a count down latch that will allow us to unit test them.
* #return The bean post processor that will wrap the JMS Listener.
*/
#Bean
public static BeanPostProcessor listenerWrapper() {
return new BeanPostProcessor() {
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof MyJmsListener) {
MethodInterceptor interceptor = new MethodInterceptor() {
#Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = invocation.proceed();
if (invocation.getMethod().getName().equals("listen")) {
latch.countDown();
}
return result;
}
};
if (AopUtils.isAopProxy(bean)) {
((Advised) bean).addAdvice(interceptor);
return bean;
}
else {
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvice(interceptor);
return proxyFactory.getProxy();
}
}
else {
return bean;
}
}
};
}
}
The MockRestServiceServer configuration is defined here.
#Configuration
#Profile("test")
public class MockRestServiceServerConfiguration {
#Bean
public MockRestServiceServer mockRestServiceServer(RestTemplate restTemplate) {
MockRestServiceServerBuilder builder = MockRestServiceServer.bindTo(restTemplate);
MockRestServiceServer server = builder.bufferContent().build();
return server;
}
}
The error that I see is as follows.
java.lang.AssertionError: Further request(s) expected leaving 1 unsatisfied expectation(s).
0 request(s) executed.
at org.springframework.test.web.client.AbstractRequestExpectationManager.verify(AbstractRequestExpectationManager.java:159)
at org.springframework.test.web.client.MockRestServiceServer.verify(MockRestServiceServer.java:116)
Update
I've been debugging and of course the test is running on thread[main], whereas the JMS listener is running on thread[DefaultMessageListenerContainer-1], so my question then becomes, what should we do with Mockito mocking when the mocks/verifications need to be used by separate threads?
It turns out that the MockRestServiceServer needed to verify after the latch is awaiting as shown in this code below.
Assert.assertTrue(JmsListenerWrapperConfiguration.latch.await(5, TimeUnit.SECONDS));
mockRestServiceServer.verify();
I'm trying to #MockBean a #Repository annotated class:
#Repository
public interface ApplicationDao extends MongoRepository<Application, String> {}
I'm injecting it into a #Service annotated class:
#Service
public class AuthorizationService {
private ApplicationDao appsDao;
private List<Application> allowedApplications;
#Autowired
public AuthorizationService(ApplicationDao appsDao) {
this.appsDao = appsDao; //<<MOCKED INJECTED BEAN>>
this.fillApplications();
}
private void fillApplications() {
this.appsDao.findAll() //<<MOCKED method>>
.forEach(entry -> {
this.allowedApplications.put(entry.getName(), entry);
});
}
public bool isAuthorized(Application application) {
return this.allowedApplications
.stream()
.anyMatch(app -> app.getId().equals(application.getId()));
}
}
My test mocking configuration looks like:
#RunWith(SpringRunner.class)
#SpringBootTest()
public class GroupReferencesTest {
private #Autowired AuthorizationService;
private #MockBean ApplicationDao applicationDao;
#Before
public void setUp() {
Application testApplication = new Application();
testApplication.setName("test-application");
List<Application> allowedApplications = new ArrayList<Application>();
allowedApplications.add(testApplication);
Mockito
.when(this.applicationDao.findAll())
.thenReturn(allowedApplications);
}
#Test
public void test() {
Application app = new Application();
app.getId("test-application");
assertTrue(this.authorizationService.isAuthorized(app)); //<<FAILS>>
}
}
Nevertheless, my mocked object is not injected. I mean, when my AuthorizationService calls its injected ApplicationDao is returns an empty list instead of my mocked list.
I've tried to use #MockBean(name="applicationDao") as well. The behavior is the same.
I've also tried to configure my mocked bean using this code:
#TestConfiguration
public class RestTemplateTestConfiguration {
#Bean("applicationDao")
#Primary
public static ApplicationDao mockApplicationDao() {
ApplicationDao mock = Mockito.mock(ApplicationDao.class);
Application testApplication = new Application();
testApplication.setName("test-application");
List<Application> allowedApplications = new ArrayList<Application>();
allowedApplications.add(testApplication);
Mockito
.when(mock.findAll())
.thenReturn(allowedApplications);
return mock;
}
}
However, it doesn't works right.
Application class is:
public class Application {
private String id;
//setters & getters
}
Any ideas?
First things first - the type of test. Answer: Unit test.
You are starting Spring context that manages a lifecycle of AuthorizationService and then you are trying to inject mock. What really happens is that Spring IoC container is injecting a real ApplicationDao (the one managed by Spring IoC container) into the AuthorizationService.
Solution:
Manage lifecyle of AuthorizationService by your test runner (like MockitoJUnitRunner and inject ApplicationDao mock into it):
#RunWith(MockitoJUnitRunner.class)
public class GroupReferencesTest {
private #InjectMocks AuthorizationService authorizationService;
private #Mock ApplicationDao applicationDao;
#Before
public void setUp() {
Application testApplication = new Application();
testApplication.setName("test-application");
List<Application> allowedApplications = new ArrayList<Application>();
allowedApplications.add(testApplication);
Mockito
.when(this.applicationDao.findAll())
.thenReturn(allowedApplications);
}
#Test
public void test() {
Application app = new Application();
app.getId("test-application");
assertTrue(this.authorizationService.isAuthorized(app));
}
}
Working example
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {AuthorizationService.class})
public class GroupReferencesTest {
#Autowired
private AuthorizationService;
#MockBean
private ApplicationDao applicationDao;
#Test
public void test() {
//given
Mockito.when(applicationDao.findAll()).thenReturn(emptyList());
//when & then
assertTrue(authorizationService.isAuthorized(app));
}
}
I have spring component class annotated with #Component and in it I have field ConcurrentHashMap map, which is init in constructor of component and used in spring stream listener:
#Component
public class FooService {
private ConcurrentHashMap<Long, String> fooMap;
public FooService () {
fooMap = new ConcurrentHashMap<>();
}
#StreamListener(value = Sink.INPUT)
private void handler(Foo foo) {
fooMap.put(foo.id, foo.body);
}
}
Listener handle messages sent by rest controller. Can you tell me why I always got there fooMap.put(...) NullPointerException because fooMap is null and not initialzied.
EDIT:
After #OlegZhurakousky answer I find out problem is with async method. When I add #Async on some method and add #EnableAsync I can't anymore use private modificator for my #StreamListener method. Do you have idea why and how to fix it?
https://github.com/schwantner92/spring-cloud-stream-issue
Thanks.
Could you try using #PostConstruct instead of constructor?
#PostConstruct
public void init(){
this.fooMap = new ConcurrentHashMap<>();
}
#Denis Stephanov
When I say bare minimum, here is what I mean. So try this as a start, you'll see that the map is not null and start evolving your app from there.
#SpringBootApplication
#EnableBinding(Processor.class)
public class DemoApplication {
private final Map<String, String> map;
public static void main(String[] args) {
SpringApplication.run(DemoRabbit174Application.class, args);
}
public DemoApplication() {
this.map = new HashMap<>();
}
#StreamListener(Processor.INPUT)
public void sink(String string) {
System.out.println(string);
}
}
With Spring everything has to be injected.
You need to declare a #Bean for the ConcurrentHashMap, that will be injected in you Component. So create a Configuration class like:
#Configuration
public class FooMapConfiguration {
#Bean("myFooMap")
public ConcurrentHashMap<Long, String> myFooMap() {
return new ConcurrentHashMap<>();
}
}
Then modify your Component:
#Component
public class FooService {
#Autowired
#Qualifier("myFooMap")
private ConcurrentHashMap<Long, String> fooMap;
public FooService () {
}
#StreamListener(value = Sink.INPUT)
private void handler(Foo foo) {
fooMap.put(foo.id, foo.body); // <= No more NPE here
}
}
I want to integration test a Spring Boot 1.5.4 service that uses an #EventListener. My problem is: when the test is run, the events are correctly published, but they are not consumed.
My ultimate purpose is to use a #TransactionEventListener, but for simplicity I start with an #EventListener.
Here is my service class:
#Service
public class MyService {
private static final Logger logger = // ...
private final ApplicationEventPublisher eventPublisher;
#Autowired
public MyService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
#Transactional
public void publish() {
logger.warn("Publishing!");
eventPublisher.publishEvent(new MyEvent());
}
// #TransactionalEventListener
#EventListener
public void consume(MyEvent event) {
logger.warn("Consuming!"); // this is never executed in the test
}
public static class MyEvent {
}
}
Here is my JUnit test class:
#RunWith(SpringRunner.class)
#SpringBootTest
#SpringBootConfiguration
public class MyServiceIT {
#Autowired
MyService myService;
#Test
public void doSomething() {
myService.publish();
}
static class TestConfig {
#Bean
public MyService myService() {
return new MyService(eventPublisher());
}
#Bean
public ApplicationEventPublisher eventPublisher() {
ApplicationEventPublisher ctx = new GenericApplicationContext();
((AbstractApplicationContext)ctx).refresh();
return ctx;
}
}
}
Note: the call to refresh() prevents an IllegalStateException with message "ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context" from occurring.
Does anyone have a clue? Many thanks in advance.
For the record, the solution was: keep the event consumer method in another bean than the event producer method. That is, extract method consume(MyEvent) from class MyService into a new #Service class MyConsumer.