你眼中和我眼中的单元测试,看看有何区别?

Posted 架构师修行录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你眼中和我眼中的单元测试,看看有何区别?相关的知识,希望对你有一定的参考价值。

大家好,我是Jensen,今天给大家分享一篇单元测试。

单元测试,大家都耳熟能详,但在开发同学中,真正掌握单元测试、愿意写单元测试的并不多!或者也可以说,项目压力大,根本没有时间写单元测试。

项目压力大,写单元测试就真的浪费时间吗?那可不一定。

本文以几个简单的测试例子,给大家讲解下我眼中的单元测试。

0x1.万能的main方法

假设你接了个需求:

商品保存时,产品说需要判断商品名称是否可以为空,假设大家不知道开源框架中的方法,那我们自己来实现一遍。

// 我的实现
public static boolean isEmpty(String str)
return str == null;

写完不放心,我还是写个单元测试测试下

public static void main(String[] args) 
String str = null;
System.out.println("null isEmpty: " + isEmpty1(str)); // 结果:true
str = "java某花宝典";
System.out.println("非空字符串 isEmpty: " + isEmpty1(str)); // 结果:false

搞定,收工。

第二天,产品说:产品名称不能是空字符串。

好吧,立马修改。

public static boolean isEmpty(String str) 
return str == null || str.length() == 0;;

public static void main(String[] args)
String str = null;
System.out.println("null isEmpty: " + isEmpty2(str)); // 结果:true
str = "java某花宝典";
System.out.println("非空字符串 isEmpty: " + isEmpty2(str)); // 结果:false
str = "";
System.out.println("空字符串 isEmpty: " + isEmpty2(str)); // 结果:true

第三天,产品说:空格也不行。

我改还不行吗……

public static boolean isEmpty(String str) 
return str == null || str.length() == 0 || Objects.equals(" ", str);

public static void main(String[] args)
String str = null;
System.out.println("null isEmpty: " + isEmpty3(str)); // 结果:true
str = "java某花宝典";
System.out.println("非空字符串 isEmpty: " + isEmpty3(str)); // 结果:false
str = "";
System.out.println("空字符串 isEmpty: " + isEmpty3(str)); // 结果:true
str = " ";
System.out.println("空格 isEmpty: " + isEmpty3(str)); // 结果:true

终于做完了。

第四天,产品说:连续空格也不行的。你眼中和我眼中的单元测试,看看有何区别?_单元测试

public static boolean isEmpty(String str) 
int strLen;
if (cs == null || (strLen = cs.length()) == 0)
return true;

for (int i = 0; i < strLen; i++)
if (!Character.isWhitespace(cs.charAt(i)))
return false;


return true;

public static void main(String[] args)
String str = null;
System.out.println("null isEmpty: " + isEmpty4(str)); // 结果:true
str = "java某花宝典";
System.out.println("非空字符串 isEmpty: " + isEmpty4(str)); // 结果:false
str = "";
System.out.println("空字符串 isEmpty: " + isEmpty4(str)); // 结果:true
str = " ";
System.out.println("空格 isEmpty: " + isEmpty4(str)); // 结果:true
str = " ";
System.out.println("连续空格 isEmpty: " + isEmpty4(str)); // 结果:true

第五天,相安无事。

通过上面这个例子,我思考了一下:

  1. 产品给的需求很模糊,没有详细说明,在联调测试过程中,才发现问题;
  2. 开发同学的main方法测试没有覆盖到所有场景,这样就造成了不知道找产品确认需求;
  3. 代码中的main方法删除了吗???

0x2.junit牛刀小试

我们用上面的栗子,再来写个的单元测试。

大部分同学的单元测试是这样写的:

@Test
public void test()
boolean result = MyStringUtils.isEmpty(null);
log.info("result: ", result); // 结果:true

result = MyStringUtils.isEmpty("java某花宝典");
log.info("result: ", result); // 结果:false

result = MyStringUtils.isEmpty("");
log.info("result: ", result); // 结果:true

result = MyStringUtils.isEmpty(" ");
log.info("result: ", result); // 结果:true

result = MyStringUtils.isEmpty(" ");
log.info("result: ", result); // 结果:true

我的单元测试是这样写的:

  1. 必须要有断言
  2. 必须要覆盖已知的场景
@Test
public void test2()
boolean result = MyStringUtils.isEmpty(null);
log.info("result: ", result); // 结果:true
Assert.assertTrue(result);

result = MyStringUtils.isEmpty("java某花宝典");
log.info("result: ", result); // 结果:false
Assert.assertFalse(result);

result = MyStringUtils.isEmpty("");
log.info("result: ", result); // 结果:true
Assert.assertTrue(result);

result = MyStringUtils.isEmpty(" ");
log.info("result: ", result); // 结果:true
Assert.assertTrue(result);

