When performing unit testing using Spock, JpaRepository is not being injected and is returning as null - spring-boot

During my unit tests using Spock and Testcontainers, the JpaRepository is not functioning properly and is not being wired correctly. This issue persists even in non-integration tests.
As suggested in another discussion, I attempted to resolve the issue by adding the spock-spring dependency to my pom.xml file. It didn't work.
No matter the scenario, the repository consistently returns as null in all instances.
An example:
#Testcontainers
class PostgresTestContainer extends Specification {
#Autowired
private PersonRepository personRepository
#Shared
PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:12-alpine")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test")
def "waits until postgres accepts jdbc connections"() {
when: "querying the database"
def response = personRepository.findAll()
then: "result is returned"
response == 0
}
}

The database is being initialized using PostgresContainer and Testcontainers annotation. However your test infrastructure doesn't know about the database. If using spring boot, then few things are missed
Add SpringBootTest or DataJpaTest annotation on top of the class. This way the spring application context is created with the right classes and PersonRepository will be injected
Switch to
static postgresContainer = new PostgreSQLContainer("postgres:12-alpine")
#Shared
PostgreSQLContainer <?> cassandra = cassandraContainer
In order to make use of the database provided by Testcontainers, add
#DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgresContainer::getUsername);
registry.add("spring.datasource.password", postgresContainer::getPassword);
}

I would suggest to separate the container setup like the following:
PostgresEnvrionement
#Testcontainers
public class PostgresEnvironment {
#Container
public static PostgreSQLContainer postgreSQLContainer = PostgresTestContainer.getInstance();
}
PostgresTestContainer
public class PostgresTestContainer extends PostgreSQLContainer<PostgresTestContainer> {
public static final String IMAGE_VERSION = "postgres:13.5";
public static final String DATABASE_NAME = "test";
private static PostgresTestContainer container;
private PostgresTestContainer() {
super(IMAGE_VERSION);
}
public static PostgresTestContainer getInstance() {
if (container == null) {
container = new PostgresTestContainer().withDatabaseName(DATABASE_NAME);
}
return container;
}
#Override
public void start() {
super.start();
System.setProperty("DB_URL", container.getJdbcUrl());
System.setProperty("DB_USERNAME", container.getUsername());
System.setProperty("DB_PASSWORD", container.getPassword());
}
#Override
public void stop() {
}
}
In your test file extend the PostgresEnvironment
#ActiveProfiles("test")
#SpringBootTest(classes = MainSpringBootApp.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AssembleEventRepositoryIntegrationTest extends PostgresEnvrionement{
// autowire jpa
// write tests
}
in your application-test.yml file in the resources section under the test directory
spring:
datasource:
password: ${DB_USERNAME}
username: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
url: ${DB_URL}
Also make sure that your main application.yml file that is being used once launching your application (not in for running your tests) matches the same syntax as your test profile.

Related

Spring boot test doesn't Autowire all dependencies

