Test Kafka consumer using EmbddedKafka fail when launching a batch of tests - spring-boot

I'm testing my Kafka Consumer in Spring Boot. My consumer are similar to the following
#Slf4j
#Component
#RequiredArgsConstructor
public class KafkaPaymentConsumer {
private final PaymentInterface paymentInterface;
#KafkaListener(topics = "#{'${kafka.topic.payment}'}",
groupId = "#{'${kafka.group-id}'}")
public void consumePaymentEvents(PaymentEvent paymentEvent) {
paymentInterface.handleReceiptPaymentReceivedEvent(paymentEvent);
}
}
And My test cases are similar to the following
#SpringBootTest
#EmbeddedKafka(brokerProperties = {"listeners=PLAINTEXT://localhost:9092"},
partitions = 1,
controlledShutdown = true)
class KafkaPaymentConsumerTest {
#Autowired
KafkaTemplate<String, PaymentEvent> kafkaTemplate;
#Autowired
private ObjectMapper objectMapper;
#Value("${kafka.topic.payment}")
private String paymentTopic;
#SpyBean
private KafkaPaymentConsumer kafkaPaymentConsumer;
#SpyBean
private PaymentInterface paymentInterface;
#Captor
ArgumentCaptor<PaymentEvent> paymentEventCaptor;
private static File PAYMENT_EVENT_JSON = Paths.get("src", "test", "resources", "files",
"Payment.json").toFile();
#Test
#SneakyThrows
#DirtiesContext
void consumePaymentEvents() {
PaymentEvent event = objectMapper.readValue(PAYMENT_EVENT_JSON,
PaymentEvent.class);
kafkaTemplate.send(paymentTopic, "1", event);
verify(kafkaPaymentConsumer, timeout(10000).times(1)).consumePaymentEvents(
paymentEventCaptor.capture());
PaymentEvent argument = paymentEventCaptor.getValue();
verify(paymentInterface, timeout(10000).times(1)).handleReceiptPaymentReceivedEvent(any());
}
}
the test works well, BUT when running a batch of tests at once, some tests fail ! ( only when I run many tests at the same time !! ) it seems that there is an issue in the context with #EmbeddedKafka
I got like theses log errors
Actually, there were zero interactions with this mock.
or a Timeout when trying to poll records from the broker
Any explanation or suggestion please

Since you don’t use a #DirtiesContext on your test class to close an application context in the end, it is not a surprise that other tests for the same topic can steal data from you. See if you can clean up contexts as I explained, or consider to use different topics in different tests. I’d prefer the dirties context since it guarantees that no any extra resources in the memory to cause race conditions and surprises .

Related

Spring Boot / Junit5+Mockito: Mock's mockitoInterceptor gets replaced during test?

