5

i am trying to make @SpyBean or @MockBean work in this test as they are working in all my other tests. the only difference here is this test uses an active profile because it mocks some AWS libs

I shouldnt have to instantiate the class it if I am using SpyBean, i've seen countless examples similar to mine below that seem to work fine.

The unit I'm testing:

@Component
public class SqsAdapter {

    @Autowired
    QueueMessagingTemplate queueMessagingTemplate;

    @Autowired
    MyProcessor processor;

    @SqsListener("${queue.name}")
    public void onEvent(String message) {
        if (!StringUtils.isEmpty(message)) {
            processor.process(message);
        } else { 
            log.error("Empty or null message received from queue");
        }
    }
}

and a simple unit test to expect process(...) to be called:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@ActiveProfiles("local")
public class SqsAdapterTest {

    @Autowired
    AmazonSQSAsync amazonSQS;

    @SpyBean
    MyProcessor processor;

    @InjectMocks
    SqsAdapter adapter;

    @Test
    public void testOnEvent() {
        doNothing().when(processor).process(any()); // <---- process() is void
        String event = "royale with cheese";
        adapter.onEvent(event);
        verify(processor, times(1)).process(event);
    }
}

when i debug in the unit test, i get a java.lang.NullPointerException on processor as if it never gets initialized:

ive also tried switching it to @MockBean with the same results

furthermore, i also tried stripping the testing of the @SpringBootTest and the @ActiveProfiles ... and removed the AmazonSQSAsync dependency since im not mocking the Listener itself, and it still throws the same NPE

what am i missing here?

heug
  • 982
  • 2
  • 11
  • 23

2 Answers2

4

Think I've got it answered: seems to be because of mixing testing frameworks via having the @InjectMocks annotation mixed with @SpyBean ... since I was trying not to use Mockito mocks, and this is a Mockito annotation, i think it was screwing up the tests

the working unit test looks like:

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("local")
public class SqsAdapterTest {

    @Autowired
    AmazonSQSAsync amazonSQS;

    @SpyBean
    SeamEventProcessor seamEventProcessor;

    @Autowired
    SqsAdapter adapter;

    @Test
    public void testOnEvent() {
        doNothing().when(seamEventProcessor).process(any());
        String event = "royale with cheese";
        adapter.onEvent(event);
        verify(seamEventProcessor, times(1)).process(event);
    }
}
heug
  • 982
  • 2
  • 11
  • 23
  • If you read the SpyBean javadoc, it seems excatly written for such a Mockito usecase, so i think the real problem was on a different location – Daniel Alder Sep 27 '22 at 14:39
0

I guess Spring Boot Test doesn't load the MyProcessor bean in this test configuration.

Usually if the bean is loaded, then @SpyBean annotation can instruct spring to wrap the original bean so that you'll get the spy. However, you have to provide spring boot test configuration in a way that it will find the original MyProcessor bean.

If you have a configuration class, put a breakpoint/log:


@Configuration
public class MyConfiguration {

    @Bean
    public MyProcessor myProcessor(...) {
       return new MyProcessor(); // <-- put breakpoint here and see whether its called at all
    }
}

Alternatively if you have something like:

@Component
public class MyProcessor {
    .... <-- put a breakpoint in constructor (create a default one if you don't have any)
}

If my assumption is correct, now you should see that this class is not called at all, and you understand why the setup doesn't work.

Now, instead of @SpringBootTest(classes = Application.class) try @SpringBootTest

This tries to mimic the spring boot application startup process with recursively scanning the configurations and everything (this is a broad topic actually you should read the documentation of spring boot test).

Instead what you did is said to spring: "I just want to work with predefined configuration class Application and that's it, don't scan anything, don't resolve configurations, don't resolve beans, I know what I'm doing".

After this step if you've followed packages conventions offered by spring boot, you should see that the original MyProcessor bean is loaded and the spy is created

Mark Bramnik
  • 39,963
  • 4
  • 57
  • 97
  • i thought you were onto something there when you reminded me about sometimes needing that default constructor with spring, but your original assumption isnt true ... the MyProcessor class is like your @Component example, and it breaks inside the constructor on debug without changing anything from my original question – heug Jan 22 '20 at 21:21
  • one thing i just realized is that it is going to the constructor *after* it gets through the onEvent(...) method. its instantiation isnt getting triggered before the test. it does this whether i include your suggested edits or not – heug Jan 22 '20 at 21:31
  • im wondering if has something to do with @InjectMocks being a Mockito annotation – heug Jan 22 '20 at 21:40
  • yeah i think this is a result of mixing frameworks ... all tests pass if i make the SqsAdapter an "@Autowired" class vs annotating it with "@InjectMocks" – heug Jan 22 '20 at 21:45