I have a bit of a confusing error in my test scenario.
We want to refactor an Application that is not tested at all. To ensure that we have the same outcame after refactoring I'll write some integration tests for one Controller class.
#RestController
#RequestMapping("/rfq")
public class RfqController {
#Autowired
private RfqRepository rfqRepo;
#Autowired
private RfqDao rfqDao;
...
#PostMapping("/get")
public #ResponseBody BuyerRfqView getRFQ(#RequestBody SingleIdBody body) {
int id = body.getId();
Optional<Rfq> rfq = rfqRepo.getById(id);
...
}
}
In that case I want to test with testcontainers and spring-boot-test everything worked well, containers are up and running and the application starts so far. But the problem is that at runtime the spring-boot-test doesn't Autowire rfqRepo in the class under test. In the Testclass, every single dependency is in the ComponentScan or EntityScan and the repositories are also injected. I have no clue why this is not working. when the test is running I get a Nullpointer Exception by rfqRepo ...
here is the Test class:
#SpringBootTest(classes = RfqController.class, webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
#ComponentScan({...})
#EnableJpaRepositories({...})
#EntityScan({...})
#EnableAutoConfiguration
#ActiveProfiles("local")
#Testcontainers
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class RfqControllerTest {
#Container
private static OracleContainer database = new OracleContainer(
"oracleinanutshell/oracle-xe-11g:latest")
.withExposedPorts(1521, 5500)
.withPassword("...");
#InjectMocks
RfqController rfqController;
#DynamicPropertySource
static void databaseProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", database::getJdbcUrl);
registry.add("spring.datasource.username", database::getUsername);
registry.add("spring.datasource.password", database::getPassword);
}
#BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
ScriptUtils.runInitScript(new JdbcDatabaseDelegate(database, ""), "ddl.sql");
}
#Test
void testGetRFQ() {
BuyerRfqView result = rfqController.getRFQ(new SingleIdBody(176501));
Assertions.assertEquals(new BuyerRfqView(), result);
}
}
In the SpringBootTest annotation you are only using RfqController. That's the only class then that is available during test.
#SpringBootTest(classes = RfqController.class, webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)
So you have to add all classes that are needed for your tests.

Passing an external property to JUnit's extension class

