Spring Boot - MongoDB integration test database config change - spring

MongoDBConfig
#Configuration
#EnableMongoRepositories(basePackages="......persistence.repositories")
public class MongoDBConfig extends AbstractMongoClientConfiguration {
#Value("${spring.data.mongodb.host}")
private String host;
#Value("${spring.data.mongodb.port}")
private String port;
#Value("${spring.data.mongodb.username}")
private String username;
#Value("${spring.data.mongodb.password}")
private String password;
#Value("${spring.data.mongodb.database}")
private String database;
#Override
public MongoClient mongoClient() {
return MongoClients.create("mongodb://" + host + ":" + port);
}
#Override
protected String getDatabaseName() {
return database;
}
}
application.yml
spring:
data:
mongodb:
host: localhost
port: 27017
username:
password:
database: spring-mongodb-demo
With the above configurations, spring app using mongo database as per defined in yml - spring-mongodb-demo.
One of my integration test beings like this:
#RunWith(SpringRunner.class)
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class UserRepositoryIntegrationTest {
#Autowired
private MongoTemplate mongoTemplate;
#Autowired
private UserRepository userRepo;
When I run an integration tests it uses the database named - test.
I want the integration tests to use the database - spring-mongodb-demo as per defined in yml file or setting a different database as my preference.
I have tried by making a new profile named application-test.yml and set #ActiveProfiles(profiles = "test") in test class.
I have found lots of thread in this regard, nothing worked.

Related

How to inject Bean of property within my context Bean?

I'm trying to inject Bean of properties within context Bean.
(Spring-boot 2.7.3 / Java 11)
My application.yml is like below:
spring:
config:
active: dev
---
spring:
config:
activate:
on-profile: dev
keycloak:
username: "local"
password: "local"
---
spring:
config:
activate:
on-profile: stg
keycloak:
username: "stg"
password: "stg"
---
spring:
config:
activate:
on-profile: prod
keycloak:
username: "prod"
password: "prod"
and my KafkaProducerConfig.java code like below:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.stereotype.Component;
import lombok.Getter;
#Getter
#Component
#ConstructorBinding
#ConfigurationProperties("keycloak")
public class KafkaProducerConfig {
private final String username;
private final String password;
public KafkaProducerConfig(String username, String password) {
this.username = username;
this.password = password;
}
}
and finally I failed to inject within another class.
Actually, UserDataProducer class extended in a context bean class which means UserDataProducer class also instanciated by spring IoC Container as I know.
I also tried #DependsOn which doesn't work.
#Slf4j
#DependsOn(value = {"KafkaProducerConfig"})
public class UserDataProducer {
#Autowired
KafkaProducerConfig kafkaProducerConfig;
private final String topicName;
public UserDataProducer() {
log.info("===========================================================");
log.info("Initializing UserDataProducer ...");
System.out.println(kafkaProducerConfig.getPassword());
log.info("===========================================================");
// additional properties for transactional producing
topicName = ProducerConfig.PRODUCER_PROPS.getProperty("default.topic");
}
#Slf4j
#Component
public class UserDataProducer {
// use static initializer block to initialize your static fields
// private static final Producer<String, Object> producer;
// initialzer order : static{} -> instance block {} -> constructor
private final String topicName;
public UserDataProducer(KafkaProducerConfig kafkaProducerConfig) {
log.info("===========================================================");
log.info("Initializing UserDataProducer ...");
System.out.println(kafkaProducerConfig.getPassword());
log.info("===========================================================");
// additional properties for transactional producing
topicName = ProducerConfig.PRODUCER_PROPS.getProperty("default.topic");
}

Springboot test not loading properties from application.yml

I have a springboot test case trying to test a method which has some dependency on env properties.
#SpringBootTest(classes = ABCApi.class)
#AutoConfigureMockMvc
class sampleTest {
#Autowired
private WebApplicationContext context;
protected MockMvc mockMvc;
#BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
#InjectMocks
private EmployeeServiceImpl employeeServiceImpl;
#Test
void testEmployeeServiceImpl() {
String summary = "summary";
String description = "description";
String response = employeeServiceImpl.createIssue(summary, description);
verify(EmployeeService, times(1)).createIssue(summary, description);
}
}
Note: I don't need to test the response. All I need is to make sure that the method is called. The response will be null because of some unavailable parameters.
Below is the EmployeeServiceImpl code which has all properties listed. I have the application.yml file in src/test/resources, which is the right location. But while debugging the test case above, the below class is not loading any properties from application.yml sitting in test folder structure.
#Service
public class EmployeeServiceImpl {
#Value("${emp.username}")
private String username;
#Value("${emp.password}")
private String password;
#Value("${emp.url}")
private String url;
#Override
public String createIssue(String summary, String description) {
EmpRestClient client = setEmpClient();
EmpSuperClient empSuperClient = client.getProject();
IssueInput newIssue = buildNewIssueInput(description, summary);
return empSuperClient.createIssue(newIssue).claim().getKey();
}
private EmpRestClient setEmpClient() {
return new EmpRestClientFactory()
.createWithBasicHttpAuthentication(URI.create(url), username, password);
}
I know that these properties are not supposed to be in service layer and instead should be loaded as a bean in config class which will be a future improvement.
How do I fix that test case?
Use constructor injection to inject dependencies. It makes code easy to test.
private final String username;
private final String password;
private final String url;
public EmployeeServiceImpl (#Value("${emp.username}") final String username, #Value("${emp.password}") final String password,
#Value("${emp.url}") String url) {
this.username = username;
this.password = password;
this.url = url;
}
Now you can use the constructor to create EmployeeServiceImpl in your test class.
E.g.
new EmployeeServiceImpl ("username", "password", "http://localhost:8080")

How to setup #ConfigurationProperties to get the values from Hashicorp Vault what is at the address secret/demo-app/keycloak?

I started a Hashicorp Vault and my secrets can be found at secret/demo-app/keycloak. I have 2 values here: clientId, clientSecret
I setup the bootstrap.properties:
spring.application.name=demo-app
spring.cloud.vault.token=00000000-0000-0000-0000-000000000000
spring.cloud.vault.scheme=http
spring.cloud.vault.kv.enabled=true
But I did not find a way to make some #Data and #Configuration classes that can read these values at startup using #ConfigurableProperties:
#Data
#Configuration
#ConfigurationProperties("keycloak")
public class Client {
private String clientId;
private String clientSecret;
#PostConstruct
public void init() {
System.out.println("PostConstruct: " + this.toString());
}
}
But does not seem to work:
PostConstruct: Client(clientId=null, clientSecret=null)
Any ideas what did I miss? Thanks in advance.
This is one of my working solution.
I have one secret in vault, which is key=secret, values=.
Please notice setters and getters should be defined accordingly.
#ConfigurationProperties("mysecret")
public class MySecret {
private String secret;
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
}
Now to use it in any service class, we can use #EnableConfigurationProperties annotation , and just autowire it.
#Service
#EnableConfigurationProperties(MySecret.class)
class my ClientService {
#autowire private MySecret mySecret;
}
it works for me.

MockBean Tests and Multiple Consumers\App Contexts with Spring AMQP

I'm currently facing an issue with testing RabbitMQ consumers with mocks. The issue seems to be that one test class runs with an application context without any mocks, as expected. The next test class to run sets up some mocks that it expects the consumers to use, however when the test runs and a message is sent and it gets picked up by the non-mocked consumers from the application context created for the first test class. As a result my second test fails.
Here is the first test:
#SpringBootTest
public class DemoApplicationTests extends AbstractTestNGSpringContextTests {
#Autowired
private RabbitAdmin rabbitAdmin;
private Logger logger = LoggerFactory.getLogger(this.getClass());
#Test(priority = 1)
public void contextLoads() {
logger.info("=============== CONSUMERS: " + rabbitAdmin.getQueueProperties(USER_MESSAGING_QUEUE).get(RabbitAdmin.QUEUE_CONSUMER_COUNT));
}
}
Second test:
#SpringBootTest
public class UserServiceTests extends AbstractTestNGSpringContextTests {
#Autowired
private UserService userService;
#Autowired
private UserMessageConsumer userMessageConsumer;
#MockBean
#Autowired
private ThirdPartyUserDataClient thirdPartyUserDataClient;
#Autowired
private UserRepository userRepository;
#Autowired
private RabbitAdmin rabbitAdmin;
#Test(priority = 2)
public void createUpdateUserTest() {
logger.info("=============== CONSUMERS: " + rabbitAdmin.getQueueProperties(USER_MESSAGING_QUEUE).get(RabbitAdmin.QUEUE_CONSUMER_COUNT));
String additionalData = org.apache.commons.lang3.RandomStringUtils.random(5);
Mockito.when(thirdPartyUserDataClient.getAdditionalUserData(ArgumentMatchers.anyLong())).thenReturn(additionalData);
User user = new User();
user.setName("Test User");
user.setState(UserState.PENDING);
user = userService.createUser(user);
Assert.assertNotNull(user.getId());
User finalUser = user;
Awaitility.await().until(() -> {
User user2 = userService.getUserById(finalUser.getId());
return finalUser != null && additionalData.equals(user2.getAdditionalData());
});
user.setState(UserState.CREATED);
user = userService.updateUser(user);
Assert.assertEquals(UserState.CREATED, user.getState());
}
}
The consumer:
#Component
public class UserMessageConsumer {
private Logger logger = LoggerFactory.getLogger(this.getClass());
public static final String FAILED_TO_GET_ADDITIONAL_DATA = "FAILED_TO_GET_ADDITIONAL_DATA";
#Autowired
private UserService userService;
#Autowired
private ThirdPartyUserDataClient thirdPartyUserDataClient;
public void handleUserCreatedMessage(UserCreatedMessage userCreatedMessage) {
Long userId = userCreatedMessage.getUserId();
User user = userService.getUserById(userId);
if (user != null) {
String additionalData;
try {
additionalData = thirdPartyUserDataClient.getAdditionalUserData(userId);
logger.info("Successfully retrieved additional data [{}] for user [{}].", additionalData, userId);
} catch (HttpClientErrorException ex) {
additionalData = FAILED_TO_GET_ADDITIONAL_DATA;
logger.warn("Failed to retrieve additional data for user [{}].", userId, ex);
}
user.setAdditionalData(additionalData);
userService.updateUser(user);
}
}
}
This brings up two related questions:
How am I supposed to properly do mock bean testing with consumers in
Spring?
It looks like Spring is bringing up a new a
ApplicationContext for each test class, indicated by the consumer count increasing on the subsequent test runs. It appears
that #MockBean affects the cache key of the ApplicationContext (see:
Mocking and Spying Beans in Spring Boot) and likely explains why there are multiple application contexts.
But how do I stop the consumers in the other stale application contexts from
consuming my test messages?
I've bugjar'd this issue here: RabbitMQ MockBean BugJar
Add #DirtiesContext to each test class to shut down the cached context.

Spring: 2 Repositories out of a single Entity

What I need is 2 Repositories created out of a single entity:
interface TopicRepository implements ReactiveCrudRepository<Topic, String>
interface BackupTopicRepository implements ReactiveCrudRepository<Topic, String>
How is that possible? Right now only one is created.
This is how you would do it.
#Configuration
#ConfigurationProperties(prefix = "mongodb.topic")
#EnableMongoRepositories(basePackages = "abc.def.repository.topic", mongoTemplateRef = "topicMongoTemplate")
#Setter
class TopicMongoConfig {
private String host;
private int port;
private String database;
#Primary
#Bean(name = "topicMongoTemplate")
public MongoTemplate topicMongoTemplate() throws Exception {
final Mongo mongoClient = createMongoClient(new ServerAddress(host, port));
return new MongoTemplate(mongoClient, database);
}
private Mongo createMongoClient(ServerAddress serverAddress) {
return new MongoClient(serverAddress);
}
}
Another configuration
#Configuration
#ConfigurationProperties(prefix = "mongodb.backuptopic")
#EnableMongoRepositories(basePackages = "abc.def.repository.backuptopic", mongoTemplateRef = "backupTopicMongoTemplate")
#Setter
class BackupTopicMongoConfig {
private String host;
private int port;
private String database;
#Primary
#Bean(name = "backupTopicMongoTemplate")
public MongoTemplate backupTopicMongoTemplate() throws Exception {
final Mongo mongoClient = createMongoClient(new ServerAddress(host, port));
return new MongoTemplate(mongoClient, database);
}
private Mongo createMongoClient(ServerAddress serverAddress) {
return new MongoClient(serverAddress);
}
}
Your TopicRepository and BackuoTopicRepository should reside in abc.def.repository.topic and abc.def.repository.backuptopic respectively.
And also you need to have these properties defined in your properties or yml file
mongodb:
topic:
host:
database:
port:
backuptopic:
host:
database:
port:
Lastly, disable springboot autoconfiguration for mongo.
#SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})

Resources