当前位置:   article > 正文

单元测试系列 | 如何更好地测试依赖外部接口的方法_单元测试依赖

单元测试依赖

背景

在现在这个微服务时代,我们项目中经常都会遇到很多业务逻辑是依赖其他服务或者第三方接口。工作中各位同学对于这类型场景的测试方式也是五花八门,有些是直接构建一个外部mock服务,返回一些固定的response;有些是单元测试都不写,直接利用IDE工具,通过debug模式调用依赖服务接口,然后自己在程序运行时插入假的返回数据或者直接粗暴调用依赖服务接口去调试自己逻辑;有些是通过单元测试,使用mockito去屏蔽外部依赖等。

刚好最近有位精神小伙跟我反馈了一个问题,他改完代码就部署到SIT进行集成测试,结果服务运行时一调用接口就报错,因此被测试同学投诉没做好单元测试就部署,也被老大痛骂一顿。他觉得很委屈,觉得明明在做单元测试时已经针对同样的Mock数据测试过,结果在服务器上代码运行到restTemplate.exchange方法就报错,直接转换类型失败,他想知道为什么他的单元测试没覆盖到。

这里主要讲解下mockitoSpring Test两种常见单元测试场景。以下用例均为模拟场景和测试数据。

场景

以下是模拟该精神小伙的代码片段:

@Service
public class CustomerServiceImpl implements CustomerServiceI {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public List<Customer> getCustomerList() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        HttpEntity<String> entity = new HttpEntity<String>(headers);
        ParameterizedTypeReference<List<Customer>> responseBodyType = new ParameterizedTypeReference<List<Customer>>(){};
        return restTemplate.exchange("http://localhost:8080/customers", HttpMethod.GET, null,responseBodyType).getBody();
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

以下是针对该代码逻辑的单元测试:

@ExtendWith(MockitoExtension.class)
public class CustomerServiceImplTest {

    @Mock
    private RestTemplate restTemplate;

    @InjectMocks
    private CustomerServiceI customerServiceI = new CustomerServiceImpl();