My Spring Boot project uses JUnit 5. I'd like to setup an integration test which requires a local SMTP server to be started, so I implemented a custom extension:
public class SmtpServerExtension implements BeforeAllCallback, AfterAllCallback {
private GreenMail smtpServer;
private final int port;
public SmtpServerExtension(int port) {
this.port = port;
}
#Override
public void beforeAll(ExtensionContext extensionContext) {
smtpServer = new GreenMail(new ServerSetup(port, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
}
#Override
public void afterAll(ExtensionContext extensionContext) {
smtpServer.stop();
}
}
Because I need to configure the server's port I register the extension in the test class like this:
#SpringBootTest
#AutoConfigureMockMvc
#ExtendWith(SpringExtension.class)
#ActiveProfiles("test")
public class EmailControllerIT {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Value("${spring.mail.port}")
private int smtpPort;
#RegisterExtension
// How can I use the smtpPort annotated with #Value?
static SmtpServerExtension smtpServerExtension = new SmtpServerExtension(2525);
private static final String RESOURCE_PATH = "/mail";
#Test
public void whenValidInput_thenReturns200() throws Exception {
mockMvc.perform(post(RESOURCE_PATH)
.contentType(APPLICATION_JSON)
.content("some content")
).andExpect(status().isOk());
}
}
While this is basically working: How can I use the smtpPort annotated with #Value (which is read from the test profile)?
Update 1
Following your proposal I created a custom TestExecutionListener.
public class CustomTestExecutionListener implements TestExecutionListener {
#Value("${spring.mail.port}")
private int smtpPort;
private GreenMail smtpServer;
#Override
public void beforeTestClass(TestContext testContext) {
smtpServer = new GreenMail(new ServerSetup(smtpPort, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
};
#Override
public void afterTestClass(TestContext testContext) {
smtpServer.stop();
}
}
The listener is registered like this:
#TestExecutionListeners(value = CustomTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
When running the test the listener gets called but smtpPort is always 0, so it seems as if the #Value annotation is not picked up.
I don't think you should work with Extensions here, or in general, any "raw-level" JUnit stuff (like lifecycle methods), because you won't be able to access the application context from them, won't be able to execute any custom logic on beans and so forth.
Instead, take a look at Spring's test execution listeners abstraction
With this approach, GreenMail will become a bean managed by spring (probably in a special configuration that will be loaded only in tests) but since it becomes a bean it will be able to load the property values and use #Value annotation.
In the test execution listener you'll start the server before the test and stop after the test (or the whole test class if you need that - it has "hooks" for that).
One side note, make sure you mergeMode = MergeMode.MERGE_WITH_DEFAULTS as a parameter to #TestExecutionListeners annotation, otherwise some default behaviour (like autowiring in tests, dirty context if you have it, etc) won't work.
Update 1
Following Update 1 in the question. This won't work because the listener itself is not a spring bean, hence you can't autowire or use #Value annotation in the listener itself.
You can try to follow this SO thread that might be helpful, however originally I meant something different:
Make a GreenMail a bean by itself:
#Configuration
// since you're using #SpringBootTest annotation - it will load properties from src/test/reources/application.properties so you can put spring.mail.port=1234 there
public class MyTestMailConfig {
#Bean
public GreenMail greenMail(#Value(${"spring.mail.port"} int port) {
return new GreenMail(port, ...);
}
}
Now this configuration can be placed in src/test/java/<sub-package-of-main-app>/ so that in production it won't be loaded at all
Now the test execution listener could be used only for running starting / stopping the GreenMail server (as I understood you want to start it before the test and stop after the test, otherwise you don't need these listeners at all :) )
public class CustomTestExecutionListener implements TestExecutionListener {
#Override
public void beforeTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.start();
}
#Override
public void afterTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.stop();
}
}
Another option is autowiring the GreenMail bean and using #BeforeEach and #AfterEach methods of JUnit, but in this case you'll have to duplicate this logic in different Test classes that require this behavour. Listeners allow reusing the code.

Spring 2 + JUnit 5, share #MockBean for entire test suite

I create a Spring 2.3 application using Spring Data REST, Hibernate, Mysql.
I created my tests, I've around 450 tests splitted in about 70 files. Because the persistence layer leans on a multi tenant approach (single db per tenant) using a Hikari connection pool, I've the need to avoid the pool is initializated for each test file but at the same time I need to use #MockBean because I need to mock up some repositories in the entire Spring test contest.
I create a custom annotation for all test in my suite:
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#SpringBootTest
#TestExecutionListeners(value = TestExecutionListener.class, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
#Transactional
#ActiveProfiles("test")
public #interface TestConfig {
}
Reading many posts and the doc, I know if I use #MockBean inside a test, the Spring context is reloaded and therefore a new pool connection is created in my case.
My idea is to create a #MockBean and share it with all tests in my suite so the context is not reloaded every time.
I tried several approaches:
#Log4j2
public class TestExecutionListener extends AbstractTestExecutionListener implements Ordered {
#Override
public void beforeTestMethod(TestContext testContext) throws Exception {
try {
TestDbUtils testDbUtils = (TestDbUtils) testContext.getApplicationContext().getBean(TestDbUtils.class);
testDbUtils.truncateDB();
TenantRepository tenantRepository = mock(TenantRepository.class);
testContext.setAttribute("tenantRepository", tenantRepository);
TenantContext.setCurrentTenantId("test");
when(tenantRepository.findByTenantId("test")).thenReturn(testDbUtils.fakeTenant());
} catch (Exception e) {
}
}
#Override
public int getOrder() {
return Integer.MAX_VALUE;
}
}
All my tests are annotated like this:
#TestConfig
#Log4j2
public class InvoiceTests {
#Test
public void test1(){
}
}
Unfortunately my tenantRepository.findByTenantId() is not mocked up. I also tried to create an abstract superclass:
#SpringBootTest
#TestPropertySource(locations = "classpath:application-test.properties")
#TestExecutionListeners(value = TestExecutionListener.class, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
#Transactional
#ActiveProfiles("test")
public abstract class AbstractIntegrationTest {
#MockBean
protected TenantRepository tenantRepository;
#MockBean
protected SubscriptionRepository subscriptionRepository;
#Autowired
protected TestDbUtils testDbUtils;
#BeforeAll
public void beforeAll() {
when(tenantRepository.findByTenantId("test")).thenReturn(testDbUtils.fakeTenant());
}
#BeforeEach
public void setup() {
testDbUtils.truncateDB();
TenantContext.setCurrentTenantId("test");
}
}
Even if my tests extended this superclass, during the run all of them were skipped (not sure why).
Is there any way to accomplish what I described?

Testcontainers and Spring Boot 1.5

We are still using Spring Boot 1.5.x and we want to start using TestContainers. However, all examples are with Spring boot 2.x which is using TestPropertyValues class only available in 2.x. Is it even possible to apply new property values to the configurable context in 1.5.x?
This is the code working in 2.x:
#RunWith(SpringRunner.class)
#SpringBootTest
#ContextConfiguration(initializers = {UserRepositoryTCIntegrationTest.Initializer.class})
public class UserRepositoryTCIntegrationTest extends UserRepositoryCommonIntegrationTests {
#ClassRule
public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa");
static class Initializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
"spring.datasource.username=" + postgreSQLContainer.getUsername(),
"spring.datasource.password=" + postgreSQLContainer.getPassword()
).applyTo(configurableApplicationContext.getEnvironment());
}
}
}
good question :). You have different options to setup your testcontext with Spring Boot 1.5 + TestContainers. Instead of using an indirect way by setting the datasource-properties with dynamic values (like in your example code), you can use the following option:
Provide DataSource Bean via #TestConfiguration
#RunWith(SpringRunner.class)
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class YourRepositoryIntTest {
#Autowired
private YourRepository sut;
#Test
public void testMethod() {
// Given
String expectedId = "SOMEID";
// When
Entity entity = sut.testMethod();
// Then
Assertions.assertThat(entity.getId()).isEqualTo(expectedId);
}
#TestConfiguration
public static class Config {
#Bean
public MySQLContainer testContainer() {
MySQLContainer container = new MySQLContainer();
container.start();
return container;
}
#Bean
#Primary
public DataSource dataSource(MySQLContainer container) {
return DataSourceBuilder.create()
.url(container.getJdbcUrl())
.username(container.getUsername())
.password(container.getPassword())
.driverClassName(container.getDriverClassName())
.build();
}
}
}
Database containers can be launched by simply using a JDBC URL scheme:
application.properties
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.url=jdbc:tc:postgresql:11://localhost/test
Note:
Testcontainers needs to be on your application's classpath at runtime for this to work

