GridFsTemplate NullPointerException in Service Unit Test Class (Tech Stack: Spring Data / Spring Boot / Micro Service / Mongodb ) - spring

I am developing a spring boot app.
The service method uploads a PDF into a mongodb repo using GridFsTemplate which is autowired in the service.
This file upload service method works as expected via postman rest client.
But, When I tried running a unit test; calling the same service method, the SpringData GridFsTemplate is not initialised (In MongoDB, you can use GridFS to store binary files). This results in the org.springframework.data.mongodb.gridfs.GridFsTemplate.store(...) throwing a NullPointerException.
Please, can you help, I have been stuck in this for a days.
Below is my service implementation:
#Service
public final class UploadServiceImpl implements UploadService {
#Autowired
private SequenceRepository sequenceDao;
#Autowired (required = true)
private GridFsTemplate gridFsTemplate;
#Override
public Long uploadFile(Invoice uploadedInvoice) {
ByteArrayInputStream byteArrayInputStream = null;
if (checkContentType(invoiceInfo.getContentType())) {
invoiceInfo.setPaymentID(sequenceDao.getNextSequenceId(INVOICE_UPLOAD_SEQ_KEY));
byteArrayInputStream = new ByteArrayInputStream(uploadedInvoice.getFileContent());
//Error thrown is java.lang.NullPointerException: null, where gridFsTemplate is null and basically autowire does not work when test is run.
GridFSFile gridFSUploadedFile= gridFsTemplate.store(byteArrayInputStream, invoiceInfo.getFileName(), invoiceInfo.getContentType(), invoiceInfo);
return 1l;
} else {
return 2l;
}
}
### Below is my Unit Test class for the service
#RunWith(MockitoJUnitRunner.class)
#ContextConfiguration
public class UploadServiceTest {
#Mock
private SequenceRepository sequenceRepositoryMock;
#Autowired
private GridFsTemplate gridFsTemplateMock;
#Mock
private Invoice invoiceMock;
#InjectMocks
private static UploadService uploadService = new UploadServiceImpl();
DBObject fileMetaData = null;
DB db = null;
Jongo jongo = null;
#Before
public void setUp() throws Exception {
db = new Fongo("Test").getDB("Database");
jongo = new Jongo(db);
}
#Test
public void testUploadFile() {
//test 1
Long mockPaymentNo = new Long(1);
Mockito.when(sequenceRepositoryMock.getNextSequenceId(INVOICE_SEQUENCE)).thenReturn(mockPaymentNo);
assertEquals(mockPaymentNo, (Long) sequenceRepositoryMock.getNextSequenceId(INVOICE_SEQUENCE));
//test 2
Invoice dummyInvoice = getDummyInvoice();
InvoiceInfo dummyInvoiceInfo = dummyInvoice.getInvoiceInfo();
MongoCollection invoicesCollection = jongo.getCollection("invoices");
assertNotNull(invoicesCollection.save(dummyInvoiceInfo));
assertEquals(1, invoicesCollection.save(dummyInvoiceInfo).getN());
System.out.println("TEST 2 >>>>>>>>>>>>>>>>>> "+ uploadService);
//test 3 : The following line is the cause of the exception, the service method is called but the GridFsTemplate is not initialized when the test is run. But it works when the endpoint is invoked via postman
uploadService.uploadFile(dummyInvoice);
System.out.println("TEST 3 >>>>>>>>>>>>>>>>>> ");
}
}

The problem is because you use #InjectMocks to autowire your UploadService.
And UploadService autowire two other beans SequenceRepository and GridFsTemplate.
If you’re doing TDD or not (and we are able to change the test first)
– clients of this code don’t know about an additional dependency,
because it’s completely hidden
The Javadoc states:
Mockito will try to inject mocks only either by constructor injection,
setter injection, or property injection in order and as described
below. If any of the following strategy fail, then Mockito won’t
report failure; i.e. you will have to provide dependencies yourself.
The solution is to use UploadServiceImpl constructor to autowire beans:
#Service
public final class UploadServiceImpl implements UploadService {
private final SequenceRepository sequenceDao;
private final GridFsTemplate gridFsTemplate;
private final PlannerClient plannerClient;
#Autowired
public PlannerServiceImpl(PlannerClient plannerClient, GridFsTemplate gridFsTemplate, SequenceRepository sequenceDao) {
this.plannerClient = plannerClient;
}
...
}
When there are more dependencies needed, they’re clearly in sight because they are initialized in constructor
More detailed:
https://tedvinke.wordpress.com/2014/02/13/mockito-why-you-should-not-use-injectmocks-annotation-to-autowire-fields/

Related

Passing an external property to JUnit's extension class

My Spring Boot project uses JUnit 5. I'd like to setup an integration test which requires a local SMTP server to be started, so I implemented a custom extension:
public class SmtpServerExtension implements BeforeAllCallback, AfterAllCallback {
private GreenMail smtpServer;
private final int port;
public SmtpServerExtension(int port) {
this.port = port;
}
#Override
public void beforeAll(ExtensionContext extensionContext) {
smtpServer = new GreenMail(new ServerSetup(port, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
}
#Override
public void afterAll(ExtensionContext extensionContext) {
smtpServer.stop();
}
}
Because I need to configure the server's port I register the extension in the test class like this:
#SpringBootTest
#AutoConfigureMockMvc
#ExtendWith(SpringExtension.class)
#ActiveProfiles("test")
public class EmailControllerIT {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Value("${spring.mail.port}")
private int smtpPort;
#RegisterExtension
// How can I use the smtpPort annotated with #Value?
static SmtpServerExtension smtpServerExtension = new SmtpServerExtension(2525);
private static final String RESOURCE_PATH = "/mail";
#Test
public void whenValidInput_thenReturns200() throws Exception {
mockMvc.perform(post(RESOURCE_PATH)
.contentType(APPLICATION_JSON)
.content("some content")
).andExpect(status().isOk());
}
}
While this is basically working: How can I use the smtpPort annotated with #Value (which is read from the test profile)?
Update 1
Following your proposal I created a custom TestExecutionListener.
public class CustomTestExecutionListener implements TestExecutionListener {
#Value("${spring.mail.port}")
private int smtpPort;
private GreenMail smtpServer;
#Override
public void beforeTestClass(TestContext testContext) {
smtpServer = new GreenMail(new ServerSetup(smtpPort, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
};
#Override
public void afterTestClass(TestContext testContext) {
smtpServer.stop();
}
}
The listener is registered like this:
#TestExecutionListeners(value = CustomTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
When running the test the listener gets called but smtpPort is always 0, so it seems as if the #Value annotation is not picked up.
I don't think you should work with Extensions here, or in general, any "raw-level" JUnit stuff (like lifecycle methods), because you won't be able to access the application context from them, won't be able to execute any custom logic on beans and so forth.
Instead, take a look at Spring's test execution listeners abstraction
With this approach, GreenMail will become a bean managed by spring (probably in a special configuration that will be loaded only in tests) but since it becomes a bean it will be able to load the property values and use #Value annotation.
In the test execution listener you'll start the server before the test and stop after the test (or the whole test class if you need that - it has "hooks" for that).
One side note, make sure you mergeMode = MergeMode.MERGE_WITH_DEFAULTS as a parameter to #TestExecutionListeners annotation, otherwise some default behaviour (like autowiring in tests, dirty context if you have it, etc) won't work.
Update 1
Following Update 1 in the question. This won't work because the listener itself is not a spring bean, hence you can't autowire or use #Value annotation in the listener itself.
You can try to follow this SO thread that might be helpful, however originally I meant something different:
Make a GreenMail a bean by itself:
#Configuration
// since you're using #SpringBootTest annotation - it will load properties from src/test/reources/application.properties so you can put spring.mail.port=1234 there
public class MyTestMailConfig {
#Bean
public GreenMail greenMail(#Value(${"spring.mail.port"} int port) {
return new GreenMail(port, ...);
}
}
Now this configuration can be placed in src/test/java/<sub-package-of-main-app>/ so that in production it won't be loaded at all
Now the test execution listener could be used only for running starting / stopping the GreenMail server (as I understood you want to start it before the test and stop after the test, otherwise you don't need these listeners at all :) )
public class CustomTestExecutionListener implements TestExecutionListener {
#Override
public void beforeTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.start();
}
#Override
public void afterTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.stop();
}
}
Another option is autowiring the GreenMail bean and using #BeforeEach and #AfterEach methods of JUnit, but in this case you'll have to duplicate this logic in different Test classes that require this behavour. Listeners allow reusing the code.

Spring Boot #Retryable Mock Test

I have a code block like below:
#Service
ExecutorService {
#Autowired
IAccountService accountService;
#Retryable(maxAttempts = 3, value = {DataIntegrityViolationException.class}, backoff = #Backoff(delay = 1000, multiplier = 2))
public void execute(RequestDto reqDto)
{
Account acc = accountService.getAccount(reqDto.getAccountId);
...
}
}
In Mockito test, I just want to see call method 3 times as expected.
#RunWith(SpringRunner.class)
public class CampaignExecuterServiceTest
{
private static final Long ACCOUNT_ID = 1L;
#InjectMocks
private ExecutorService executorService;
#Mock
private IAccountService accountService;
#Test
public void execute_success()
{
Account account = new Account(ACCOUNT_ID, null, null, null, null);
RequestDto reqDto = new RequestDto();
when(accountService.getAccount(any())).thenThrow(DataIntegrityViolationException.class);
executorService.execute(reqDto);
verify(executorService, times(3)).execute(any());
}
}
Test just throws an exception. But I expected it to call it 3 times.
There are a few problems here.
1) #SpringBootTest is need to create a Spring Boot app runner that will intercept your retryable method. JUnit won't do that on its own. Also, the classes param needs to be your main class in place of MainApplication or a subset of classes that can run as a Spring boot app.
2) ExecutorService must be annotated with #Autowired so it will be the bean from the Spring boot application the test is creating.
3) IAccountService must be a #MockBean that is how the test Spring boot environment will know to use a mocked class in ExecutorService instead of the real bean.
4) In the test the third mock call needs to return a valid result or it will throw an exception and the test will fail. Alternately, you could catch the exception in the test.
5) ExecutorService is not a mock or spy so verify will not accept it as an arg at runtime, but accountService is a mock so just assert it is called 3 times.
Another note is that somewhere in the Spring boot config or on the MainApplication you must have the #EnableRetry annotation.
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = MainApplication.class)
public class CampaignExecuterServiceTest {
private static final Long ACCOUNT_ID = 1L;
#Autowired
private ExecutorService executorService;
#MockBean
private IAccountService accountService;
#Test
public void execute_success() {
Account account = new Account(ACCOUNT_ID, null, null, null, null);;
RequestDto reqDto = new RequestDto();
when(accountService.getAccount(any()))
.thenThrow(DataIntegrityViolationException.class)
.thenThrow(DataIntegrityViolationException.class)
.thenReturn(account);
executorService.execute(reqDto);
verify(accountService, times(3)).getAccount(any());
}
}