result = MyStringUtils.isEmpty(" ");
log.info("result: ", result); // 结果:true
Assert.assertTrue(result);
关于Junit的介绍,网上太多了,我就不废话了,不是本文的重点大家自己搜下看看就好了。

比如这篇:​​使用JUnit进行单元测试​

案例:Junit 时间&异常测试

package com.xxx.test.junit;

import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

@Slf4j
public class MyUtilsTest

@Before
public void before()
log.info("before##################创建对象");
log.info("每个方法都执行##################每个方法都执行");


@After
public void after()
log.info("after###################销毁对象");
log.info("每个方法都执行##################每个方法都执行");


@BeforeClass
public static void beforeClass()
log.warn("beforeClass##################创建对象");
log.warn("只执行一次##################只执行一次");


@AfterClass
public static void afterClass()
log.warn("afterClass###################销毁对象");
log.warn("只执行一次##################只执行一次");


@Test
public void testIsEmpty()
boolean result = MyUtils.isEmpty(null);
log.info("result: ", result); // 结果:true

result = MyUtils.isEmpty("java某花宝典");
log.info("result: ", result); // 结果:false

result = MyUtils.isEmpty("");
log.info("result: ", result); // 结果:true

result = MyUtils.isEmpty(" ");
log.info("result: ", result); // 结果:true

result = MyUtils.isEmpty(" ");
log.info("result: ", result); // 结果:true


/** 时间测试 **/
@Test(timeout = 2000)
public void testPayTime()

MyUtils.payTime(1000);
log.warn("payTime1:1000");

MyUtils.payTime(1000);
log.warn("payTime2:1000");

MyUtils.payTime(3000);
log.warn("payTime3:3000");


@Test(timeout = 2000)
public void testPayTime2()

MyUtils.payTime(1900);
log.warn("payTime1:1900");



@Test
public void testThrowErr2()
log.warn("测试不通过");
MyUtils.throwErr();


/** 异常测试 **/
@Test(expected = ArithmeticException.class)
public void testThrowErr()
log.warn("测试通过");
log.warn("start~~~~~~~~~~~~~~~");
MyUtils.throwErr();

0x3.Mockito 值得拥有

注意Mockito读“摸key头”,不是周杰伦唱的那首。

在 springBoot 项目中,service 方法测试依赖的东西比较多,数据库、缓存、MQ等等,只有项目能够正常启动,单元测试才能启动。

单元测试启动后,又容易造成脏数据、需要不停地造数据等等。

一个复杂的 service 方法,里面有比较多的过程,有数据库操作,有接口调用,大家不理解测试,直接对整个方法进行测试。

其实,真正需要测试的内容无非就是这些:

  1. 核心算法
  2. 复杂运算逻辑(如:金额计算、优惠券使用逻辑)
  3. 有幂等需求等

复杂方法的测试,可以拆解成几个小逻辑的测试,这样依然能够达到测试的效果,提高测试、联调的通过率,并且在发现问题时,能够快速回归。

个人经验:编写单元测试的时间 + 依靠单元测试解决问题的时间 < 联调发现错误 + 解决问题的时间。上面说了这么多,和 mockito 又有什么关系呢?

  1. 如果不懂得拆分方法,掌握哪些方法需要测试,哪些方法不需要测试,那就会造成浪费;
  2. 如果你只会使用junit,那么一些复杂的测试,你每次启动测试时,都需要花大量的时间等待测试启动;如果你掌握了1,想快速进行测试,那么,mockito 就是你最好的选择;

​Mockito 简明教程​

0x4.使用 mockito 进行复杂业务测试

以下是我们公司的网关功能权限 mockito 测试案例。

私有静态方法:判断用户是否有权限

private boolean isUserRight(String service, String path, String requestMethod, CacheGatewayUseDTO gatewayUse) 
if (isDirectUserRight(gatewayUse.getUserRights(), service, path, requestMethod))
return true;


return isUserRoleRight(service, path, requestMethod, gatewayUse.getRoleIds());

使用 mockito 进行测试

@Slf4j
// 使用PowerMockRunner 进行测试
@RunWith(PowerMockRunner.class)
// 测试用使用了静态工具类 CacheUtils
@PrepareForTest(CacheUtils.class)
public class PowerFilterTest

// 测试的类
@InjectMocks
private PowerFilter powerFilter;

private String service;
private String path;
private String requestMethod;
private CacheGatewayUseDTO gatewayUse;
private boolean isMatch;
private UcRoleEntity role;
private List<UcRightsEntity> rightList;
private List<Long> roleIds;
private WhiteListResponse whiteListResponse;

@Test
public void isUserRight() throws InvocationTargetException, IllegalAccessException

