SpringBoot test isolation not guaranteed - java

I created a SpringBoot test:
#RunWith(SpringRunner.class)
#SpringBootTest
#TestPropertySource(locations = "classpath:application-dev.properties")
#Transactional
public class ContactTests2 {
private Logger log = LogManager.getLogger();
#PersistenceContext
private EntityManager entityManager;
#Autowired
private ContactRepository customerRepository;
#Autowired
private StoreRepository storeRepository;
#Autowired
private NoteService noteService;
#Autowired
private Validator validator;
private Store store;
#Before
#WithMockUser(roles = "ADMIN")
public void setup() {
log.debug("Stores {}", storeRepository.count());
store = createStore();
storeRepository.save(store);
}
#Test
#WithMockUser(roles = "ADMIN")
public void saveWithNote() {
Contact customer = new Contact();
customer.setPersonType(PersonType.NATURAL_PERSON);
customer.setFirstName("Daniele");
customer.setLastName("Rossi");
customer.setGender(Gender.MALE);
customer.setBillingCountry(Locale.ITALY.getCountry());
customer.setShippingCountry(Locale.ITALY.getCountry());
customer.setStore(store);
Note note = new Note();
note.setGenre(NoteGenre.GENERIC);
note.setOperationType(AuditType.NOTE);
note.setText("note");
customer = customerRepository.save(customer);
noteService.addNote(note, customer);
}
#Test
#WithMockUser(roles = "ADMIN")
public void save() {
Contact customer = new Contact();
customer.setPersonType(PersonType.NATURAL_PERSON);
customer.setFirstName("Daniele");
customer.setLastName("Rossi");
customer.setGender(Gender.MALE);
customer.setBillingCountry(Locale.ITALY.getCountry());
customer.setShippingCountry(Locale.ITALY.getCountry());
customer.setStore(store);
customerRepository.save(customer);
assertEquals(customer, customerRepository.findById(customer.getId()).get());
}
// ====================================================
//
// UTILITY METHODS
//
// ====================================================
private Store createStore() {
Store store = new Store();
store.setName("Padova");
store.setCode("PD");
store.setCountry("IT");
return store;
}
}
this is the note service:
#Service
#Transactional
#PreAuthorize("isAuthenticated()")
public class NoteService {
#PersistenceContext
private EntityManager entityManager;
#Autowired
private NoteRepository noteRepository;
/**
* Add a note to a specific object (parent).
*
* #param note
* #param parent
* #return the added note
*/
public Note addNote(Note note, Persistable<Long> parent) {
// ****************************************************
// VALIDATION CHECKS
// ****************************************************
Assert.notNull(note, InternalException.class, ExceptionCode.INTERNAL_ERROR);
Assert.notNull(parent, InternalException.class, ExceptionCode.INTERNAL_ERROR);
// ****************************************************
// END VALIDATION CHECKS
// ****************************************************
note.setParentId(parent.getId());
note.setParentType(parent.getClass().getSimpleName());
note.setRemoteAddress(NetworkUtils.getRemoteIpFromCurrentContext());
note = noteRepository.save(note);
return note;
}
}
I'm using Hibernate and Mysql 5.7. The problem is that the test called saveWithNote(). When I run this test, following tests fails because the setup() method throw a duplicated exception. It seems the previous test is not rolledback.
This is what happens:
Removing the line noteService.addNote(note, customer); everything works like a charm.
What am I doing wrong? Why test isolation is not preserved?

This is because you are using a real data store as the dependency.
When running saveWithNote(), the customer entry is persisted in database. It is not removed in your test setup, so the when you run save(), you bump into a duplicate database entry.
Solution 1:
Use teardown() method to remove database entries you created during the test.
Example:
#After
#WithMockUser(roles = "ADMIN")
public void teardown() {
// delete the customer entry here
}
Reference: https://examples.javacodegeeks.com/core-java/junit/junit-setup-teardown-example/
Solution 2: Every time you run setup(), wipe the database tables clean.
Example:
#Before
#WithMockUser(roles = "ADMIN")
public void setup() {
// wipe your database tables to make them empty
}
Both solution 1 and 2 should be done with test database only. You DON'T want to clean up production DB.
Solution 3 (recommended):
Use mocked repositories and mock injection (instead of autowiring repositories with real implementation).
Sample/ Reference: https://stackoverflow.com/a/36004293/5849844

