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.
Related
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
I'm trying to write a unit test, for my spring server.
First it will check if a username is present or not in repository, if not so the username is available, then it will return true, and then I will save that username to my repository and check if available ot not it should return false.
Here is my code:
#Test
public void availableTest() {
String username="some_username";
LoginCredential lc=new LoginCredential();
lc.setUsername(username);
lc.setPasswordHash("1");
lc.setSessionID(0);
assertEquals(true, loginCredentialService.available(username));
loginCredentialRepository.save(lc);
assertEquals(false, loginCredentialService.available(username));
}
But for some reason, for the last assertEquals it gives me error. So I can say, the data is not being saved in repository, as I have tested my APIs using postman.
So how this can be resolved?
I think testing class is properly annotated:
#RunWith(SpringRunner.class)
#SpringBootTest
public class MainApplicationTests {
#Autowired
private LoginCredentialService loginCredentialService;
#MockBean
private LoginCredentialRepository loginCredentialRepository;
...
You tagged the OP with H2, so I guess you know about In-Memory databases.
If you use the #SpringBootTest annotation, you are writing an integration test, so you'll test the full application as wired by Spring. For efficiency you might want to use an in-memory database for testing instead of a full SQL Server.
You can achieve this by adding H2 database as a test dependency, which will be picked up by Spring Boot for integration test repositories. Then you can inject the actual repository:
#Autowired
private LoginCredentialRepository loginCredentialRepository;
Additionaly, you can make your test #Transactional. Then every test case will run in a separate transaction, and the transaction will be rolled back after every test, so you don't need to worry about cross-test polution.
If you just want to Unit Test the LoginCredentialService, you need to stub the relevant methods on the repository, e.g.
#MockBean
private LoginCredentialRepository loginCredentialRepository;
#Test
public void availableTest1() {
when(loginCredentialRepository.existByName(username)).thenReturn(true);
assertEquals(false, loginCredentialService.available(username));
}
#Test
public void availableTest2() {
when(loginCredentialRepository.existByName(username)).thenReturn(false);
assertEquals(true, loginCredentialService.available(username));
}
You can do this as a pure Mockito test, too, without #SpringBootTest.
EDIT: As C. Weber suggested in the comments, the solution is to add #Transactional to the test class.
I have some tests that use an H2 in-memory DB. I need to reset the DB before each test. Although my SQL scripts are run each a test is executed, the DB is not properly reset, resulting in a missing needed entry after a delete test.
Test class:
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureTestDatabase(replace=Replace.ANY, connection=EmbeddedDatabaseConnection.H2)
public class RepositoryTests {
#Autowired
private Repository repository;
#Autowired
private DataSource dataSource;
#Before
public void populateDb() {
Resource initSchema = new ClassPathResource("database/schema.sql");
Resource initData = new ClassPathResource("database/data.sql");
DatabasePopulator dbPopulator = new ResourceDatabasePopulator(initSchema, initData);
DatabasePopulatorUtils.execute(dbPopulator, dataSource);
}
#Test
public void testMethod1() {
// ...
repository.delete("testdata");
}
#Test
public void testMethod2() {
// ...
Object test = repository.get("testdata");
// is null but should be an instance
}
}
schema.sql drops all tables before recreating them. data.sql inserts all needed test data into the DB.
Running the testMethod2 alone succeeds. However, running all tests makes the test fail with a NullPointerException.
I have successfully tried to use #DirtiesContext, however this is not an option because I can't afford to have a 20 second startup for each 0.1 second test.
Is there another solution?
The Spring Test Framework provides a mechanism for the behaviour you want for your tests. Simply annotate your Test class with #Transactional to get the default rollback behaviour for each test method.
There are ways to configure the transactional behaviour of tests and also some pitfalls (like using RestTemplate inside test method), which you can read more about in the corresponding chapter of the Spring manual.
Spring Test Framework
I am facing some error when I try to mock objects like Tracer and Span in unit tests if I use Dalston.SR3 or Dalston.Release versions, but this problem doesn't happen if I use Camden.SR6 or Camden.SR7 versions.
Find an example code here
Microservice msvc-a is using Dalston version and has two test classes, where only is failing the class where I am trying to mock the Tracer and the Span objects.
Microservice msvc-b is using Camden version and has the same test classes.
At the same time, I can't understand this situation when I am in debug mode in STS and why I can't see any error trace or something like that... only a NullpointerException.
public class AbstractSpanAccessorTest {
#MockBean
private Tracer tracer;
#MockBean
private Span span;
private Random random = new Random();
#Before
public void mockSpan() {
long id = createId();
Span spanMock = Span.builder().name("mock").traceId(id).spanId(id).build();
doReturn(spanMock.traceIdString()).when(span).traceIdString();
doReturn(span).when(tracer).getCurrentSpan();
doReturn(span).when(tracer).createSpan(anyString());
}
private long createId() {
return random.nextLong();
}
}
It was my mistake. The correct way to mock the Span is:
#Before
public void mockSpan() {
long id = createId();
span = Span.builder().name("mock").traceId(id).spanId(id).build();
doReturn(span).when(tracer).getCurrentSpan();
doReturn(span).when(tracer).createSpan(anyString());
}
Tracer wouldn't mock at all with Finchley.SR2 so I ended up with this:
Tracing tracing = Tracing.newBuilder().build();
Tracer tracer = tracing.tracer();
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
}