Is it possible to use dependency injection with unit tests using Spring Boot? For integration testing #SpringBootTest start the whole application context and container services. But is it possible to enable dependency injection functionality at unit test granularity?
Here's the example code
#ExtendWith(SpringExtension.class)
public class MyServiceTest {
#MockBean
private MyRepository repo;
#Autowired
private MyService service; // <-- this is null
#Test
void getData() {
MyEntity e1 = new MyEntity("hello");
MyEntity e2 = new MyEntity("world");
Mockito.when(repo.findAll()).thenReturn(Arrays.asList(e1, e2));
List<String> data = service.getData();
assertEquals(2, data.size());
}
}
#Service
public class MyService {
private final MyRepository repo; // <-- this is null
public MyService(MyRepository repo) {
this.repo = repo;
}
public List<String> getData() {
return repo.findAll().stream()
.map(MyEntity::getData)
.collect(Collectors.toList());
}
}
Or should I just manage the SUT (service class) as POJO and manually inject the mocked dependencies? I want to keep tests fast but minimize boilerplate code.
As #M.Deinum mentioned in the comments, unit tests shouldn't use dependency injection. Mock MyRepository and inject MyService using Mockito (and Junit5):
#ExtendWith(MockitoExtension.class)
public class MyServiceTest {
#InjectMocks
private MyService service;
#Mock
private MyRepository repo;
#Test
void getData() {
MyEntity e1 = new MyEntity("hello");
MyEntity e2 = new MyEntity("world");
Mockito.when(repo.findAll()).thenReturn(Arrays.asList(e1, e2));
List<String> data = service.getData();
assertEquals(2, data.size());
}
}
If you want to test the repository, use #DataJpaTest. From the docs:
Using this annotation will disable full auto-configuration and instead
apply only configuration relevant to JPA tests.
#DataJpaTest
public class MyRepositorTest {
#Autowired
// This is injected by #DataJpaTest as in-memory database
private MyRepository repo;
#Test
void testCount() {
repo.save(new MyEntity("hello"));
repo.save(new MyEntity("world"));
assertEquals(2, repo.count());
}
}
In conclusion, the suggested approach is to test the service layer mocking the repository layer with Mockito (or similar library) and to test the repository layer with #DataJpaTest.
You have not added the #Autowired in service for MyRepository
Service Class
#Service
public class MyService {
private final MyRepository repo; // <-- this is null
#Autowired
public MyService(MyRepository repo) {
this.repo = repo;
}
public List<String> getData() {
return repo.findAll().stream()
.map(MyEntity::getData)
.collect(Collectors.toList());
}
}
Service Test Class
#ExtendWith(MockitoExtension.class)
public class MyServiceTest {
#Mock
private MyRepository repo;
#InjectMocks
private MyService service;
#Test
void getData() {
MyEntity e1 = new MyEntity("hello");
MyEntity e2 = new MyEntity("world");
Mockito.when(repo.findAll()).thenReturn(Arrays.asList(e1, e2));
List<String> data = service.getData();
assertEquals(2, data.size());
}
}
Related
I am writing some unit tests and I am confused due to the different annotations in unit tests:
One of them is like:
#ExtendWith(SpringExtension.class)
class EmployeeServiceImplTest {
#MockBean
private EmployeeRepository employeeRepository;
#Autowired
private EmployeeServiceImpl employeeService;
#Test
void testFindAll() {
//...
}
//...
}
And another is using the following annotations:
#RunWith(MockitoJUnitRunner.class)
public class EmployeeServiceImplTest {
#Mock
private EmployeeRepository employeeRepository;
#InjectMocks
private EmployeeServiceImpl employeeService;
#Test
void testFindAll() {
//...
}
//...
}
So, I use Java and Spring Boot for language and framework. So, which annotations should I use?
#ExtendWith(SpringExtension.class) or #RunWith(MockitoJUnitRunner.class),
#MockBean or #Mock,
#Autowired or #InjectMocks
Any idea regarding to these annotations?
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
#Mapper(uses = SomeMapper.class,imports = Date.class)
public interface DomainModelMapper {
Model domainToModel(Domain domain);
#Mapping(target="dateUpdated", source="dateUpdated" ,defaultExpression = "java(Date.from(java.time.OffsetDateTime.now().toInstant()))")
#Mapping(target="id.key",source="id.key",defaultExpression = "java(com.datastax.driver.core.utils.UUIDs.timeBased())")
Domain modelToDomain(Model model);
}
I have a mapper class to do some Date conversions
public class SomeMapper {
public Date OffsetDateTimeToDate(OffsetDateTime offsetDateTime) {
return offsetDateTime != null ? Date.from(offsetDateTime.toInstant()):null;
}
public OffsetDateTime DateToOffsetDateTime(Date date) {
return date != null ? date.toInstant().atOffset(ZoneOffset.UTC) : null;
}
}
This is my service class where I use DomainModelMapper
#Service
public class SomeServiceImpl implements SomeService {
#Autowired
someRepository someRepository;
private final DomainModelMapper domainToModelMapper =
Mappers.getMapper(DomainModelMapper.class);
#Override
public Model saveSomething(Model model) {
return DomainModelMapper.domainToModel(someRepository
.save(DomainModelMapper.modelToDomain(model)));
}
How can I unit test saveSomething(Model model) method? How I can inject Mapstruct classes or mock them?
If you make the #Mapper interface as a Spring-based component model, then it can be autowired through #Autowired annotation. Read more at 4.2. Using dependency injection
#Mapper(uses = SomeMapper.class,imports = Date.class, componentModel = "spring")
public interface DomainModelMapper {
// IMPLEMENTATION
}
#Service
public class SomeServiceImpl implements SomeService {
#Autowired
SomeRepository someRepository;
#Autowired
DomainModelMapper domainModelMapper;
// THE REST OF THE IMPLEMENTATION
}
The testing becomes fairly easy since all the beans can be also injected in the #SpringBootTest with the #Autowired annotation.
The DomainModelMapper can be autowired and used in the unit test as is and rely on its implementation
The SomeRepository shall be either mocked using #MockBean which overwrites an existing bean or creates a new one if none of that type exists... or autowired as in the implementation if you use an in-memory database for the testing phase, such as H2.
In any case, the test class will be ready for testing.
#SpringBootTest
public class SomeServiceTest {
#Autowired // or #MockBean
SomeRepository someRepository;
#Autowired // no need to mock it
DomainModelMapper domainModelMapper;
#Test
public void test() {
// TEST
}
}
I am new in writing junit test for spring boot applications. Could anybody help me to understand the situation?
I have a service I’d like to test:
#Service
public class MyService {
private final JdbcTemplate jdbcTemplate;
…
#Autowired
public MyService(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
…
}
#Async
public SomeType myMethod(SomeDTO request) {
DataSource dataSource = this.jdbcTemplate.getDataSource();
…
}
…
}
When I start my application and call service through REST API then my dataSource is correct and based on parameters from application.yml.
However, when I call it from my unit tests then this.jdbcTemplate.getDataSource() is always null.
Here my test classes:
SpringBootTestApplication:
#RunWith(SpringRunner.class)
#SpringBootTest(
classes = TestConfigurator.class
)
public abstract class SpringBootTestApplication {
}
MyServiceTest:
public class MyServiceTest extends SpringBootTestApplication {
#MockBean
private JdbcTemplate jdbcTemplate;
#Autowired
#InjectMocks
private MyService myService;
#Test
public void Test_1(){
DataSource dataSource = this.jdbcTemplate.getDataSource();
myService.getSomething(dataSource, ...)
…
}
}
Should I add something special to my TestConfigurator.class?
public class MyServiceTest extends
SpringBootTestApplication {
#MockBean
private JdbcTemplate jdbcTemplate;
private MyService myService;
#Before
public void init(){
myService = new MyService(this.jdbcTemplate);
}
#Test
public void Test_1(){
DataSource datasource = new DataSource());
when.jdbcTemplate.getDatasource()).thenReturn(datasource);
myService.getSomething(dataSource, ...)
…
}
}
my application normally works fine, but when I run tests, or build application by maven, application is shutting down due tests with errors java.lang.NullPointerException. I debugged it and find out my that my beans in service layer are not Autowired and they are null.
Here is my class with tests:
public class CompanyServiceSimpleTest {
private CompanyService companyService;
#Before
public void setUp() {
companyService = new CompanyServiceImpl();
}
// Here is sample test
#Test
public void testNumberOfCompanies() {
Assert.assertEquals(2, companyService.findAll().size());
}
}
companyService is initialized, but beans in it not. Here is CompanyServiceImpl:
#Service
public class CompanyServiceImpl implements CompanyService {
#Autowired
private CompanyRepository companyRepository; // is null
#Autowired
private NotificationService notificationService; // is null
#Override
public List<CompanyDto> findAll() {
List<CompanyEntity> entities = companyRepository.find(0, Integer.MAX_VALUE);
return entities.stream().map(Translations.COMPANY_DOMAIN_TO_DTO).collect(Collectors.toList());
}
// ... some other functions
}
So when is called companyRepository.find() applications crashes. Here is repository class:
#Repository
#Profile("inMemory")
public class CompanyInMemoryRepository implements CompanyRepository {
private final List<CompanyEntity> DATA = new ArrayList<>();
private AtomicLong idGenerator = new AtomicLong(3);
#Override
public List<CompanyEntity> find(int offset, int limit) {
return DATA.subList(offset, Math.min(offset+limit, DATA.size()));
}
// ... some others functions
}
I have set up profile for that service but I had that VM options in Idea:
-Dspring.profiles.active=develpment,inMemory
So it should works.
To make autowiring work it has to be a Spring integration test. You have to anotate your test class with:
#RunWith(SpringJUnit4ClassRunner.class) and #ContextConfiguration(classes = {MyApplicationConfig.class})
If it is a Spring Boot app e.g.:
#RunWith(SpringJUnit4ClassRunner.class) and #SpringBootTest(classes = {MyApp.class, MyApplicationConfig.class}, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
More on this topic: http://www.baeldung.com/integration-testing-in-spring and http://www.baeldung.com/spring-boot-testing
You are not configuring Spring in your TestClasses, so it's impossible to inject anything...
Try configurin your class with
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations = { "path to your config xml" })
A little example:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations = {"classpath:config/applicationContext.xml"})
public class MyTest {
#Autowired
private MyClass myInjectedClass;
#Test
public void someTest() {
assertNotNull(myInjectedClass);
}
}