PowerMockito.mockStatic(CacheUtils.class);
PowerMockito.when(CacheUtils.notExistNewRole(ArgumentMatchers.anyLong())).thenReturn(false);
// administrator uc 角色
role = UcRoleEntity.builder().id(13L).roleCode("RO210111000023").build();
// 创建测试数据
rightList = Arrays.asList(
createRight("uc", "/get/**", "GET"),
createRight("uc", "/getUserInfo", "GET"),
createRight("uc", "/getDept", "GET"),
createRight("uc", "/regrister", "POST"),
createRight("oms", "/getDept", "GET"),
createRight("oms", "/createOrder", "POST"),
createRight("oms", "/getOrder", "GET")

);
PowerMockito.when(CacheUtils.getNewRole(ArgumentMatchers.anyLong())).thenReturn(BizConverUtils.getCacheRole(role, rightList));

Method method = PowerMockito.method(PowerFilter.class, "isUserRight",
String.class,
String.class,
String.class,
CacheGatewayUseDTO.class);


service = "uc";
path = "/getUserInfo4";
requestMethod = "GET";

// 无权限
gatewayUse = CacheGatewayUseDTO.builder()
.userCode("")
.nickname("")
.roleIds(Arrays.asList(role.getId()))
.userRights(Arrays.asList(
UserRightsDO.builder().rightUrl("/getRoleList").requestMethod("GET").services(service).build()
, UserRightsDO.builder().rightUrl("/getTest").requestMethod("GET").services(service).build()
, UserRightsDO.builder().rightUrl("/getRights").requestMethod("GET").services(service).build()
))
.build();
isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse);
log.info("isMatch ", isMatch);
Assert.assertTrue(!isMatch);

// 直接权限
path = "/getRoleList";
isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse);
log.info("isMatch ", isMatch);
Assert.assertTrue(isMatch);

// 角色权限,服务不一样,没有权限
path = "/getOrder";
isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse);
log.info("isMatch ", isMatch);
Assert.assertTrue(!isMatch);

0x5.拔高篇——Spring Boot Test

Sptring Boot Test 集成了 junit mockito 之大成,还有其他额外的能力。

spring-boot-starter-test 包含了下面组件:

名称

功能

JUnit

Java语言的单元测试框架,默认依赖版本是4.12(JUnit5和JUnit4差别比较大,集成方式有不同)

Spring Test & Spring Boot Test

Spring的测试支持

AssertJ

提供了流式的断言方式

Hamcrest

提供了丰富的 matcher 和 assertThat 结合使用,如:判断bean是否包含某个属性等

Mockito

mock框架,可以按类型创建mock对象,可以根据方法参数指定特定的响应,也支持对于mock调用过程的断言

JSONassert

为JSON提供了断言功能

JsonPath

为JSON提供了XPATH功能,类似 JQuery 选择器

分享两篇写得比较好的文章给大家:

​Spring Boot Test 快速入门​

​Spring Boot Test 注解详解​

@SpringBootTest 注解参数 webEnvironment

指定web环境,可选值有:MOCK、RANDOM_PORT、DEFINED_PORT、NONE

名称

功能

MOCK

此值为默认值,该类型提供一个mock环境,此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web端口。

RANDOM_PORT

启动一个真实的web服务,监听一个随机端口。

DEFINED_PORT

启动一个真实的web服务,监听一个定义好的端口(从配置中读取)。

NONE

启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务。可以理解为,不启动web容器

0x6.总结一下

继续讲下去就炒冷饭了,梳理这篇文章,主要想让告诉大家真正的单元测试应该怎么做。说实话,我自己也只掌握了单元测试的30%而已,但仅仅靠这30%知识,就能:

  • 提高我的代码质量
  • 减少我测试联调时间
  • 让我能够在出现问题时快速定位问题

因此,我希望大家也可以重视单元测试,学习更多的单元测试知识。

一起共勉!


本文作者:Jensen

7年Java老兵,小米主题设计师,手机输入法设计师,ProcessOn特邀讲师。

曾涉猎航空、电信、IoT、垂直电商产品研发,现就职于某知名电商企业。

技术公众号【架构师修行录】号主,专注于分享程序员日常、架构技术、职场干货,关注回复“DDD”领学习DDD领域建模。

你眼中和我眼中的单元测试,看看有何区别?_单元测试_02

交个朋友,一起成长!

以上是关于你眼中和我眼中的单元测试,看看有何区别?的主要内容,如果未能解决你的问题,请参考以下文章

我眼中的微软Azure:Azure DevOps 介绍

SQL事务概念是啥,举个例子说明啥样的东西是事务,与程序又有何区别,

微信小程序单元测试攻略

微信小程序单元测试攻略

微信小程序单元测试攻略

一枚程序员眼中的单元测试