Note: I already look at and tried some approaches on SO e.g. How to test Spring's declarative caching support on Spring Data repositories?, but as most of them old, I cannot make them work properly and I need a solution with the latest library versions. So, I would be appreciated if you have a look at the question and help.
#Service
#EnableCaching
#RequiredArgsConstructor
public class DemoServiceImpl implements DemoService {
private static final String CACHE_NAME = "demoCache";
private final LabelRepository labelRepository;
private final LabelTranslatableRepository translatableRepository;
private final LanguageService languageService;
#Override
public LabelDTO findByUuid(UUID uuid) {
final Label label = labelRepository.findByUuid(uuid)
.orElseThrow(() -> new EntityNotFoundException("Not found."));
final List<LabelTranslatable> translatableList = translatableRepository.findAllByEntityUuid(uuid);
return new LabelDTO(Pair.of(label.getUuid(), label.getKey()), translatableList);
}
}
I created the following Unit Test to test caching for the nethod above:
#EnableCaching
#ImportAutoConfiguration(classes = {
CacheAutoConfiguration.class,
RedisAutoConfiguration.class
})
#ExtendWith(MockitoExtension.class)
class TextLabelServiceImpl_deneme_Test {
#Autowired
private CacheManager cacheManager;
#InjectMocks
private LabelService labelService;
#Mock
private LabelRepository labelRepository;
#Mock
private LabelTranslatableRepository translatableRepository;
#Test
void test_Cache() {
UUID uuid = UUID.randomUUID();
final TextLabel textLabel = new TextLabel();
textLabel.setId(1);
textLabel.setKey("key1");
TextLabelTranslatable textLabelTranslatable = new TextLabelTranslatable();
textLabelTranslatable.setEntityUuid(uuid);
textLabelTranslatable.setLanguage(SupportedLanguage.fr);
textLabelTranslatable.setValue("value1");
final List<TextLabelTranslatable> translatableList = new ArrayList<>();
translatableList.add(textLabelTranslatable);
when(labelRepository.findByUuid(uuid)).thenReturn(Optional.of(textLabel));
when(translatableRepository.findAllByEntityUuid(uuid)).thenReturn(translatableList);
TextLabelDTO result1 = labelService.findByUuid(uuid);
TextLabelDTO result2 = labelService.findByUuid(uuid);
assertEquals(result1, result2);
Mockito.verify(translatableRepository, Mockito.times(1)).findAllByEntityUuid(uuid);
}
I am not sure if there is a missing part in my test, but at the last line (Mockito.verify()), it returns 2 instead of 1 that means caching not works. But it is working properly and there is a problem in my test I think. How should I complete the unit test to check the caching properly?
You need to annotate the service class method with #Cacheable. Try to follow the code in this tutorial. The following test code works as expected
#Import({CacheConfig.class, DemoServiceImpl.class})
#ExtendWith(SpringExtension.class)
#EnableCaching
#ImportAutoConfiguration(classes = {
CacheAutoConfiguration.class,
RedisAutoConfiguration.class
})
class DemoServiceImplTest {
#MockBean
private LabelRepository labelRepository;
#Autowired
private DemoServiceImpl demoService;
#Autowired
private CacheManager cacheManager;
#TestConfiguration
static class EmbeddedRedisConfiguration {
private final RedisServer redisServer;
public EmbeddedRedisConfiguration() {
this.redisServer = new RedisServer();
}
#PostConstruct
public void startRedis() {
redisServer.start();
}
#PreDestroy
public void stopRedis() {
this.redisServer.stop();
}
}
#Test
void givenRedisCaching_whenFindItemById_thenItemReturnedFromCache() {
UUID id = UUID.randomUUID();
Label aLabel = new Label(id, "label");
Mockito.when(labelRepository.findById(id)).thenReturn(Optional.of(aLabel));
Label labelCacheMiss = demoService.findByUuid(id);
Label labelCacheHit = demoService.findByUuid(id);
Mockito.verify(labelRepository, Mockito.times(1)).findById(id);
}
}
With this service class code:
#Service
#RequiredArgsConstructor
#EnableCaching
public class DemoServiceImpl {
public static final String CACHE_NAME = "demoCache";
private final LabelRepository labelRepository;
#Cacheable(value = CACHE_NAME)
public Label findByUuid(UUID uuid) {
return labelRepository.findById(uuid)
.orElseThrow(() -> new EntityNotFoundException("Not found."));
}
}
And this CacheConfig:
#Configuration
public class CacheConfig {
#Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return (builder) -> builder
.withCacheConfiguration(DemoServiceImpl.CACHE_NAME,
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)));
}
#Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
}
}
Related
I have a spring-boot application.
I have entity:
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Document(COLLECTION_NAME)
public class PersonEntity {
public static final String COLLECTION_NAME = "person_info";
private static final String PERSON_NAME = "person_name";
#Id
private PersonId id;
#Field(name = PERSON_NAME)
private String personName;
#Indexed(name = "ttl_index", expireAfterSeconds=20)
private LocalDateTime date;
}
I have a repository interface:
public interface PersonRepository {
void saveWithTtl(PersonEntity entity);
}
The repository implementation:
#Slf4j
#Repository
public class PersonRepositoryImpl implements PersonRepository {
private final int expireAfterSeconds;
private final ReactiveMongoTemplate mongoTemplate;
public PersonRepositoryImpl(#Value("${ttl.index}") int expireAfterSeconds,
ReactiveMongoTemplate mongoTemplate) {
this.expireAfterSeconds = expireAfterSeconds;
this.mongoTemplate = mongoTemplate;
}
#Override
public void saveWithTtl(PersonEntity entity) {
mongoTemplate.indexOps(PersonEntity.class)
.ensureIndex(new Index().on(PersonEntity.CREATED_AT, ASC)
.expire(expireAfterSeconds)).subscribe(result -> log.info("Ttl index has been created: {}", result));
mongoTemplate.save(entity).subscribe(result -> log.info("Entity has been saved: {}", result));
}
}
And, finally, I have test that does not work:
#DataMongoTest
#Testcontainers
public class PersonRepositoryIT {
#Autowired
private ReactiveMongoTemplate mongoTemplate;
#Autowired
private PersonRepository repository;
#Container
private static MongoDbContainer mongoDbContainer = new MongoDbContainer();
#AfterEach
void cleanUp() {
repository.deleteAll();
}
#DynamicPropertySource
static void registerMongoProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", mongoDbContainer::getReplicaSetUrl);
}
#Test
public void shouldCreateAndDeleteRecordsAfterDelay_whenSaveWithTtl_givenDefinedTll() {
//given
PersonEntity givenEntity = PersonEntity.builder().createdAt(LocalDateTime.now())
.personName("Joe")
.id(PERSON_ID).build();
//when
repository.saveWithTtl(givenEntity);
//then
StepVerifier.create(mongoTemplate.estimatedCount(PersonEntity.COLLECTION_NAME))
.expectNext(1L)
.verifyComplete();
}
}
On expectNext it fails coz it returns 0 and not 1.
mongoTemplate.estimatedCount returns 0
When I test the repository from Postman (repo is calling inside service), it creates the document in MongoDB wil ttl index, as expected.
In test fonfig I have set the ${ttl.index} to 20.
What am I doing wrong?
I don't know if it is to late, but I had the same problem today.
I Found your question looking for an answer for my problem hahahaha.
This snipped worked for me:
#Container
public static MongoDBContainer container = new MongoDBContainer(DockerImageName.parse("mongo:6"));
#DynamicPropertySource
static void mongoDbProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", container::getReplicaSetUrl);
}
#Bean
public ReactiveMongoTemplate reactiveMongoTemplate() throws Exception {
container.start();
ConnectionString connectionString = new ConnectionString(container.getReplicaSetUrl());
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.build();
MongoClient mongoClient = MongoClients.create(mongoClientSettings);
return new ReactiveMongoTemplate(mongoClient,"test");
}
Apparently ReactiveMongoTemplate is not being injected by default, then I created my own Bean an it worked
I have a job that looks like this:
#Named
public class MyCamelRouteBuilder extends RouteBuilder {
private static final String JOB_NAME = "abc";
private static final String JOB_METHOD_NAME = "xyz";
private final MyJob myJob;
#Inject
public MyCamelRouteBuilder(MyJob myJob) {
super();
this.myJob = myJob;
}
#Override
public void configure() {
fromF("direct:%s", JOB_NAME)
.routeId(JOB_NAME)
.bean(myJob, JOB_METHOD_NAME)
.end();
fromF("master:some_name_1/some_name_2:scheduler:%s?delay=%s", JOB_NAME, 1234)
.routeId("JobTimer")
.toF("direct:%s", JOB_NAME)
.end();
}
}
A very simplified version of the job class:
#Named
public class MyJob {
private MyJob() {}
}
public void xyz() {
}
}
This does work and it does gets triggered as expected.
The problem starts here:
Now, I also want to create a REST controller that will be able to trigger the exact same job. Something like this:
#Named
#RestController
#RequestMapping
#Validated
public class MyController {
private static final String JOB_NAME = "abc";
private final ProducerTemplate producerTemplate;
#Inject
public MyController(
ProducerTemplate producerTemplate
) {
this.producerTemplate = producerTemplate;
}
#PostMapping(path = "/my_endpoint")
public String run() throws Exception {
producerTemplate.requestBody("direct:" + JOB_NAME);
return "ok";
}
}
But once it reaches this line, the job is not triggered and the request call keeps hanging.
producerTemplate.requestBody("direct:" + JOB_NAME);
Any ideas?
The fix for my problem:
#Named
#RestController
#RequestMapping
#Validated
public class MyController {
private static final String JOB_NAME = "abc";
#Produce("direct:" + JOB_NAME)
private final ProducerTemplate producerTemplate;
private final CamelContext context;
#Inject
public MyController(
ProducerTemplate producerTemplate, CamelContext context
) {
this.producerTemplate = producerTemplate;
this.context = context;
}
#PostMapping(path = "/my_endpoint")
public String run() throws Exception {
Exchange exchange = new DefaultExchange(context);
producerTemplate.send(exchange);
return "ok";
}
}
How to write unit test (via Junit) for MongoTemplate updateFirst() method ?
I have this service class:
#Service
public class TemplateServiceImpl implements TemplateService
{
#Resource
private TemplateRepository templateRepository;
#Resource
private MongoTemplate mongoTemplate;
#Override
public TemplateDto create(TemplateDto templateDto)
{
TemplateDto result;
templateDto.setDeleted(false);
result = templateRepository.insert(templateDto);
return result;
}
#Override
public boolean update(ObjectId id, TemplateDto templateDto)
{
if (templateDto != null)
{
Update update = new Update();
if (templateDto.getName() != null)
{
update.set("name", templateDto.getName());
}
if (templateDto.getTemplate() != null)
{
update.set("template", templateDto.getTemplate());
}
if (templateDto.isDeleted())
{
update.set("deleted", Boolean.TRUE);
}
update.set("lastUpdate", new Date());
return mongoTemplate.updateFirst(new Query(Criteria.where("_id").is(id)), update, "template")
.wasAcknowledged();
}
return false;
}
}
I want write unit test for update method .
This is my relevant test class:
#ExtendWith(MockitoExtension.class)
class TemplateServiceImplTest
{
#InjectMocks
private static TemplateServiceImpl templateService;
#Mock
private static TemplateRepository templateRepository;
#Mock
private static MongoTemplate mongoTemplate;
private static TemplateDto templateDto;
private static List<TemplateDto> listOfTemplates;
private static String requestBody;
#BeforeAll
private static void setUp(){
templateDto = createTemplateDto();
listOfTemplates = createListOfTemplate();
requestBody = createRequesyBody();
}
#Test
void create()
{
when(templateRepository.insert(templateDto)).thenReturn(templateDto);
TemplateDto actualResult = templateService.create(TemplateServiceImplTest.templateDto);
assertThat(actualResult).isNotNull();
assertThat(actualResult).isInstanceOf(TemplateDto.class);
assertThat(actualResult.getId()).isEqualTo(templateDto.getId());
}
#Test
void update()
{
Update update = new Update();
/*
when(mongoTemplate.updateFirst(new Query(Criteria.where("_id").is(templateDto.getId().toString())) ,update , TemplateDto.class)).thenReturn(
UpdateResult.acknowledged());
*/
/*
given(mongoTemplate.updateFirst(new Query(Criteria.where("_id").is(templateDto.getId().toString())) ,update , TemplateDto.class));
*/
boolean actualResult = templateService.update(templateDto.getId(), templateDto);
assertThat(actualResult).isNotNull();
assertThat(actualResult).isTrue();
}
// other methods...
}
I mocked MongoTemplate like this:
#Mock
private static MongoTemplate mongoTemplate;
when I run test I get NPE for mongoTemplate at update method , I searched and I saw this 2 approach:
when(mongoTemplate.updateFirst(new Query(Criteria.where("_id").is(templateDto.getId().toString())) ,update , TemplateDto.class)).thenReturn(
UpdateResult.acknowledged());
// Or
given(mongoTemplate.updateFirst(new Query(Criteria.where("_id").is(templateDto.getId().toString())) ,update , TemplateDto.class));
But None of them worked! .
Thank you all .
Tried to create a configuration class which depends on another bean class using #DependsOn annotation and later found that #DependsOn only work along with #Bean and #Component and not with #Configuration.
#SpringBootApplication
public class Application extends SpringBootServletInitializer {
private static final String URL_MAPPINGS = "/*";
private static final Class<?>[] classes = new Class[]{Application.class, Config1.class};
public static void main(String[] args) {
SpringApplication.run(classes, args);
}
...
}
#Configuration
public class MongoConfig {
#Value("${db.name}")
private String dbName;
#Value("${db.url}")
private String url;
#Override
public MongoClient mongo() {
ConnectionString connectionString = new ConnectionString("mongodb:"+url);
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.build();
return MongoClients.create(mongoClientSettings);
}
#Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongo(), dbName);
}
}
#Import({ MongoConfig.class, Config2.class})
#Configuration
public class Config1 {
public BeanA beanA(MongoTemplate mongoTemplate, Environment environment) {
return new BeanA(mongoTemplate);
}
}
#Configuration
#ComponentScan("com.test")
//this should be processed after BeanA is created, similar to #DependsOn
public class Config2 {
...
}
public class BeanA {
private MongoTemplate mongoTemplate;
public BeanA(MongoTemplate mongoTemplate, Environment environment) {
this.mongoTemplate = mongoTemplate;
this.applicationPropertySource = new ApplicationPropertySource(SOURCE_NAME);
MutablePropertySources mutablePropertySources = ((StandardEnvironment) environment).getPropertySources();
mutablePropertySources.addFirst(applicationPropertySource);
getExistingProperties().forEach((key, val) -> applicationPropertySource.set(key, val));
}
private Map<String, String> getExistingProperties() {
//assume values are obtained from DB
}
public static class ApplicationPropertySource extends PropertiesPropertySource {
public ApplicationPropertySource(String name) {
super(name, new HashMap());
}
public Object set(String key, String value) {
return getSource().put(key, value);
}
}
I like to achieve this to avoid adding #DependsOn annotation on all the beans in the project, is this even possible?
Spring version is v1.5.12.RELEASE
I have scheduller:
#Component
public class MyScheduler {
private static final long INIT_DELAY = 1L;
private static final long DELAY = 10L;
private final UserService userService;
public MyScheduler(UserService userService) {
this.userService = userService;
}
#EventListener(ApplicationReadyEvent.class)
public void schedule() {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleWithFixedDelay(this::process, INIT_DELAY, DELAY, TimeUnit.SECONDS);
}
private void process() {
userService.process(new User("Bill", 20));
}
}
In UserService I save new user and throw exception:
#Slf4j
#Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public void process(User user) {
log.info("Start process...");
userRepository.save(user);
methodWithException();
log.info("End process...");
}
private void methodWithException() {
throw new RuntimeException();
}
}
As a result, the user is saved despite the exception. To fix this I can apply several ways:
1) Add #Transactional above private void process() and change this method to public
2) Add #Transactional above public void process(User user) method in UserService
In first case it not helps because process() witn #Transactional calls from the same class.
In second case it helps.
But if I add new Service, for example LogService:
#Service
public class LogServiceImpl implements LogService {
private final LogRepository logRepository;
public LogServiceImpl(LogRepository logRepository) {
this.logRepository = logRepository;
}
#Transactional
#Override
public Log save(Log log) {
return logRepository.save(log);
}
}
And change scheduler to:
#Component
public class MyScheduler {
private static final long INIT_DELAY = 1L;
private static final long DELAY = 10L;
private final UserService userService;
private final LogService logService;
public MyScheduler(UserService userService, LogService logService) {
this.userService = userService;
this.logService = logService;
}
#EventListener(ApplicationReadyEvent.class)
public void schedule() {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleWithFixedDelay(this::process, INIT_DELAY, DELAY, TimeUnit.SECONDS);
}
private void process() {
User user = userService.process(new User("Bill", 20));
logService.save(new Log(user.getId(), new Date()));
}
}
Question:
userService.process call in one transaction and logService.save cals in another transaction. I need call bouth services in One transaction.
I see two ways:
1) inject logService to userService and call logService.save in userService.process method
2) Create new service for example SchedulerService with method process and inject userService and logService in this service. And call in bouth services in one transaction.
In first case I get new dependency in userService and this can violate area of responsibility in this service. Why should the service know to pull another service
In second case I need create additional service (one more class)
It would be ideal to be able to annotate the internal Schedulers method #Transactional annotation. I know that this can be done using cglib instead proxy but I use proxy.
Which approach would be better?
Imho, this is a good usecase for PlatformTransactionManager, with or without TransactionTemplate.
For this I'm going with a pure PlatformTransactionManager solution.
If you're on Spring Boot you'll have it as a Bean by default.
#Component
class MyScheduler {
private static final long INIT_DELAY = 1L;
private static final long DELAY = 10L;
private final PlatformTransactionManager txManager;
private final ConcurrencyService userService;
private final LogService logService;
MyScheduler(
final PlatformTransactionManager txManager,
final ConcurrencyService userService,
final LogService logService) {
this.txManager = txManager;
this.userService = userService;
this.logService = logService;
}
#EventListener(ApplicationReadyEvent.class)
public void schedule() {
final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.scheduleWithFixedDelay(this::process, INIT_DELAY, DELAY, TimeUnit.SECONDS);
}
private void process() {
final DefaultTransactionDefinition definition = new DefaultTransactionDefinition(PROPAGATION_REQUIRES_NEW);
final TransactionStatus tx = txManager.getTransaction(definition);
try {
final User user = userService.process(new User("Bill", 20));
logService.save(new Log(user.getId(), new Date()));
txManager.commit(tx);
} catch (final YourException e) {
txManager.rollback(tx);
}
}
}
Using TransactionTemplate will "eliminate" the need of explicitly invoking commit and rollback.
You can have TransactionTemplate as a Bean, or you can construct it manually from a PlatformTransactionManager, as I've done here.
final TransactionTemplate transactionTemplate = new TransactionTemplate(txManager);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(final TransactionStatus status) {
final User user = userService.process(new User("Bill", 20));
logService.save(new Log(user.getId(), new Date()));
}
});