Arpan Banerjee
Arpan Banerjee

Reputation: 1016

SpringBoot Controller test using Mockito & jUnit5 failing as Spring is unable to create bean of PropertyService class which loads properties?

I have a simple spring boot project--

Here is the project structure-

enter image description here

If I run my spring boot application, it runs fine without any errors. I was able to get all the customers, get a single customer, delete a customer and add a customer through my rest controller methods.

Through Postman I am able to add customers--

<Customer>
<firstName>TestData</firstName>
<lastName>Test</lastName>
<gender>M</gender>
<date>2020-01-26T09:00:00.000+0000</date>
<authId>6AE-BH3-24F-67FG-76G-345G-AGF6H</authId>
<addressdto>
<city>Test City</city>
<country>Test Country</country>
</addressdto>
</Customer>

Response

Customer with 34 sucessfully added

This means while the application is up, it is able to instantiate PropertyService.java. thus I am able to access an authentication id which is present in my application-dev.properties through PropertyService.java. The same property is present in my src/test/resources-> application.properties.

There are two problems--

  1. Now when I run my HomeControllerTest.java class asjUnit test , I am getting a error. I debugged and found out the root cause of the error. Inside my HomeController.java class , it is unable to instantiate the PropertyService.java class, so i am getting a null pointer exception there.Thus the further execution of test class failed.
  2. I am unable to access the authId through PropertyService.java in my test class., so I had to hardcode.

Can anyone tell me why I am getting this issue? And how do I fix it?

HomeController.java

@PostMapping("/customer")
    public ResponseEntity<String> addCustomer(@RequestBody CustomerDto customerDto) {
        String message = "";
        ResponseEntity<String> finalMessage = null;
        try {
            if ((!customerDto.getAuthId().equals(propertyService.getKeytoAddCustomer()))) {
                System.out.println("If check failed: "+propertyService.getKeytoAddCustomer());
                System.out.println("Unauthorized access attempted");
                message = "Unauthorized access attempted";
                finalMessage = new ResponseEntity<>(message, HttpStatus.UNAUTHORIZED);
            }
            System.out.println("If check passed :"+propertyService.getKeytoAddCustomer());

            Customer customer = mapper.mapToEntity(customerDto);
            customerService.addCustomer(customer);
            message = "Customer with " + customer.getId() + " sucessfully added";
            finalMessage = new ResponseEntity<>(message, HttpStatus.OK);

        } catch (Exception e) {
            message = "Failed to add customer due to " + e.getMessage();
            finalMessage = new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return finalMessage;
    }

PS- equals(propertyService.getKeytoAddCustomer())) (Problem 1) --> here I am getting the null pointer exception

PropertyService.java

package com.spring.liquibase.demo.utility;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource("classpath:config.properties")
public class PropertyService {
    @Autowired
    private Environment env;

    public String getKeytoAddCustomer() {
        return env.getProperty("auth.key.to.add.customer");
    }
}

HomeControllerTest.java


@ExtendWith(SpringExtension.class)
class HomeControllerTest {
    private MockMvc mvc;

    @InjectMocks
    private HomeController homeController;

    @MockBean
    private CustomerService customerService;
//
//  @Autowired
//  private PropertyService propertyService;

    @BeforeEach
    public void setup() {
        mvc = MockMvcBuilders.standaloneSetup(homeController).build();
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testaddCustomer() throws Exception {
        String uri = "/customer";
        CustomerDto custDto = this.mockCustomerObject();
        String actualResult = mvc
                .perform(MockMvcRequestBuilders.post(uri).contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(custDto)))
                .andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse().getContentAsString();
        Assertions.assertEquals(actualResult, "Customer with " + custDto.getId() + " sucessfully added");
    }

    private CustomerDto mockCustomerObject() {
        CustomerDto cusDto = new CustomerDto();
        AddressDto addressDto = new AddressDto();
        addressDto.setCity("BBSR");
        addressDto.setCountry("INDIA");
        cusDto.setDate(new Date());
        cusDto.setFirstName("Biprojeet");
        cusDto.setLastName("KAR");
        cusDto.setGender("M");
        cusDto.setAuthId(" 6AE-BH3-24F-67FG-76G-345G-AGF6H");
        cusDto.setAddressdto(addressDto);
        return cusDto;

    }

