I'm trying to test a Spring Boot service that depends on both a repository and another service. I'm using TestContainers to verify that the service interacts correctly with the repository. However, I need to mock the other service in order to provide the test data that will be used. The service method in question needs to be transactional.
#Service
public class MyService {
private MyRepository myRepository; // This needs to be a real repository
private OtherService otherService; // This needs to be mocked
#Autowired
public MyService(MyRepository myRepository, OtherService otherService) {...}
#Transactional
public void methodToTest() {
// Get (mock) data from otherService, and interact with (real) myRepository
}
}
I can get it working in my test by manually creating the service with its dependencies. However, the resulting object is just a POJO that has not been enhanced by Spring to be Transactional:
#SpringBootTest
#ContextConfiguration(initializers = {TestContainersConfig.class})
public class MyServiceTest {
#Autowired
MyRepository myRepository;
OtherService otherService;
MyService myService;
#BeforeEach
void setup() {
otherService = Mockito.mock(OtherService.class);
// This works, except that the object is a POJO and so #Transactional doesn't work
myService = new MyService(myRepository, otherService);
}
#Test
#Transactional
void testMethodToTest() {
// Provide mock data
when(otherSerivce.something()).thenReturn(...);
// Run the test
myService.methodToTest();
// Would like to remove these lines: myService should already do it
TestTransaction.flagForCommit();
TestTransaction.end();
// assertions...
}
}
How can I get Spring to enhance the test MyService instance so that methodToTest will handle the transaction?
Related
I'm using spring boot rest API and want to test my service layer. In my service I have autowired few beans and its not through constructor. (I like it that way to keep it short).
In the junit, I have created mocks and for private field which I do want to execute, I have assigned using ReflectionTestUtils.setField. When I debug, the method inside the field is not getting executed which assigned by bean.
Service Class
#Component
public class MyService {
#Autowired
private MyRepository myRepository;
#Autowired
private ResponseMapper responseMapper;
public List<MyObj> getList(String param) throws MyException {
log.info("Getting details");
Optional<List<MyObj>> list = myRepository.findByParam(param);
List<MyObj> data = responseMapper.mapToResponseData(list);
return data;
}
}
Test Class
#RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
#InjectMocks
private MyService myService;
#Mock
private MyRepository myRepository;
#Mock
private ResponseMapper responseMapper;
#Before
public void setUp() {
ReflectionTestUtils.setField(myService, "responseMapper", responseMapper);
}
#Test
public void getListTest() throws Exception {
when(myRepository.findByParam(anyString()))
.thenReturn(Optional.of(getSampleList()));
List<MyObj> list = myService.getList("param");
assertTrue(list.size() >0);
}
}
This results in Assertion failure and when I debug, the method mapToResponseData in ResponseMapper is not getting executed.
I know I can mock mapToResponseData method also. But I wanted it to execute so I don't have to write another test class for mapper alone.
What am I doing wrong? Is this the right wat to inject bean? And is using constructor in service to inject beans only option?
I have classes Two levels to reach the EntityManager
Service Class
class MyServiceClass {
#Autowired
MyRepositoryInterfaceImpl repo;
void myMethod() {
repo.myMethod();
}
}
Repo Class
class MyRepositoryInterfaceImpl {
#Autowire
EntiryManager em;
void myMethod() {
em.getResultList();
}
}
My question I am able to mock for the class MyRepositoryInterfaceImpl with this code
#ExtendWith(MockitoExtention.class)
class MyRepositoryInterfaceImplTest {
#InjectMocks
MyRepositoryInterfaceImpl repo;
#Mock
EntiryManager em;
}
above code works fine
But I don't want to do that, why can't I directly write test code like this
#ExtendWith(MockitoExtention.class)
class MyServiceClassTest {
#InjectMocks
MyServiceClass service;
#InjectMocks
MyRepositoryInterfaceImpl repo;
#Mock
EntiryManager em;
}
and directly mock entity manager from this level ? and call service.myMethod() will it automatically propagate and trigger the entity manager mock ?
but it is not happening that way
This doesn't work because #InjectMock is not treated as #Mock.
From a UnitTest perspective it is desired to only test one thing/unit.
so you only need to mock the Repository and forget about the EntityManger. Instead use Mockito.when(repo.findAll()).thenReturn(Collections.emptyList()) for UnitTests.
When you want to test the integration between the Service- and RepositoryLayer. You can write an IntegrationTest.
My goal is to use an in-memory database for these unit tests, and those dependancies are listed as:
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
So that the repository instance actually interacts with a DB, and I dont just mock return values.
The problem is that when I run my unit test, the repository instance inside the service instance is null.
Why is that? Am I missing some annotation on the unit test class to initialise the repository instance?
This is the console output when running my unit test:
null
java.lang.NullPointerException
at com.my.MyService.findAll(MyService.java:20)
at com.my.MyTest.testMy(MyTest.java:23)
My unit test class:
public class MyTest {
#MockBean
MyRepository myRepository;
#Test
void testMy() {
MyService myService = new MyService();
int size = myService.findAll().size();
Assertions.assertEquals(0, size);
}
}
My service class:
#Service
public class MyService {
#Autowired
MyRepository myRepository;
public List<MyEntity> findAll() {
System.out.println(myRepository); // null
return (List<MyEntity>) myRepository.findAll(); // throws NullPointerException
}
#Transactional
public MyEntity create(MyEntity myEntity) {
myRepository.save(myEntity);
return myEntity;
}
}
My repository class:
#Repository
public interface MyRepository extends CrudRepository<MyEntity, Long> {
}
My entity class:
#Entity
public class MyEntity {
#Id
#GeneratedValue
public Long id;
}
Why is that? Am I missing some annotation on the unit test class to initialise the repository instance?
Basically yes :)
You need to initialise a Spring Context by Annotating your Testclass with #SpringBootTest
The other Problem you have is that you create your MyService Object manually.
By doing so SpringBoot has no chance to inject any Bean for you. You can fix this by simply injecting your MyService in your Testclass. Your Code should look something like this:
#SpringBootTest
public class MyTest {
#Autowired
private MyService myService;
#Test
void testMy() {
int size = myService.findAll().size();
assertEquals(0, size);
}
}
To use #MockBean annotation, you have to use SpringRunner to run the test. Use #RunWith Annotation on top of your test class and pass SpringRunner.class.
#RunWith(SpringRunner.class)
public class MyTest {
#MockBean
MyRepository myRepository;
#Test
void testMy() {
MyService myService = new MyService();
int size = myService.findAll().size();
Assertions.assertEquals(0, size);
}
}
The problem here is your service implementation. Using #Autowired to inject the dependency will work when you run the whole app, but it do not allow you to inject a custom dependency when you'll need it, and a good example of this is testing.
Change your service implementation to:
#Service
public class MyService {
private MyRepository myRepository;
public MyService(MyRepository myRepository){
this.myRepository = myRepository;
}
public List<MyEntity> findAll() {
System.out.println(myRepository); // null
return (List<MyEntity>) myRepository.findAll(); // throws NullPointerException
}
#Transactional
public MyEntity create(MyEntity myEntity) {
myRepository.save(myEntity);
return myEntity;
}
}
This constructor will be called by spring. Then change your test to:
public class MyTest {
#Mock
MyRepository myRepository;
#Test
void testMy() {
MyService myService = new MyService(myRepository);
int size = myService.findAll().size();
Assertions.assertEquals(0, size);
}
}
Note I have replaced #MockBean to #Mock as the previous annotation is for injecting a mock bean into the spring context, which is not needed if you're doing unit testing. If you want to boot spring context (which I would not recommend you) you need to configure your test class with #SpringBootTest or some of the other available alternatives. That will convert your test into an integration test.
PD: This test will not work if you don't provide a mock to myRepository.findAll(). Mockito default behaviour is to return null, but you're expecting it to return 0, so you'll need to do something like given(myRepository.findAll()).willReturn(0).
I believe you wish to write an integration test. Here you could remove the MockBean annotation and simply autowire your repository. Also, run with The SpringRunner class.
#RunWith(SpringRunner.class)
public class MyTest {
#Autowired
MyRepository myRepository;
#Autowired
MyService myService
#Test
void testMy() {
int size = myService.findAll().size();
Assertions.assertEquals(0, size);
}
}
This should work
I'm learning unit testing with Spring Boot. I've created a stub object from Spring Data repository class using Mockito. All works fine, here's a code:
#SpringBootTest
class UserServiceTest {
#MockBean
private UserRepository userRepository;
#BeforeAll
public void configureMock() {
User user = new User("mishenev",
"Dmitrii",
"Mishenev",
"123",
"mishenev.8#gmailcom",
new UserSettings());
when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));
when(userRepository.findUserByUserName(anyString())).thenReturn(user);
// .. Tests
}
But it's kind of boilerplate style to use this #BeforeAll test repository configuration for each class, if we don't need different stub behaviour. Then i tried to use #TestConfiguration
#TestConfiguration
public class SpringRepositoryTestConfiguration {
#Bean
public UserRepository userRepository () {
UserRepository userRepository = Mockito.mock(UserRepository.class);
// Configuring mock's behaviour
return userRepository;
}
}
After that i used
#SpringBootTest(classes = SpringRepositoryTestConfiguration.class)
But i can't using #MockBean to autowire repository in UserService now.
I want to understand is it possible to take out and use all test Repository type stubs in one configuration class. Thank you in advance!
Just use this
class TestConfig {
#Bean
UserRepository userRepository() {
final UserRepository repo = mock(UserRepository.class);
.. do mocking ..
return repo;
}
}
Then you can just #Import(TestConfig.class) where you need it
How to write JUnit Test cases for RestController, Service and DAO layer?
I've tried MockMvc
#RunWith(SpringRunner.class)
public class EmployeeControllerTest {
private MockMvc mockMvc;
private static List<Employee> employeeList;
#InjectMocks
EmployeeController employeeController;
#Mock
EmployeeRepository employeeRepository;
#Test
public void testGetAllEmployees() throws Exception {
Mockito.when(employeeRepository.findAll()).thenReturn(employeeList);
assertNotNull(employeeController.getAllEmployees());
mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/employees"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
How can I verify the CRUD methods inside the rest controller and other layers ?
You can use #RunWith(MockitoJUnitRunner.class) for unit testing with your Service Layer mocking your DAO Layer components. You don't need SpringRunner.class for it.
Complete source code
#RunWith(MockitoJUnitRunner.class)
public class GatewayServiceImplTest {
#Mock
private GatewayRepository gatewayRepository;
#InjectMocks
private GatewayServiceImpl gatewayService;
#Test
public void create() {
val gateway = GatewayFactory.create(10);
when(gatewayRepository.save(gateway)).thenReturn(gateway);
gatewayService.create(gateway);
}
}
You can use #DataJpaTest for integration testing with
your DAO Layer
#RunWith(SpringRunner.class)
#DataJpaTest
public class GatewayRepositoryIntegrationTest {
#Autowired
private TestEntityManager entityManager;
#Autowired
private GatewayRepository gatewayRepository;
// write test cases here
}
Check this article for getting more details about testing with Spring Boot