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

news/2024/5/3 12:00:42/文章来源:https://blog.csdn.net/Evan_Leung/article/details/130002860

背景

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

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

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

场景

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

@Service
public class CustomerServiceImpl implements CustomerServiceI {@Autowiredprivate RestTemplate restTemplate;@Overridepublic 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();}}

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

@ExtendWith(MockitoExtension.class)
public class CustomerServiceImplTest {@Mockprivate RestTemplate restTemplate;@InjectMocksprivate CustomerServiceI customerServiceI = new CustomerServiceImpl();@Testvoid testGetCustomerList() {//givenCustomer 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>>(){};//WhenMockito.when(restTemplate.exchange("http://localhost:8080/customers", HttpMethod.GET, null,responseBodyType)).thenReturn(new ResponseEntity(customerList, HttpStatus.OK));//expectList<Customer> customers = customerServiceI.getCustomerList();Assertions.assertEquals(customerList,customers);}}

测试数据

[{"id": "1","name": "Evan"},{"id": "2","name": null}
]

测试能顺利通过

大家会发现,他这里的单元测试,如果只针对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));

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

改进

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

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

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = SpringTestConfig.class)
public class CustomerServiceMockRestServiceServerUnitTest {@Autowiredprivate CustomerServiceI customerServiceI;@Autowiredprivate RestTemplate restTemplate;@Value("classpath:mockdata/response.json")Resource mockResponse;private MockRestServiceServer mockServer;private ObjectMapper mapper = new ObjectMapper();@BeforeEachpublic void init() {mockServer = MockRestServiceServer.createServer(restTemplate);}@Testvoid givenMockingIsDoneByMockCustomerRestServiceServer_whenGetIsCalled_thenReturnsMockedCustomerListObject() throws Exception {//givenCustomer customer1 = new Customer(1, "Evan");Customer customer2 = new Customer(2, null);List<Customer> customerList = new ArrayList<Customer>();customerList.add(customer1);customerList.add(customer2);//whenmockServer.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();//expectAssertions.assertEquals(customerList, customers);}
}

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

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

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

测试数据

[{"id": "1","name": "Evan"},{"id": "2","name": null}
]

测试结果

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

@Data
public class Customer {@NonNullprivate int id;@NonNullprivate String name;public Customer(){}public Customer(int id,String name){this.id=id;this.name =name;}}

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

[{"id": "1","name": "Evan"},{"id": "2","name": "Alin"}
]

测试代码也要改为非Null

    //givenCustomer customer1 = new Customer(1, "Evan");Customer customer2 = new Customer(2, "Alin");List<Customer> customerList = new ArrayList<Customer>();customerList.add(customer1);customerList.add(customer2);

测试结果

结论

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

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

代码

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