I have two service classes (there are more, of course, but those two are relevant here), which are in use during an integration test.
For test, I set up a mock (ConfigurationService) and stub two methods:
#ExtendWith(SpringExtension.class)
#ActiveProfiles("test")
#SpringBootTest
#TestPropertySource(properties =
"spring.main.allow-bean-definition-overriding=true")
public class VehicleAuditExecutionServiceIT {
#MockBean
private ConfigurationService configurationServiceMock;
#Autowired
private VehicleAuditExecutionService vehicleAuditExecutionService;
#Test
void testExecuteVehicleAudits() throws IOException {
// quite some DB operations for the test setup here
AuditDurationConfigDTO auditDurationConfigDTO = new AuditDurationConfigDTO();
auditDurationConfigDTO.setMaxDuration(deMaxDuration);
Map<String, ScheduledAuditConfigDTO> map = new HashMap<>();
map.put(country, ScheduledAuditConfigDTO.builder()
.groupCalculationEnabled(false)
.build());
when(configurationServiceMock.getAuditDurationConfig(country)).thenReturn(auditDurationConfigDTO);
when(configurationServiceMock.getScheduledAuditConfigurationForCountry(country)).thenReturn(ScheduledAuditConfigDTO.builder()
.groupCalculationEnabled(false)
.sameAuditDurationAllDealersSameGroupId(false)
.build());
vehicleAuditExecutionService.executeVehicleAudits(startDate, country);
verify(publishAuditInterfaceMock).pushExecutions(country, dealerDe1ExportAuditDtoList, auditCategory);
}
#ComponentScan(basePackages = "com.application")
#SpringBootApplication
#PropertySource("classpath:application-test.yml")
static class TestConfiguration {}
}
After the setup, the stubbings are available:
During the test's execution, vehicleAuditExecutionService.executeVehicleAudits(startDate, country) calls the AuditPreparationService, which in turn uses the configurationServiceMock (using #Autowired constructor injection). As expected, the calls gets matched and result set up is returned.
Later, the execution returns to vehicleAuditExecutionService.executeVehicleAudits(startDate, country), where it calls the configurationServiceMock (#Autowired constructor injection, as well) again. But here, the mock's configuration has been changed: the mock's attribute mockitoInterceptor gets replaced by some other instance.
Result: the stubbing is gone and the call returns null - leading to a NPE.
The screenshots were taken using org.springframework.boot:spring-boot-starter-parent:2.7.6, but I've tried that with multiple Spring Boot versions:
2.7.0
2.6.14
2.5.14
2.4.13
2.3.12.RELEASE
Each version has this issue - but I've never seen it in any other test. So I guess, there's something wrong with my test setup - but I cannot spot it.
Any idea, why this is happening?
Thanks a lot - please do not hesitate to ask for any further information, if needed for analysis.
kniffte

SonarQube doesn't recognize Mapper's unit test

Running SonarQube Version 6.7.2 (build 37468) - LGPL v3, Junit 4 and mockito in our project, I noted that SonarQube does not recognize Mapper's unit test and decrease the project's percentual. In my local enviroment, the test works well and the coverage is 100% by Eclipse.
Below is the UT code:
#RunWith(MockitoJUnitRunner.class)
public class ClassMapperTest {
#Mock
private ClassMapper mapper;
#Mock
private ClassDTO dto;
#Before
public void setUp() {
mapper = Mockito.mock(ClassMapper.class);
dto = Mockito.mock(ClassDTO.class);
}
#Test
public void returnResource() {
Mockito.when(mapper.fromMethod(Mockito.anySet())).thenReturn(new HashSet<>());
mapper.fromMethod(new HashSet<ClassDTO>());
}
The statistics:
After Commit:
Does anyone have any idea?
Sonarqube is right with the computation. You do have a major issue within your test, the code you seem to be testing is mocked aka you are not testing the actual code, but a fake of it.
When you mock a class you create a dummy fake version of this class, which does not have any implementation (mapper = Mockito.mock(ClassMapper.class);).
you then tell your mock to return a value when a method is called Mockito.when(mapper.fromMethod(Mockito.anySet())).thenReturn(new HashSet<>());. This way you are actually not testing your fromMethod, you just testing a method, which you told in your test what to return.
A proper test would look something like this:
#RunWith(MockitoJUnitRunner.class)
public class ClassMapperTest {
private ClassMapper mapper;
#Mock
private ClassDTO dto;
#Before
public void setUp() {
mapper = new ClassMapper();
// no need to do that, MockitoJUnitRunner is doing this for you
// dto = Mockito.mock(ClassDTO.class);
}
#Test
public void returnResource() {
// calling the actual method
assertTrue(mapper.fromMethod(new HashSet<ClassDTO>()) != null);
}
}
There is also no need for the dto as it is not used within your test at all, but I left it in there, to mark the unnecessary mock instantiation, which is done by the MockitoJUnitRunner.
// Disclaimer: I am not guaranteeing that your tests will pass, with my suggestion, I only want to highlight the problem with the test.

Spring Boot: Completely Ignore All Datasources During Unit Tests

I have all kinds of unit tests that mock the repositories they would otherwise need to connect to, yet Spring Boot insists on trying to connect to them.
I've scoured this place, and have seen the suggestions to exclude all the different autoconfiguration classes from each and every one of the test classes I have (or do it globally in /src/test/resources/application.properties) but that doesn't stop Spring Boot from trying to get some kind of data sources going. It seems to just starve it for the stuff it needs to get those datasources going. There doesn't seem to be anything I can do to tell Spring Boot "Do absolutely nothing involving trying to connect to datasources at any time".
I want to be able to do this so I can write and run unit tests on code when I'm not connected to a network. I've considered running H2 and that Flapdoodle whatever, but I've heard that Flapdoodle has problems with later builds of Spring Boot these days, and the H2 module hasn't been updated since 2019.
Can anyone point me in the right direction? Here's a sample test, complete with the exclusions:
#RunWith(MockitoJUnitRunner.class)
#SpringBootTest
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
public class SweepsServiceTest {
#InjectMocks
private SweepsService sweepsService;
#Mock
private PrizeEntryRepository mockPrizeEntryRepository;
#Mock
private PrizeRepositoryPrimary mockPrizeRepositoryPrimary;
#Mock
private PrizeRepositorySecondary mockPrizeRepositorySecondary;
#Mock
private PointService mockPointService;
private final String prizeId = "prize1";
private final String awardId = "award1";
private final int userId = 1234;
private final int costToEnter = 25;
private final int maxEntriesPerUser = 1;
private final int numberToPurchase = 1;
#Test
public void givenPrizeNotFound_whenProcessSweepsEntry_thenThrowException() {
User user = new User();
user.setId(userId);
Mockito.when(mockPrizeRepositoryPrimary.findById(prizeId)).thenReturn(Optional.empty());
assertThatIllegalArgumentException().isThrownBy(
() -> sweepsService.processSweepsEntry(prizeId, "RAFFLE", null, null, user, numberToPurchase)
).withMessage("User " + user.getId() + " tried to enter sweepstakes id " + prizeId + " but that ID was not found.");
}
}
When you don't need to build a spring application context (in case of unit tests) just refrain from using the #SpringBootTest annotation and use JUnit and Mockito exclusively. Otherwise, in case of integration tests, for example, I usually just define which configuration classes I want to include explicitly:
#SpringBootTest(classes = {SomeConfigurationClass.class, AnotherConfigurationClass.class})
class SomeIntegrationTest { ... }
See the JavaDoc for SpringbootTest#classes:
The component classes to use for loading an ApplicationContext. Can also be specified using #ContextConfiguration(classes=...). If no explicit classes are defined the test will look for nested #Configuration classes, before falling back to a #SpringBootConfiguration search.

Сamunda replace behaviour for external tasks in tests

I created simple Camunda spring boot project and also created simple BPMN process with switcher. (5.5 KB)
I used service task with external implementation as a spring beans. I want to write tests for process but I don't want to test how beans works. Because in general I use external implementation for connection to DB and save parameter to context or REST call to internal apps. For example I want skip execute service task(one) and instead set variables for switcher. I tried to use camunda-bpm-assert-scenario for test process and wrote simple test WorkflowTest.
I noticed if I use #MockBean for One.class then Camunda skip delegate execution. If use #Mock then Camunda execute delegate execution.
PS Sorry for bad english
One
#Service
public class One implements JavaDelegate {
private final Random random = new Random();
#Override
public void execute(DelegateExecution execution) throws Exception {
System.out.println("Hello, One!");
execution.setVariable("check", isValue());
}
public boolean isValue() {
return random.nextBoolean();
}
}
WorkflowTest
#SpringBootTest
#RunWith(SpringRunner.class)
#Deployment(resources = "process.bpmn")
public class WorkflowTest extends AbstractProcessEngineRuleTest {
#Mock
private ProcessScenario insuranceApplication;
#MockBean
private One one;
#Before
public void init() {
MockitoAnnotations.initMocks(this);
Mocks.register("one", one);
}
#Test
public void shouldExecuteHappyPath() throws Exception {
// given
when(insuranceApplication.waitsAtServiceTask("Task_generator")).thenReturn(externalTaskDelegate -> {
externalTaskDelegate.complete(withVariables("check", true));
}
);
String processDefinitionKey = "camunda-test-process";
Scenario scenario = Scenario.run(insuranceApplication)
.startByKey(processDefinitionKey) // either just start process by key ...
.execute();
verify(insuranceApplication).hasFinished("end_true");
verify(insuranceApplication, never()).hasStarted("three");
verify(insuranceApplication, atLeastOnce()).hasStarted("two");
assertThat(scenario.instance(insuranceApplication)).variables().containsEntry("check", true);
}
}
I found two solutions:
It's a little hack. If user #MockBean for delegate in a test. The delegate will be skipped but you have trouble with process engine variables.
Create two beans with one qualifier and use profiles for testing and production. I used to default profile for local start and test profile for testing.

What is the best way to test that a spring application context fails to start?

I use the spring-boot-starter-web and spring-boot-starter-test.
Let's say I have a class for binding configuration properties:
#ConfigurationProperties(prefix = "dummy")
public class DummyProperties {
#URL
private String url;
// getter, setter ...
}
Now I want to test that my bean validation is correct. The context should fail to start (with a specfic error message) if the property dummy.value is not set or if it contains an invalid URL. The context should start if the property contains a valid URL. (The test would show that #NotNull is missing.)
A test class would look like this:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = MyApplication.class)
#IntegrationTest({ "dummy.url=123:456" })
public class InvalidUrlTest {
// my test code
}
This test would fail because the provided property is invalid. What would be the best way to tell Spring/JUnit: "yep, this error is expected". In plain JUnit tests I would use the ExpectedException.
The best way to test Spring application context is to use ApplicationContextRunner
It is described in Spring Boot Reference Documentation:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-test-autoconfig
And there is a quick guide about it:
https://www.baeldung.com/spring-boot-context-runner
Sample usage
private static final String POSITIVE_CASE_CONFIG_FILE =
"classpath:some/path/positive-case-config.yml";
private static final String NEGATIVE_CASE_CONFIG_FILE =
"classpath:some/path/negative-case-config.yml";
#Test
void positiveTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + POSITIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
Assertions.assertThat(context).hasNotFailed();//6
});
}
#Test
void negativeTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + NEGATIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
assertThat(context)
.hasFailed();
assertThat(context.getStartupFailure())
.isNotNull();
assertThat(context.getStartupFailure().getMessage())
.contains("Some exception message");
assertThat(extractFailureCauseMessages(context))
.contains("Cause exception message");
});
}
private List<String> extractFailureCauseMessages(AssertableApplicationContext context) {
var failureCauseMessages = new ArrayList<String>();
var currentCause = context.getStartupFailure().getCause();
while (!Objects.isNull(currentCause)) {//7
failureCauseMessages.add(currentCause.getMessage());
currentCause = currentCause.getCause();
}
return failureCauseMessages;
}
Explanation with examples of similar definitions from Junit5 Spring Boot Test Annotations:
Triggers loading of config files like application.properties or application.yml
Logs ConditionEvaluationReport using given log level when application context fails
Provides class that specifies mock beans, ie. we have #Autowired BookRepository in our BookService and we provide mock BookRepository in MockBeansTestConfiguration. Similar to #Import({MockBeansTestConfiguration.class}) in test class and #TestConfiguration in class with mock beans in normal Junit5 Spring Boot Test
Equivalent of #TestPropertySource(properties = { "spring.config.location=" + POSITIVE_CASE_CONFIG_FILE})
Triggers spring auto configuration for given class, not direct equivalent, but it is similar to using #ContextConfiguration(classes = {BookService.class}) or #SpringBootTest(classes = {BookService.class}) together with #Import({BookService.class}) in normal test
Assertions.class from AssertJ library, there should be static import for Assertions.assertThat, but I wanted to show where this method is from
There should be static import for Objects.isNull, but I wanted to show where this method is from
MockBeansTestConfiguration class:
#TestConfiguration
public class MockBeansTestConfiguration {
private static final Book SAMPLE_BOOK = Book.of(1L, "Stanisław Lem", "Solaris", "978-3-16-148410-0");
#Bean
public BookRepository mockBookRepository() {
var bookRepository = Mockito.mock(BookRepository.class);//1
Mockito.when(bookRepository.findByIsbn(SAMPLE_BOOK.getIsbn()))//2
.thenReturn(SAMPLE_BOOK);
return bookRepository;
}
}
Remarks:
1,2. There should be static import, but I wanted to show where this method is from
Why is that an integration test to begin with? Why are you starting a full blown Spring Boot app for that?
This looks like unit testing to me. That being said, you have several options:
Don't add #IntegrationTest and Spring Boot will not start a web server to begin with (use #PropertySource to pass value to your test but it feels wrong to pass an invalid value to your whole test class)
You can use spring.main.web-environment=false to disable the web server (but that's silly given the point above)
Write a unit test that process that DummyProperties of yours. You don't even need to start a Spring Boot application for that. Look at our own test suite
I'd definitely go with the last one. Maybe you have a good reason to have an integration test for that?
I think the easiest way is:
public class InvalidUrlTest {
#Rule
public DisableOnDebug testTimeout = new DisableOnDebug(new Timeout(5, TimeUnit.SECONDS));
#Rule
public ExpectedException expected = ExpectedException.none();
#Test
public void shouldFailOnStartIfUrlInvalid() {
// configure ExpectedException
expected.expect(...
MyApplication.main("--dummy.url=123:456");
}
// other cases
}

Resources