    public static String asJsonString(CustomerDto cusDto) {
        try {
            return new ObjectMapper().writeValueAsString(cusDto);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

PS- I have commented out the codes as I am unable to access the prop file here.Need help here as well(Problem 2)

application.properties-- inside src/test/resources

# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url=jdbc:mysql******useSSL=false
spring.datasource.username=****
spring.datasource.password=****

# Hibernate
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
logging.level.org.springframework.web=INFO
logging.level.com=DEBUG

customer.auth.key = 6AE-BH3-24F-67FG-76G-345G-AGF6H

application-dev.properties

same as above

application.properties inside->src/main/resources

spring.profiles.active=dev
logging.level.org.springframework.web=INFO
logging.level.com=DEBUG
server.port=8080

jUnit Error Log

java.lang.AssertionError: Status expected:<200> but was:<500>
    at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
    at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
    at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:627)
    at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:196)
    at com.spring.liquibase.demo.controller.HomeControllerTest.testaddCustomer(HomeControllerTest.java:50)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.base/java.lang.reflect.Method.invoke(Unknown Source)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:436)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:170)
    at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:166)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:113)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:58)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:112)
    at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
    at java.base/java.util.stream.ReferencePipeline$2$1.accept(Unknown Source)
    at java.base/java.util.Iterator.forEachRemaining(Unknown Source)
    at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Unknown Source)
    at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
    at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
    at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
    at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
    at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
    at java.base/java.util.stream.ReferencePipeline$2$1.accept(Unknown Source)
    at java.base/java.util.Iterator.forEachRemaining(Unknown Source)
    at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Unknown Source)
    at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
    at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
    at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
    at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
    at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:55)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:43)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:154)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:90)
    at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:89)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)

Upvotes: 4

Views: 7129

Answers (3)

Anurag Srivastava
Anurag Srivastava

Reputation: 129

And there is one more error. This is a major one-- my test case passes even if i give a wrong authId . Please try in your local and let me know if u r having the same behaviour.

Off-course your test will be passed because your code is only checking the conditions it's not preventing the code to get executed, Please refer your code below:

try {
        if ((!customerDto.getAuthId().equals(propertyService.getKeytoAddCustomer()))) {
            System.out.println("If check failed: "+propertyService.getKeytoAddCustomer());
            System.out.println("Unauthorized access attempted");
            message = "Unauthorized access attempted";
            finalMessage = new ResponseEntity<>(message, HttpStatus.UNAUTHORIZED);
        }
        System.out.println("If check passed :"+propertyService.getKeytoAddCustomer());

        Customer customer = mapper.mapToEntity(customerDto);
        customerService.addCustomer(customer);
        message = "Customer with " + customer.getId() + " sucessfully added";
        finalMessage = new ResponseEntity<>(message, HttpStatus.OK);

    }

Over here under if condition you are only executing the block of codes and then instructed to execute the block of code which is outside the if condition.

So under if condition it's doing nothing. So you have to improve your code as per your expected behaviour.

If you would like to prevent your code execution then please refer the code below

try {
        if ((!customerDto.getAuthId().equals(propertyService.getKeytoAddCustomer()))) {
            System.out.println("If check failed: "+propertyService.getKeytoAddCustomer());
            System.out.println("Unauthorized access attempted");
            message = "Unauthorized access attempted";
            return new ResponseEntity<>(message, HttpStatus.UNAUTHORIZED);
        }
        System.out.println("If check passed :"+propertyService.getKeytoAddCustomer());

        Customer customer = mapper.mapToEntity(customerDto);
        customerService.addCustomer(customer);
        message = "Customer with " + customer.getId() + " sucessfully added";
        finalMessage = new ResponseEntity<>(message, HttpStatus.OK);

    } catch (Exception e) {
        message = "Failed to add customer due to " + e.getMessage();
        e.printStackTrace();
        finalMessage = new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR);
    }

Here I have set the return statement under If condition.

But If you want to fail the current test case where you comparing the message then please refer the code below:

try {
        if ((!customerDto.getAuthId().equals(propertyService.getKeytoAddCustomer()))) {
            System.out.println("If check failed: "+propertyService.getKeytoAddCustomer());
            System.out.println("Unauthorized access attempted");
            message = "Unauthorized access attempted";
            finalMessage = new ResponseEntity<>(message, HttpStatus.UNAUTHORIZED);
        }else {
            System.out.println("If check passed :" + propertyService.getKeytoAddCustomer());

            Customer customer = mapper.mapToEntity(customerDto);
            customerService.addCustomer(customer);
            message = "Customer with " + customer.getId() + " sucessfully added";
            finalMessage = new ResponseEntity<>(message, HttpStatus.OK);
        }

    } catch (Exception e) {
        message = "Failed to add customer due to " + e.getMessage();
        e.printStackTrace();
        finalMessage = new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR);
    }

Here you just need to set the block of code which is outside the if condition under the else part.

Upvotes: 1

Anurag Srivastava
Anurag Srivastava

Reputation: 129

After gone through with your repo here is the final code

