Spring使用 Mockito 和 jUnit5 的启动控制器测试失败,因为 Spring 无法创建加载属性的 PropertyService class 的 bean?

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

我有一个简单的spring启动项目--

这是项目结构-

如果我 运行 我的 spring 启动应用程序,它 运行 没问题,没有任何错误。我能够通过我的其余控制器方法获取所有客户、获取单个客户、删除客户和添加客户。

通过Postman我可以添加客户--

<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>

回应

Customer with 34 sucessfully added

这意味着当应用程序启动时,它能够实例化 PropertyService.java。 因此我可以通过 PropertyService.java 访问我的 application-dev.properties 中存在的身份验证 ID。同样的 属性 出现在我的 src/test/resources-> application.properties.

有两个问题--

  1. 现在当我 运行 我的 HomeControllerTest.java class 为 jUnit test 时,我 我收到一个错误。我调试并找出了根本原因 错误。在我的 HomeController.java class 里面,它无法 实例化 PropertyService.java class,所以我得到一个 null pointer exception there.Thus 进一步执行测试 class 失败。
  2. 我在测试class中无法通过PropertyService.java访问authId,所以我不得不硬编码。

谁能告诉我为什么会遇到这个问题?我该如何解决?

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()))(问题 1)--> 这里我得到 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- 我已经注释掉了代码,因为我无法访问道具文件 here.Need 在这里也有帮助(问题 2)

application.properties--里面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

应用-dev.properties

same as above

application.properties里面->src/main/resources

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

jUnit 错误日志

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(StatusResultMatchers.java:627)
    at org.springframework.test.web.servlet.MockMvc.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(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(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(HierarchicalTestExecutor.java:120)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
    at java.base/java.util.stream.ReferencePipeline.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(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(HierarchicalTestExecutor.java:120)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
    at java.base/java.util.stream.ReferencePipeline.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(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)

完成你的回购后,这里是最终代码

@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);
    }
}

}

这里的问题是你混淆了概念。在您的实施中,您尝试进行单元测试但期望集成行为。

由于您一直在使用 Spring 启动及其测试入门工具包,该工具包附带 JUnit 和 Mockito 等框架的依赖项,您可以轻松地模拟那些 类 和抛出空指针异常的方法通过使用 mockitio 框架,因为服务器不是 运行 并且 IOC 容器未启动,这就是为什么它们为 NULL。

因此在您的代码中,CustomerService、PropertyService 和 EntityToDtoMapper 为 NULL。

所以这里的问题是我们如何在不启动服务器的情况下上传 spring 应用程序上下文。

可以通过两种方式完成,要么使用@SpringBootTest 和@AutoConfigureMockMvc 注释加载整个spring 应用程序上下文。

或者我们可以通过使用@WebMvcTest 注释

缩小 spring 应用程序上下文,仅针对控制器本身

所以我在这里使用的解决方案是仅通过使用 @WebMvcTest(HomeController.class) 注释将测试范围缩小到控制器。

但那些 CustomerService、PropertyService 和 EntityToDtoMapper 仍然是 NULL。因此,要模拟那些 类 我们可以使用 @Mock 或 @MockBean 注释,但这些注释之间存在细微差别

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.

因此,由于我们已经为控制器上传了 spring 应用程序上下文,因此控制器也在应用程序上下文中期待这些 bean,这可以通过 @MockBean 注释来实现。

在模拟所有这些 bean 之后,您的控制器 bean 将被创建,但是有些方法您期望一些 return 值,因此您必须在代码中编写预期的 return 值,这可能是这样做

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

如果你错过了这个特定的步骤,那么在控制器中你将在这行代码中得到一个 NULL 指针异常

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

因为你编码

Customer customer = mapper.mapToEntity(customerDto);

将 return NULL。

我希望这会帮助并激励您获得更多关于这些概念的知识。

如果需要任何进一步的帮助,请告诉我

尝试使用 @MockBean@Mock 模拟 PropertyService。

我注意到您在 class 定义之上缺少 @WebMvcTest(Controller.class),您需要对 Mvc 控制器进行单元测试。 Explanation

如果@MockBean不起作用。

尝试:

使用 Mockito.when(),您可以简单地 return 您的 desired/expected 结果以及何时调用所需的方法。

使用Mockito.verify()保证执行时的愿望

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

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

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

verify(propertyService).getKeytoAddCustomer();

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

问题二

我假设 属性 文件的问题是因为您使用的是 spring.profile.active=dev 但 属性 文件是 test/resources 是 application.properties 而不是 application-dev.properties 尽管它是 test/resources 中唯一的 属性 文件。将两个资源文件夹中的文件重命名为完全相同的文件,看看会发生什么。

还有一个错误。这是一个主要问题——即使我提供了错误的 authId ,我的测试用例也通过了。请在您当地尝试,如果您有相同的行为,请告诉我。

当然你的测试将通过,因为你的代码只是检查条件它不会阻止代码被执行,请参考下面的代码:

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);

    }

这里在if条件下你只执行代码块,然后指示执行if条件之外的代码块。

所以在 if 条件下它什么都不做。所以你必须根据你的预期行为改进你的代码。

如果您想阻止您的代码执行,请参考下面的代码

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);
    }

这里我在If条件下设置了return语句

但是如果你想让你比较消息的当前测试用例失败,那么请参考下面的代码:

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);
    }

这里只需要在else部分设置if条件之外的代码块即可。