    @Test
    void testGetCustomerList() {
        //given
        Customer customer1 = new Customer(1, "Evan");
        Customer customer2 = new Customer(2, null);
        List<Customer> customerList = new ArrayList<Customer>();
        customerList.add(customer1);
        customerList.add(customer2);
        ParameterizedTypeReference<List<Customer>> responseBodyType = new ParameterizedTypeReference<List<Customer>>(){};
        //When
        Mockito.when(restTemplate.exchange("http://localhost:8080/customers", HttpMethod.GET, null,responseBodyType)).thenReturn(new ResponseEntity(customerList, HttpStatus.OK));

        //expect
        List<Customer> customers = customerServiceI.getCustomerList();

        Assertions.assertEquals(customerList,customers);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

测试数据

[
  {
    "id": "1",
    "name": "Evan"
  },
  {
    "id": "2",
    "name": null
  }
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

测试能顺利通过

大家会发现,他这里的单元测试,如果只针对GetCustomerList这个方法进行测试并没有多大问题,因为他把外表的依赖已经Mock掉了,该单元测试测试对象就是customerServiceI.getCustomerList()这个方法,很多同学平时也是这样子做。

问题

其实他的测试代码并没有太大问题,问题在于他想要的测试对象是谁。如果他的测试对象只是关注customerServiceI.getCustomerList()返回是不是正常,也就是这个方法业务逻辑是否正常,不需要关注restTemplate对接口调用过程,那么他这个单元测试没有问题。但是在这里,他有一个隐藏的测试用例,就是该方法从调用依赖服务接口到接收返回数据的整个方法生命周期是否正常,也就是需要关注restTemplate对接口调用过程。

听起来有点绕,那么这里的区别是什么?
这里的区别就是 是否需要关注接口调用这个过程

 Mockito.when(restTemplate.exchange("http://localhost:8080/customers", HttpMethod.GET, null,responseBodyType)).thenReturn(new ResponseEntity(customerList, HttpStatus.OK));
  • 1

很多时候,我们单元测试可能不会关注接口调用,也会像上面例子那样子,直接把restTemplate操作mock掉,这时候我们只需要关注我们写的代码逻辑是否符合预期。但问题也是出在这里,因为我们把restTemplate.exchange整个过程以及返回值mock掉了,所以这里的单元测试并没有真正调用restTemplate.exchange方法,它只是按照我们写的data直接返回而已,这也就是为什么在单元测试的时候没有测试出来。

改进

在现有的基础上增加一个单元测试用例,主要覆盖该方法从调用依赖服务接口到接收返回数据的整个方法生命周期是否正常这场景。

以下是针对上面代码逻辑增加的一个单元测试用例:

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = SpringTestConfig.class)
public class CustomerServiceMockRestServiceServerUnitTest {

    @Autowired
    private CustomerServiceI customerServiceI;

    @Autowired
    private RestTemplate restTemplate;

    @Value("classpath:mockdata/response.json")
    Resource mockResponse;

    private MockRestServiceServer mockServer;
    private ObjectMapper mapper = new ObjectMapper();

    @BeforeEach
    public void init() {
        mockServer = MockRestServiceServer.createServer(restTemplate);
    }

    @Test
    void givenMockingIsDoneByMockCustomerRestServiceServer_whenGetIsCalled_thenReturnsMockedCustomerListObject() throws Exception {
        //given
        Customer customer1 = new Customer(1, "Evan");
        Customer customer2 = new Customer(2, null);
        List<Customer> customerList = new ArrayList<Customer>();
        customerList.add(customer1);
        customerList.add(customer2);
        //when
        mockServer.expect(ExpectedCount.once(),
                requestTo(new URI("http://localhost:8080/customers")))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withStatus(HttpStatus.OK)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body( FileUtils.readFileToString(mockResponse.getFile(), Charset.forName("utf-8")))
                );

        List<Customer> customers = customerServiceI.getCustomerList();
        mockServer.verify();
        //expect
        Assertions.assertEquals(customerList, customers);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

从上面这个测试用例可以看到,这里并没有mockrestTemplateCustomerServiceI,只是使用了mock server把接口返回数据mock掉了,跟上面最大的区别是这里测试会启动spring容器,并且调用真实restTemplate实例进行调用,可以模拟真实的 API调用,此时restTemplate.exchange会被真实执行,相当于是调用了一个外部Mock API服务拿到一个预定义的返回数据。

这里使用上面同样的测试数据,通过注解注入外部Json文件:

@Value("classpath:mockdata/response.json")
    Resource mockResponse;
  • 1
  • 2

测试数据

[
  {
    "id": "1",
    "name": "Evan"
  },
  {
    "id": "2",
    "name": null
  }
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

测试结果

这里你会发现,测试结果跟上面不一样,这里在单元测试restTemplate调用时就已经暴露问题了,因为Customer的属性都加了NonNull注解,因此在类型转换的时候,由于测试数据包含Null值,所以调用 restTemplate.exchange方法时尝试转换成Customer对象时失败,由于上面第一个单元测试它直接把restTemplate实例 mock掉了,因此单元测试可以直接通过,而第二个单元测试是会直接使用restTemplate进行接口调用,可以更真实模拟接口调用情况。

@Data
public class Customer {
    @NonNull
    private int id;
    @NonNull
    private String name;

    public Customer(){

    }

    public Customer(int id,String name){
            this.id=id;
            this.name =name;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

对于第二个单元测试,只需要使用的测试数据均为非Null值,就可以测试通过

[
  {
    "id": "1",
    "name": "Evan"
  },
  {
    "id": "2",
    "name": "Alin"
  }
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

测试代码也要改为非Null

    //given
        Customer customer1 = new Customer(1, "Evan");
        Customer customer2 = new Customer(2, "Alin");
        List<Customer> customerList = new ArrayList<Customer>();
        customerList.add(customer1);
        customerList.add(customer2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

测试结果

结论

单元测试方法有很多,但针对外部接口依赖测试方法,以上两种比较常见。第一种单元测试更偏向于方法结果的测试,不关注接口调用过程,因为里面有第三方依赖,一般都会直接mock掉,只关注非外部依赖部分的代码逻辑。而第二种单元测试,会关注接口调用,覆盖了整个方法执行过程,所以一旦接口调用有问题更容易发现,但是有些同学也喜欢把这部分设计外部接口依赖的逻辑放在集成测试的时候再测试。大家这里就根据自己实际情况进行选择即可,一般以上两种单元测试用例结合使用覆盖更全。

注意:以上的测试数据一般是由接口提供者提供或者根据API接口文档定义生成,这样才能更好模拟真实API接口返回的数据。

代码

这里是本文的测试代码,供大家参考。

  • https://github.com/EvanLeung08/java-unit-test-samples/tree/main/spring-web/spring-resttemplate-unit-test
本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号