Geting java.lang.IllegalStateException: Duplicate mock definition while using #MockBean in test case

I have one service class that I want to mock but while running the test I am Getting Caused by: java.lang.IllegalStateException: Duplicate mock definition [MockDefinition#482ba4b1 name = '', typeToMock = com.service.ThirdPartyService, extraInterfaces = set[[empty]], answer = RETURNS_DEFAULTS, serializable = false, reset = AFTER]
I have tried to create mock service using #MockBean at class level, field level, and used #Qualifier as well to resolve the issue
#Service
public class ThirdPartyService{
.......................
public String decrypt(String encryptedText) {
//third party SDK I am using
return Service.decrypt.apply(encryptedText);
}
.........
..............
}
#ComponentScan("com")
#PropertySource({"classpath:/api.properties", "classpath:/common.properties"})
#SpringBootConfiguration
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
#Transactional
public class TestControllerTest extends IntegrationTest {
#MockBean
ThirdPartyService thirdPartyService;
#Before
public void initMocks(){
MockitoAnnotations.initMocks(this);
}
#Test
public void test() throws Exception {
when(ts.decrypt("encryptedText")).thenReturn("decryptedText")
Request req = Request.builder().name("name123").build();
//written performPost method in some other class
ResultActions action = performPost("/test", req);
action.andExpect(status().isOk());
}
}
public class IntegrationTest {
protected final Gson mapper = new Gson();
private MockMvc mvc;
#Autowired
private WebApplicationContext context;
public ObjectMapper objectMapper = new ObjectMapper();
#Before
public void setup() {
this.mvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
}}
When I am calling Thirdparty service decrypt method then it should return me decryptedText as a string. But getting duplicate mock definition error
I had the same issue.
Cause of this were test configuration file which was put somewhere else and it contained the mocked bean.
I have solved this by using #Autowired instead of #MockBean as this will result in autowiring the already mocked bean.
In my case the problem appeared after another dependency update and the reason was in the #SpringBootTest annotation referencing the same class twice:
#SpringBootTest(classes = {MyApplication.class, ApiControllerIT.class})
class ApiControllerIT extends IntegrationTestConfigurer {
// ...
}
#SpringBootTest(classes = {MyApplication.class, TestRestTemplateConfiguration.class})
public class IntegrationTestConfigurer {
// ...
}
I fixed it by removing #SpringBootTest annotation from the child class (ApiControllerIT).
In my case it was incorrect test class name that doesn't end with 'Test'.
If you have nested test classes try this:
#NestedTestConfiguration(OVERRIDE)
From Spring release notes: https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-5.x#upgrading-to-version-53

CrudRepository test cases without inserting data in DB

I have one repository class which which implements CrudRepository. Then in service class I have auto wired this repositary. Then in controller class I have autowired this service.
I want to write test cases of controller Class. I am using below configuration.
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class XYZControllerTest {
MockMvc mockMvc;
#Mock
private XYZController xyzController;
#Autowired
private TestRestTemplate template;
#Autowired
XYZRepository xyzRepository;
#Before
public void setup() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(xyzController).build();
}
#Test
public void testPanelShouldBeRegistered() throws Exception {
HttpEntity<Object> xyz = getHttpEntity("{\"name\": \"test 1\", \"email\": \"test10000000000001#gmail.com\","
+ " \"registrationNumber\": \"41DCT\",\"registrationDate\":\"2018-08-08T12:12:12\" }");
ResponseEntity<XYZ> response = template.postForEntity("/api/xyz", xyz, XYZ.class);
}
}
My problem is that when I run test case, data is going to insert in DB which is used for application. Can I test it without inserting data in DB.
Conceptually when we are testing services we mock repositories instead of injection.
You need to mock your repository and setup behavior for returning data.
An example :
#MockBean
XYZRepository xyzRepository;
#Test
public void test() {
// other mocks
//
when(xyzRepository.findAll()).thenReturn(Arrays.asList(new XYZ()));
// service calls
// assertions
}