Most likely your table is using MyISAM storage engine which does not support transactions (as per Table 15.2 MyISAM Storage Engine Features docs).
Redefine the table using InnoDB storage engine. Take a look at 14.8.1.1 Creating InnoDB Tables docs, it should be on by default but you can check it with:
SELECT ##default_storage_engine;

Related

#Cacheable not working, still calling the caching method

I am trying to use #Cacheable to cache the roles regardless of the parameter. But the #Cacheable does not quite work and the method would get called twice.
CachingConfig:
#Configuration
#EnableCaching
public class CachingConfig {
#Bean
public CacheManager cacheManager(#Value("${caching.ttl.period}") long period,
#Value("${caching.ttl.unit}") String unit) {
return new ConcurrentMapCacheManager() {
#Override
public Cache createConcurrentMapCache(String name) {
return new ConcurrentMapCache(name, CacheBuilder.newBuilder()
.expireAfterWrite(period, TimeUnit.valueOf(unit)).build().asMap(), true);
}
};
}
}
RoleMappingService:
#Service
public class RoleMappingService {
private final AdminClient adminClient;
public RoleMappingService(AdminClient adminClient) {
this.adminClient = adminClient;
}
#Cacheable(value = "allRoles", key = "#root.method")
public List<Role> getAllRoles(String sessionToken) {
AdminSession adminSession = new AdminSession();
AdminSession.setSessionToken(sessionToken);
List<RoleGroup> allRoleGroups = this.adminClient.getAllRoleGroups(adminSession)
.orElse(Collections.emptyList());
List<Role> allRoles = allRoleGroups
.stream()
.map(RoleGroup::getRoles)
.flatMap(List::stream)
.collect(Collectors.toList());
return allRoles;
}
Test:
#SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RoleCachingTest {
private final JFixture fixture = new JFixture();
private AdminClient adminClient = mock(AdminClient.class);
#Test
public void allRolesShouldBeCached(){
RoleGroup mockRoleGroup = mock(RoleGroup.class);
Role mockRole = this.fixture.create(Role.class);
when(this.adminClient.getAllRoleGroups(any(AdminSession.class)))
.thenReturn(Optional.of(Arrays.asList(mockRoleGroup)));
when(mockRoleGroup.getRoles()).thenReturn(Arrays.asList(mockRole));
RoleMappingService sut = new RoleMappingService(adminClient);
List<Role> firstRes = sut.getAllRoles(
fixture.create(String.class));
List<Role> secondRes = sut.getAllRoles(
fixture.create(String.class));
assertEquals(firstRes.size(), secondRes.size());
assertEquals(firstRes.get(0).getId(), secondRes.get(0).getId());
assertEquals(firstRes.get(0).getRoleName(), secondRes.get(0).getRoleName());
// The getAllRoleGroups() should not be called on the second call
verify(this.adminClient, times(1)).getAllRoleGroups(any(AdminSession.class));
}
The adminClient.getAllRoleGroups() would always get called twice in this test, while I expect it would only get called once because of #Cacheable.
The project structure:
project structure
I think your #Cacheable annotation is not working because you have not specified Interface for class. This is because of proxy created for caching by Spring. Spring has specified below in its documentation . I thing you have not specified proxy-target-class, it means it will be default to false. If it is false it will use JDK interface based proxies. But in your case you class i.e. RollMappingService is not implementing interface. Create interface RollMappingService with method getAllRoles and implement it, will sole your problem.
Controls what type of caching proxies are created for classes annotated with the #Cacheable or #CacheEvict annotations. If the proxy-target-class attribute is set to true, then class-based proxies are created. If proxy-target-class is false or if the attribute is omitted, then standard JDK interface-based proxies are created. (See Section 9.6, “Proxying mechanisms” for a detailed examination of the different proxy types.)
Also modify your test class to create Spring bean for RoleMappingService in following ways and inject mock of AdminClient into it
#Mock
private AdminClient mockedAdminClient;
#InjectMocks
#Autowired
private RoleMappingService roleMappingService
#Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(roleMappingService,
"adminClient",
mockedAdminClient);
}

How to test spring batch job within #Transactional SpringBootTest test case?

