背景
在现在这个微服务时代,我们项目中经常都会遇到很多业务逻辑是依赖其他服务或者第三方接口。工作中各位同学对于这类型场景的测试方式也是五花八门,有些是直接构建一个外部mock
服务,返回一些固定的response
;有些是单元测试都不写,直接利用IDE工具,通过debug
模式调用依赖服务接口,然后自己在程序运行时插入假的返回数据或者直接粗暴调用依赖服务接口去调试自己逻辑;有些是通过单元测试,使用mockito
去屏蔽外部依赖等。
刚好最近有位精神小伙跟我反馈了一个问题,他改完代码就部署到SIT
进行集成测试,结果服务运行时一调用接口就报错,因此被测试同学投诉没做好单元测试就部署,也被老大痛骂一顿。他觉得很委屈,觉得明明在做单元测试时已经针对同样的Mock
数据测试过,结果在服务器上代码运行到restTemplate.exchange
方法就报错,直接转换类型失败,他想知道为什么他的单元测试没覆盖到。
这里主要讲解下
mockito
和Spring 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();
}
}
以下是针对该代码逻辑的单元测试:
@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);
}
}
测试数据
[
{
"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 {
@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);
}
}
从上面这个测试用例可以看到,这里并没有mock
掉restTemplate
和CustomerServiceI
,只是使用了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 {
@NonNull
private int id;
@NonNull
private 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
值
//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);
测试结果
结论
单元测试方法有很多,但针对外部接口依赖测试方法,以上两种比较常见。第一种单元测试更偏向于方法结果的测试,不关注接口调用过程,因为里面有第三方依赖,一般都会直接mock
掉,只关注非外部依赖部分的代码逻辑。而第二种单元测试,会关注接口调用,覆盖了整个方法执行过程,所以一旦接口调用有问题更容易发现,但是有些同学也喜欢把这部分设计外部接口依赖的逻辑放在集成测试的时候再测试。大家这里就根据自己实际情况进行选择即可,一般以上两种单元测试用例结合使用覆盖更全。
注意:以上的测试数据一般是由接口提供者提供或者根据API接口文档定义生成,这样才能更好模拟真实API接口返回的数据。文章来源:https://www.toymoban.com/news/detail-765686.html
代码
这里是本文的测试代码,供大家参考。文章来源地址https://www.toymoban.com/news/detail-765686.html
- https://github.com/EvanLeung08/java-unit-test-samples/tree/main/spring-web/spring-resttemplate-unit-test
到了这里,关于单元测试系列 | 如何更好地测试依赖外部接口的方法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!