Mock Spring Data Rest Repository

I faced with problem while testing data rest repositories. I call rest resource and check if it gets me proper json. But for pre-population data I don't want to use in memory db, so I mocked repository method invocation.
#MockBean
private CommentRepository commentRepository;
and did this
given(commentRepository.findOne(1L)).willReturn(comment);
And now, while calling "/comments/1" I get 404 error, so data rest didn't expose my repository. The main question is "How can we mock repository method for getting data from database?"
My test class:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CommentTest
{
#Autowired
private TestRestTemplate restTemplate;
#MockBean
private CommentRepository commentRepository;
#Before
public void setup()
{
Comment comment = new Comment();
comment.setText("description");
comment.setCommentId(1L);
given(commentRepository.findOne(1L)).willReturn(comment);
}
#Test
public void shouldCheckCommentGetResource()
{
ParameterizedTypeReference<Resource<Comment>> responseType = new ParameterizedTypeReference<Resource<Comment>>() {};
ResponseEntity<Resource<Comment>> responseEntity =
restTemplate.exchange("/comments/1", HttpMethod.GET, null, responseType, Collections
.emptyMap());
Comment actualResult = responseEntity.getBody().getContent();
assertEquals("description", actualResult.getText());
// more assertions
}
}
As I understand, using MockBean annotation I replace current repository bean and it will not be exposed by data rest, Do we have any way to pre-populate data into db or mock invocation of repository method?
I do not think this is possible. Spring data registers the repository beans using a FactoryBean - in the spring-data-rest case this is EntityRepositoryFactoryBean. So you cannot just override these beans with a mock.
For an interesting read on why mocking spring data repositories is not useful see this answer https://stackoverflow.com/a/23442457/5371736
In the same thread there is a reference to a project introducing mock support for spring-data repositories - https://stackoverflow.com/a/28643025/5371736
There is quick and dirty way to mock Spring Data Rest repositories with mockito, just in case somebody have no other options, but try to avoid this unless absolutely necessary
class MockRestRepositoryUtil {
public static <T> T mockRepository(Class<T> repositoryClass,
T springRepository) throws Exception {
Object springRepositoryImpl = AopTestUtils.getTargetObject(springRepository);
T mockRepository = mock(repositoryClass, delegatesTo(springRepositoryImpl));
Object springProxyHandler = Proxy.getInvocationHandler(springRepository);
ProxyFactory proxyFactory = extractProxyFactory(springProxyHandler);
proxyFactory.setTarget(mockRepository);
removeSpringDataAspects(proxyFactory);
return mockRepository;
}
private static ProxyFactory extractProxyFactory(Object springProxyHandler) throws Exception {
Field advisedField = springProxyHandler.getClass().getDeclaredField("advised");
advisedField.setAccessible(true);
return (ProxyFactory) advisedField.get(springProxyHandler);
}
private static void removeSpringDataAspects(ProxyFactory proxyFactory) {
Advisor[] advisors = proxyFactory.getAdvisors();
Arrays.stream(advisors)
.filter(advisor -> advisor.getAdvice().getClass().getPackage().getName()
.contains("org.springframework.data"))
.collect(toImmutableList())
.forEach(proxyFactory::removeAdvisor);
}
}

Resources