I just can't seem to win today...
Is there a way to read from a OneToMany relationship in a Spock SpringBootTest integration test, without annotating the test as #Transactional or adding the unrealistic spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true?
OR, is there a way to launch a Spring-Batch Job from within a #Transactional test case?
Let me elaborate...
I'm trying to get a simple Spring Boot Integration test working for my Spring Batch reporting process, which reads from tangled web of DB2 tables and generates a series of change messages for interested systems. I'm using the Groovy Spock testing framework and an H2 in-memory database filled with a representative slice of my DB2 tables' data.
At the beginning of the test, I'm attempting to use every entity from a given Table to generate entries in a change-tracking table that drives my messaging.
setup:
List allExistingTestPeople = peopleRepository.findAll()
Collections.shuffle(allExistingTestPeople)
allExistingTestPeople?.each { Person person ->
Nickname nicknames = person.nicknames
nicknames?.each { Nickname nickname ->
changeTrackingRepository.save(new Change(personId: person.id, nicknameId: nickname.id, status: NEW))
}
}
Given these as my DB2 domain classes:
#Entity
#Table(name = "T_PERSON")
public class Person {
#Id
#Column(name = "P_ID")
private Integer id;
#Column(name = "P_NME")
private String name;
#OneToMany(targetEntity = Nickname.class, mappedBy = "person")
private List<Nickname> nicknames;
}
#Entity
#Table(name = "T_NICKNAME")
public class Nickname{
#EmbeddedId
private PersonNicknamePK id;
#Column(name = "N_NME")
private String nickname;
#ManyToOne(optional = false, targetEntity = Person.class)
#JoinColumn(name = "P_ID", referencedColumnName="P_ID", insertable = false, updatable = false)
private Person person;
}
#Embeddable
public class PersonNicknamePK implements Serializable {
#Column(name="P_ID")
private int personId;
#Column(name="N_ID")
private short nicknameId;
}
But I'm getting this LazyInitializationException, even though I'm reading from that OneToMany relationship within the context of a test case...
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.my.package.db2.model.Person.nicknames, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:161)
at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:350)
I came across the advice online to annotate my test case with the #Transactional annotation, which definitely got me a little further, allowing me to read from this OneToMany relationship. However, when I then attempt to launch the Spring Batch Job I'd like to test from my when clause:
#Transactional
def "Happy path test to validate I can generate a report of changes"() {
setup:
//... See above
when:
service.launchBatchJob()
then:
//... Messages are generated
}
I'm getting the exception that a Spring Batch Job can't be launched from the context of a transaction! Even though I'm using an in-memory Job manager via ResourcelessTransactionManager and MapJobRepositoryFactoryBean, since this is just a short lived scheduled script I'm writing...
java.lang.IllegalStateException: Existing transaction detected in JobRepository. Please fix this and try again (e.g. remove #Transactional annotations from client).
at org.springframework.batch.core.repository.support.AbstractJobRepositoryFactoryBean$1.invoke(AbstractJobRepositoryFactoryBean.java:177)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy125.createJobExecution(Unknown Source)
at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:134)
at com.my.package.service.MyService.launchBatchJob(MyService.java:30)
The only thing that seems to work so far is if I scrap the #Transactional annotation and instead add spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true to my application-test.properties file. BUT, this doesn't seem like a very good idea, because it's not realistic - if I add this, then even if there's a bug in my code due to a lazy-initialization-exception, I'd never see it in the tests.
Sorry for the novel, hoping someone can point me in the right direction :(
EDIT:
Also here's my In-memory Spring-Batch configuration, in which I've tried turning off the transaction validation. Unfortunately, while this gets me a little further, the Spring Batch partioner's autowired EntityManager is suddenly failing to run queries in the H2 database.
#Configuration
#EnableBatchProcessing
public class InMemoryBatchManagementConfig {
#Bean
public ResourcelessTransactionManager resourceslessTransactionManager() {
ResourcelessTransactionManager resourcelessTransactionManager = new ResourcelessTransactionManager();
resourcelessTransactionManager.setNestedTransactionAllowed(true);
resourcelessTransactionManager.setValidateExistingTransaction(false);
return resourcelessTransactionManager;
}
#Bean
public MapJobRepositoryFactoryBean mapJobRepositoryFactory(ResourcelessTransactionManager txManager)
throws Exception {
MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean(txManager);
factory.setValidateTransactionState(false);
factory.afterPropertiesSet();
return factory;
}
#Bean
public JobRepository jobRepository(MapJobRepositoryFactoryBean factory) throws Exception {
return factory.getObject();
}
#Bean
public SimpleJobLauncher jobLauncher(JobRepository jobRepository) throws Exception {
SimpleJobLauncher launcher = new SimpleJobLauncher();
launcher.setJobRepository(jobRepository);
launcher.afterPropertiesSet();
return launcher;
}
#Bean
public JobExplorer jobExplorer(MapJobRepositoryFactoryBean factory) {
return new SimpleJobExplorer(factory.getJobInstanceDao(), factory.getJobExecutionDao(),
factory.getStepExecutionDao(), factory.getExecutionContextDao());
}
#Bean
public BatchConfigurer batchConfigurer(MapJobRepositoryFactoryBean mapJobRepositoryFactory,
ResourcelessTransactionManager resourceslessTransactionManager,
SimpleJobLauncher jobLauncher,
JobExplorer jobExplorer) {
return new BatchConfigurer() {
#Override
public JobRepository getJobRepository() throws Exception {
return mapJobRepositoryFactory.getObject();
}
#Override
public PlatformTransactionManager getTransactionManager() throws Exception {
return resourceslessTransactionManager;
}
#Override
public JobLauncher getJobLauncher() throws Exception {
return jobLauncher;
}
#Override
public JobExplorer getJobExplorer() throws Exception {
return jobExplorer;
}
};
}
}
This error happens because your code will be already executed in a transaction driven by Spring Batch. So running the job in the scope of a transaction is not correct. However, if you still want to disable the transaction validation done by the job repository, you can set the validateTransactionState to false, see AbstractJobRepositoryFactoryBean#setValidateTransactionState.
That said, running the job in a transaction is not the way to fix org.hibernate.LazyInitializationException. The property spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true is there for a reason, and if it works for you, I believe it is a better approach than running the entire job in a transaction (and btw, if I had to use a transaction for that, I would narrow its scope to the minimum (for example the step) and not the entire job).
You can do transactions programmatically using TransactionTemplate to run only the "setup" inside a transaction (instead of having everything in #Transactional). Unfortunately this way the transaction will be committed and you will need to do some manual cleanup.
It can be autowired as any other bean:
#Autowired
private TransactionTemplate transactionTemplate;
...and it's used this way:
transactionTemplate.execute((transactionStatus) -> {
// ...setup...
return null; // alternatively you can return some data out of the callback
});

Cannot inject service into Spring test

financialReportService is null in REST controller that denotes it fails to inject.
Test:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = SnapshotfindocApp.class)
public class FindocResourceIntTest {
#Inject
private FinancialReportService financialReportService;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
FindocResource findocResource = new FindocResource();
ReflectionTestUtils.setField(findocResource, "findocRepository", findocRepository);
this.restFindocMockMvc = MockMvcBuilders.standaloneSetup(findocResource)
.setCustomArgumentResolvers(pageableArgumentResolver)
.setMessageConverters(jacksonMessageConverter).build();
}
#Test
#Transactional
public void getFinancialRecords() throws Exception {
// Get all the financial-reports
restFindocMockMvc.perform(get("/api/financial-reports"))
.andExpect(status().isOk());
List<Findoc> finReports = financialReportService.getFinancialReports();
for (Findoc fr : finReports) {
assertThat(fr.getNo_months()).isBetween(12, 18);
LocalDate documentTimeSpanLimit = LocalDate.now().minusMonths(18);
assertThat(fr.getFinancial_date()).isAfterOrEqualTo(documentTimeSpanLimit);
}
}
The service:
#Service
#Transactional
public class FinancialReportService {
private final Logger log = LoggerFactory.getLogger(FinancialReportService.class);
#Inject
private FinancialReportDAO financialReportDAO;
public List<Findoc> getFinancialReports(){
return financialReportDAO.getFinancialReports();
}
}
Controller:
#GetMapping("/financial-reports")
#Timed
public List<Findoc> getFinancialReports() {
log.debug("REST request to get financial records");
return financialReportService.getFinancialReports(); // financialReportService is null
}
Update:
The application is generated by JHipster. Then the new service and DAO files were added to enable custom database queries to H2.
After #Injecting the service, you also need to set the field in the setup() method. Adding the below line should solve your problem
ReflectionTestUtils.setField(findocResource, "financialReportService", financialReportService);
On a separate note, the following part of the test looks weird. You are fetching the financial reports twice. This file is the FindocResourceIntTest, so I would remove any direct calls to financialReportService.
// Get all the financial-reports
restFindocMockMvc.perform(get("/api/financial-reports"))
.andExpect(status().isOk());
List<Findoc> finReports = financialReportService.getFinancialReports();

Get org.hibernate.LazyInitializationException in spring boot integration test

I'm trying to write integration test for Spring Boot application. I have Product and GalleryImage domain model. They are in one-to-many relationship.
public class Product {
...
#OneToMany(mappedBy = "product")
private List<GalleryImage> galleryImages;
}
I have a integration test as below:
#Test
public void testProductAndGalleryImageRelationShip() throws Exception {
Product product = productRepository.findOne(1L);
List<GalleryImage> galleryImages = product.getGalleryImages();
assertEquals(1, galleryImages.size());
}
However, this test gives me a LazyInitializationException. I searched on Google and StackOverFlow, it says that the session is closed after productRepository.findOne(1L), since galleryImages are lazily loaded, so galleryImages.size() gives me this exception.
I have tried to add a #Transactional annotation on the test, but it's still not working.
Hibernate Session has been closed after following line productRepository.findOne(1L).
You can try to do Hibernate.initialize(product.getGalleryImages())
public static void initialize(Object proxy)
throws HibernateException
Force initialization of a proxy or persistent collection.
Note: This only ensures intialization of a proxy object or collection; it is not guaranteed that the elements INSIDE the collection will be initialized/materialized.
To avoid Hibernate.initialize you can create a service.
#Service
#Transactional
public class ProductService {
#Transactional(readOnly = true)
public List<GalleryImage> getImages(final long producId) throws Exception {
Product product = productRepository.findOne(producId);
return product.getGalleryImages();
}
}
If you do use Spring Data JPA in you application then dynamic finder is a good alternative.
I had similar issues in the past and the solution was indeed adding the #Transactional annotation, but contrarily to what is proposed by #Anton M answer I believe that in this case we should annotate the test instead as proposed here.
It works both ways but the annotation should be placed if and where necessary, i.e. add it on the service if you need it there and not just for testing purposes.
For example, keep this as it was:
public class Product {
...
#OneToMany(mappedBy = "product")
private List<GalleryImage> galleryImages;
}
And add the annotation here:
#Test
#Transactional
public void testProductAndGalleryImageRelationShip() throws Exception {
Product product = productRepository.findOne(1L);
List<GalleryImage> galleryImages = product.getGalleryImages();
assertEquals(1, galleryImages.size());
}

Multiple SessionFactories' sessions together

I am trying to implement a sharded Hibernate logic. All Databases have same table called MyTable which is mapped to MyClass through Hibernate POJO.
public class SessionFactoryList {
List<SessionFactory> factories;
int minShard;
int maxShard;
// getters and setters here.
}
In my Dao implementation, I have a method getAll which is following -
public class MyClassDao {
#Autowired // through Spring
private SessionFactoryList list;
List<MyClass> getAll() {
List<MyClass> outputList = new ArrayList<>();
for(SessionFactory s : list.getFactories()) {
Criteria c = s.getCurrentSession.createCriteria(MyClass.class);
outputList.addAll(c.list());
}
return outputList;
}
Here is my test for the corresponding getAll implementation -
public class MyClassTest {
#Autowired
SessionFactoryList list;
#Autowired
MyClassDao myClassDao;
#Test
void getAllTest() {
Session session1 = list.getFactories.get(0).getCurrentSession();
session1.beginTransaction();
session1.save(new MyClass(// some parameters here));
Session session2 = list.getFactories.get(1).getCurrentSession();
session2.beginTransaction();
session2.save(new MyClass(// some parameters here));
//Set up done.
assert myClassDao.getAll().size() == 2
}
}
I am using HSQL in-memory database for the test cases.
I have verified that DB connections are correctly setup, but the Assert statement is failing.
'getAll' method of MyClassDao is returning 3 rows. MyClass object inserted in SessionFactory1's session is getting duplicated.
Is there anything I am missing out here?
I found it. The 2 sessionFactory configurations which I used for the test had same Database URL. Hence the same database was queried twice which caused the duplicates.

Categories