spring-boot: Application loads but tests fail

I am experiencing rather strange thing when using Spring Boot. Lets get with it.
I have an app which, when ran from spring-boot:run, loads perfectly fine and I can use my server. However, if I try to run tests (either via launching test from IntelliJ or via surefire plugin) context fails to load.
Issue lies within this class (only relevant part shown):
#RestController
#RequestMapping(
value = "/sa/revisions/"
)
#SuppressWarnings("unchecked")
class RevisionController {
#Autowired
// cant autowire this field
private RepositoryEntityLinks repositoryEntityLinks = null;
/* omitted */
}
And here is my main class:
#EnableAsync
#EnableCaching
#EnableAutoConfiguration
#EnableConfigurationProperties
#Import({
SecurityConfiguration.class,
DataConfiguration.class,
RestConfiguration.class
})
public class SpringAtomApplication {
#Autowired
private DataLoaderManager dataLoaderManager = null;
public static void main(String[] args) {
SpringApplication.run(SpringAtomApplication.class, args);
}
#Bean
public CacheManager cacheManager() {
final GuavaCacheManager manager = new GuavaCacheManager();
manager.setAllowNullValues(false);
return manager;
}
#PostConstruct
private void doPostConstruct() {
this.dataLoaderManager.doLoad();
}
}
As I said, application loads without an issue when ran normally, however when it comes to this simple test, everything falls apart:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = SpringAtomApplication.class)
public class SpringAtomApplicationTests {
#Test
public void contextLoads() {
}
}
Would appreciate any suggestion, because I'd love to start with testing it.
You should set SpringApplicationContextLoader in your test class:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(
classes = SpringAtomApplication.class,
loader = SpringApplicationContextLoader.class)
public class SpringAtomApplicationTests {
#Test
public void contextLoads() {
}
}
With that you can test non-web features (like a repository or a service) or start an fully-configured embedded servlet container and run your tests using MockMvc.
Reference: http://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/SpringApplicationContextLoader.html

Resources