@WebMvcTest(HomeController.class)
class HomeControllerTest {

@Autowired
private MockMvc mvc;

@MockBean
private CustomerService customerService;

@MockBean
private PropertyService propertyService;

@MockBean
private EntityToDtoMapper mapper;


@Test
public void testaddCustomer() throws Exception {
    String uri = "/customer";
    CustomerDto custDto = this.mockCustomerObject();
    Customer customer = getCustomerEntity();
    Mockito.when(mapper.mapToEntity(Mockito.any(CustomerDto.class))).thenReturn(customer);
    String actualResult = mvc
            .perform(MockMvcRequestBuilders.post(uri).contentType(MediaType.APPLICATION_JSON)
                    .content(asJsonString(custDto)))
            .andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse().getContentAsString();
    Assertions.assertEquals(actualResult, "Customer with " + custDto.getId() + " sucessfully added");
}

private CustomerDto mockCustomerObject() {
    CustomerDto cusDto = new CustomerDto();
    AddressDto addressDto = new AddressDto();
    addressDto.setCity("BBSR");
    addressDto.setCountry("INDIA");
    cusDto.setDate(new Date());
    cusDto.setFirstName("Biprojeet");
    cusDto.setLastName("KAR");
    cusDto.setGender("M");
    cusDto.setAuthId(" 6AE-BH3-24F-67FG-76G-345G-AGF6H");
    cusDto.setAddressdto(addressDto);
    return cusDto;

}


private Customer getCustomerEntity() {
    Customer customer = new Customer();
    Address address = new Address();
    address.setCity("BBSR");
    address.setCountry("INDIA");
    customer.setDate(new Date());
    customer.setFirstName("Biprojeet");
    customer.setLastName("KAR");
    customer.setGender("M");
    customer.setAddress(address);
    return customer;

}

public static String asJsonString(CustomerDto cusDto) {
    try {
        return new ObjectMapper().writeValueAsString(cusDto);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

}

The issue over here that you are mixing the concepts. In your implementation you were trying to do unit testing but was expecting the behaviour of integration.

As you been using Spring boot with it's test starter kit which comes with the dependencies of framework like JUnit and Mockito, You can easily mock those classes and methods which throwing the Null pointer exception by using mockitio framework because server is not running and IOC container is not up that's why those are NULL.

So in your code CustomerService, PropertyService and EntityToDtoMapper was NULL.

So question over here is how we can upload the spring application context without starting the server.

It can be done by two ways either load the whole spring application context by using @SpringBootTest and @AutoConfigureMockMvc annotations.

Or we can narrow the spring application context only for the controller itself by using @WebMvcTest annotation

So the solution which I used over here is narrow the test to the controller only by using @WebMvcTest(HomeController.class) annotation.

But still those CustomerService, PropertyService and EntityToDtoMapper are NULL. So to mock those classes We can use either @Mock or @MockBean annotation, but there is a slight difference between those annotation

The Mockito.mock() method allows us to create a mock object of a class or an interface and the @MockBean to add mock objects to the Spring application context. The mock will replace any existing bean of the same type in the application context.

So as we have uploaded the spring application context for the controller so the controller is expecting those beans in the application context as well, which can be achieved by @MockBean annotation.

After mocking all those beans your controller bean will be created but there are the methods where you expecting some return values so you have to code the expected return value in your code which could be done like that

Mockito.when(mapper.mapToEntity(Mockito.any(CustomerDto.class))).thenReturn(customer);

if you miss this particular step then in the controller you will get an NULL pointer exception on this line of code

message = "Customer with " + customer.getId() + " sucessfully added";

Because you code

Customer customer = mapper.mapToEntity(customerDto);

will return NULL.

I hope this will help and motivate you to get more knowledge on those concepts.

Please let me know if any further help is required

Upvotes: 4

Tony
Tony

Reputation: 466

Try mocking the PropertyService with either @MockBean or @Mock.

I notice you're missing @WebMvcTest(Controller.class) above the class definition which you'll need for unit testing Mvc Controllers. Explanation

If @MockBean doesn't work.

Try:

Using Mockito.when(), you can simply return your desired/expected result and when the desired method is called.

Use Mockito.verify() to ensure the desire when is executed.

when(propertyService.getKeytoAddCustomer()).thenReturn("Desired String");

when(customerService.addCustomer(customerObject)).thenReturn("Desired result");

//DO mvc.perform(...);

verify(propertyService).getKeytoAddCustomer();

verify(customerService).addCustomer(customerObject());

Problem Two

The issue with the property file I assume is because you're using spring.profile.active=dev but property file is test/resources is application.properties instead of application-dev.properties despite that it is the only property file in test/resources. Rename the files exactly the same in both resources folder and see what happens.

Upvotes: 0

Related Questions