  • https://github.com/EvanLeung08/java-unit-test-samples/tree/main/spring-web/spring-resttemplate-unit-test

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_283059.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux复习 / 命令与权限部分QA梳理

文章目录前言Q&AshellQ&#xff1a;什么是shell&#xff1f;Q&#xff1a;shell的作用&#xff1f;Q&#xff1a;为什么要有shell&#xff1f;Q&#xff1a;shell的生命周期多长&#xff1f;Q&#xff1a;shell的原理/实现是怎样的&#xff1f;Q&#xff1a;为什么会有内建…

Scrum Master 应该采取哪些措施来提高团队效率?

项目经理应该从这5方面提高团队的开发效率 1、目标明确有时间节点 提高团队开发效率&#xff0c;最重要的是明确目标与期限。制定SMART目标&#xff0c;明确告知成员要实现什么&#xff0c;输出什么&#xff0c;标准以及时限等&#xff0c;需要考虑目标的可达成性和目标与项目的…

【牛客刷题专栏】0x17:JZ17打印从1到最大的n位数(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录前言问题…

Java初阶(异常)

文章目录一、异常的结构体系二、异常的处理2.1 防御式编程2.2 异常的抛出2.4 异常的捕获&#xff08;异常的具体处理方式&#xff09;&#xff08;1&#xff09;异常声明 throws&#xff08;2&#xff09; 捕获处理 try-catch2.4 异常的处理流程三、自定义异常类一、异常的结构…

go学习线路图

1. go学习线路图 1.1.2. 资源 先决条件 GoSQL 通用开发技能 学习 GIT&#xff0c;在 GitHub 上建立一些仓库&#xff0c;与其它人分享你的代码了解 HTTP(S) 协议&#xff0c;request 方法&#xff08;GET, POST, PUT, PATCH, DELETE, OPTIONS&#xff09;不要害怕使用 Google&a…

和数软件荣获上海市“专精特新”企业荣誉认定

近日&#xff0c;上海市经济和信息化委员会公示了2022年上海市“专精特新”企业名单。根据《关于组织开展2022年创新型中小企业评价、专精特新中小企业认定和复核工作的通知》&#xff08;沪经信企〔2022〕776号&#xff09;&#xff0c;经专家评审和综合评估&#xff0c;上海和…

学会吊打面试官之map

小白&#xff1a;大牛&#xff0c;我最近学习了一些C的STL容器&#xff0c;但是我还是有一些疑惑&#xff0c;特别是对于map&#xff0c;我不太理解它的底层实现和具体用法。能否跟我讲一下&#xff1f; 大牛&#xff1a;当然可以啊&#xff0c;map是一种非常常用的关联式容器…

小企业选择什么样的CRM系统比较合适,有什么特点?

CRM客户管理系统已经成为各种规模的企业&#xff0c;特别是小型企业的重要工具。CRM系统帮助小型企业更有效地管理客户数据和互动&#xff0c;简化销售流程&#xff0c;并提高客户满意度。市场上有如此多的选择&#xff0c;小企业该如何选择合适的CRM系统&#xff1f; 什么是C…

深圳CPDA|如何着手商业数据分析?

商业数据分析是一项非常重要的工作&#xff0c;可以帮助企业做出更明智的决策。 下面是一些着手商业数据分析的步骤&#xff1a; 1.确定你的问题 首先需要明确你想要解决什么问题。 这通常需要与业务团队沟通&#xff0c;以便了解他们正在寻找哪些信息。 2.收集数据 收集数…

linux语言学习记录

文章目录前言一、linux文件结构二、指令三、Gvim编辑器1、命令模式2、底行命令四、正则表达式1、表达式匹配举例2、对文件里面内容进行操作3、使用 \( 和 )\ 符号括起正规表达式&#xff0c;即可在后面使用\1和\2等变量来访问和中的内容前言 记录自己学习linux的笔记&#xff…

IFPUG功能点度量5:计算功能规模

功能点计数类型&#xff1a;开发项目、升级项目、应用 一、 三种功能能规模计算&#xff1a; 1、开发项目计算 DFP&#xff08;开发项目功能规模&#xff09;ADD&#xff08;交付用户的功能规模&#xff09;CFP&#xff08;转换功能的功能规模&#xff09; 2、升级项目计算 …

【代码笔记】Pytorch学习 DataLoader模块详解

Pytorch DataLoader模块详解dataloader整体结构DataLoaderinit 初始化参数解释代码解析IterableDataset 判断构建Sampler&#xff0c;单样本构建BatchSampler&#xff0c;组建batch构建collate_fn 对获取的batch进行处理其他的一些逻辑判断_get_iterator代码解析multiprocessin…

【Python】轻松掌握基础语法(一)

文章目录常量和表达式变量和类型变量的定义变量的使用变量的类型intfloatstrbool动态类型注释输入和输出输出输入运算符算数运算符关系运算符逻辑运算符赋值运算符其他常量和表达式 print(1 2 * 3)print是Python内置的一个函数&#xff0c;作用为输入打印到控制台形如1 2 * …

人工智能前沿——「全域全知全能」人类新宇宙ChatGPT

&#x1f680;&#x1f680;&#x1f680;OpenAI聊天机器人ChatGPT——「全域全知全能」人类全宇宙大爆炸&#xff01;&#xff01;&#x1f525;&#x1f525;&#x1f525; 一、什么是ChatGPT?&#x1f340;&#x1f340; ChatGPT是生成型预训练变换模型&#xff08;Chat G…

1.半导体基础知识

1.半导体基础知识本征半导体什么是半导体&#xff1f;什么是本征半导体&#xff1f;本征半导体的结构本征半导体中的两种载流子为什么将自然界导电性能中等的半导体材料制成本征半导体杂质半导体N型半导体P型半导体PN结PN结中的扩散运动漂移运动和PN结的形成PN结的单向导电性PN…

uniapp开发小程序-swiper点击预览大图(商品详情页轮播图)

1.实现效果&#xff1a; 2.具体代码&#xff1a; <view class"swiper_box"><!--轮播图--><swiper class"ms_swiper" :autoplay"true" circular"true" change"swiperChange"><swiper-item v-for"…

【动态规划模板】神似的01和完全背包、多重背包和分组背包问题

神似的01背包与完全背包&#x1f349; 【经典题目】01背包采药 题目描述 &#x1f388; 辰辰是个天资聪颖的孩子&#xff0c;他的梦想是成为世界上最伟大的医师。为此&#xff0c;他想拜附近最有威望的医师为师。医师为了判断他的资质&#xff0c;给他出了一个难题。医师把他…

【Redis学习】SpringBoot集成Redis

总体概述 jedis-lettuce-RedisTemplate三者的联系 本地Java连接Redis常见问题 bind配置请注释掉 保护模式设置为no Linux系统的防火墙设置 redis服务器的IP地址和密码是否正确 忘记写访问redis的服务端口号和auth密码 集成jedis 简介 Jedis Client是Redis官网推荐的一个面…

【c语言】文件的读写

文件读写使用二进制读写比较方便&#xff0c;分别使用fread和fwrite函数进行。 一、函数定义 以二进制形式读取文件&#xff0c;从stream流中读取内容&#xff0c;读到ptr指向的空间中&#xff0c;读取size大小的count个内存单元。 返回值为读取到的字符个数。 以二进制形式读…

解决Abp设置DefaultLanguage默认语言不生效的问题

文章目录现象原因分析解决问题现象 默认地&#xff0c;Abp的语言提供程序将返回的CultureInfo为En&#xff0c;在一些默认实现的接口&#xff08;比如/api/TokenAuth/Authenticate&#xff09;返回的错误信息是英文 目标是改成简体中文显示&#xff0c;但是即便我们在AbpSett…