在编写代码的时候,大部分时间想的都是如何实现功能,很少会考虑到代码的可测试性。
又因为大部分公司没有要求写单元测试,完成的功能都是通过服务模拟的方式测试,更加不会考虑代码的可测试性了。
常见的可测试性不好的代码,几种情况(取自极客时间王铮设计模式之美)
- 未决行为,例如时间、随机数等
- 全局变量,要考虑用例的执行顺序,或者有些mock框架是并发执行的
- 静态方法,比如耗时长,依赖外部资源、逻辑复杂、行为未决时,需要进行模拟
- 复杂继承
- 高度耦合
单元测试编写过程中,经常会遇到下面几类问题。实际上是由于编写的代码可测试性差导致的。
1、单元测试时,维护一个第三方的服务,而且需要按照需要返回各种结果(成功的、失败、异常),成本是比较高的。如果第三方程序不是自己维护的,想要做到,更是不可能的。
解决方案:新增一个serviceEx类,继承正常运行时调用的service类,然后在serviceEx中重写方法,模拟自己想要的结果,供单元测试用例使用。而运行的程序仍旧使用service类。
2、第三方的类,比如RedisDistributeLock这种类似工具类的锁,要想确定其返回锁成功或者失败,也是很难做到的。
3、一些未决定行为,比如随机数、当前时间System.currentTimeMillis(),因其不确定性,在运行单元测试时,会导致结果不可控
原则:就是把不确定、调用不通的内容进行封装然后通过继承、重写等方式,把封装的内容进行替换,直接返回自己需要的内容。
下面是示例代码,解决以上三类问题,仅供参考
想运行示例可直接下载代码,免费https://download.csdn.net/download/zhaoronghui1314/86765041
不可测试代码示例
package com.zrh.jsd.temp;
import javax.transaction.InvalidTransactionException;
import java.util.UUID;
public class Transaction {
private String id;
private Long buyerId;
private Long createTimestamp;
private int status;
private String walletTransactionId;
public Transaction(String preAssignedId, Long buyerId) {
if (preAssignedId != null && !preAssignedId.isEmpty()) {
this.id = preAssignedId;
} else {
this.id = UUID.randomUUID().toString();
}
if (!this.id.startsWith("t_")) {
this.id = "t_" + preAssignedId;
}
this.buyerId = buyerId;
this.status = STATUS.TO_BE_EXECUTD;
this.createTimestamp = System.currentTimeMillis();
}
public boolean execute() throws InvalidTransactionException {
if (buyerId == null) {
throw new InvalidTransactionException();
}
if (status == STATUS.EXECUTED) {
return true;
}
boolean isLocked = false;
try {
// 修改点1:可以理解为第三方类,运行单元测试时,需要的lock状态不方便得到
// 仅做示例,此代码不可运行。按下方修改后可运行。
isLocked = RedisDistributedLock.getSingletonIntance().lockTransction(id);
if (!isLocked) {
return false;
}
if (status == STATUS.EXECUTED) {
return true;
} ;
// 修改点2:当前时间未决定的,运行单元测试时,此处不可控。
long executionInvokedTimestamp = System.currentTimeMillis();
if (executionInvokedTimestamp - createTimestamp > 14) {
this.status = STATUS.EXPIRED;
return false;
}
// 修改点3:WalletRpcService是第三方服务,运行单元测试时不一定可以正常调用
WalletRpcService walletRpcService = new WalletRpcService();
String walletTransactionId = walletRpcService.moveMoney();
if (walletTransactionId != null) {
this.walletTransactionId = walletTransactionId;
this.status = STATUS.EXECUTED;
return true;
} else {
this.status = STATUS.FAILED;
return false;
}
} finally {
if (isLocked) {
// 仅做示例,此代码不可运行。按下方修改后可运行。
RedisDistributedLock.getSingletonIntance().unlockTransction(id);
}
}
}
}
package com.zrh.jsd.temp;
public class WalletRpcService {
public String moveMoney() {
System.out.println("这里是WalletRpcService第三方服务");
return "asb";
}
}
package com.zrh.jsd.temp;
public class STATUS {
static final int TO_BE_EXECUTD = 0;
static final int EXECUTED = 1;
static final int EXPIRED = 2;
static final int FAILED = 3;
}
优化之后可测试的代码,包含单元测试用例
package org.example;
import javax.transaction.InvalidTransactionException;
import java.util.UUID;
public class Transaction {
private String id;
private Long buyerId;
private Long createTimestamp;
private int status;
private String walletTransactionId;
// 添加一个成员变量及其 set 方法。就可以将对象放到外面
private WalletRpcService walletRpcService;
private TransactionLock lock;
// 修改点2,提出方法,在test类中重写此方法。
protected boolean isExpired() {
long executionInvokedTimestamp = System.currentTimeMillis();
System.out.println("=======方法内部的isExpired==");
return executionInvokedTimestamp - createTimestamp > 14;
}
public void setTransactionLock(TransactionLock lock) {
this.lock = lock;
}
// 修改点3:WalletRpcService改为注入的方式,通过构造传入,避免在类中new
public Transaction(String preAssignedId, Long buyerId, WalletRpcService walletRpcService) {
if (preAssignedId != null && !preAssignedId.isEmpty()) {
this.id = preAssignedId;
} else {
this.id = UUID.randomUUID().toString();
}
if (!this.id.startsWith("t_")) {
this.id = "t_" + preAssignedId;
}
this.buyerId = buyerId;
this.status = STATUS.TO_BE_EXECUTD;
this.createTimestamp = System.currentTimeMillis();
this.walletRpcService = walletRpcService;
}
public boolean execute() throws InvalidTransactionException {
if (buyerId == null) {
throw new InvalidTransactionException();
}
if (status == STATUS.EXECUTED) {
return true;
}
boolean isLocked = false;
try {
isLocked = lock.lock(id);
if (!isLocked) {
return false; // 锁定未成功,返回 false,job 兜底执行
}
if (status == STATUS.EXECUTED) {
return true;
}
// createTimestamp 临时
if (isExpired()) {
this.status = STATUS.EXPIRED;
return false;
}
String walletTransactionId = walletRpcService.moveMoney();
if (walletTransactionId != null) {
this.walletTransactionId = walletTransactionId;
this.status = STATUS.EXECUTED;
return true;
} else {
this.status = STATUS.FAILED;
return false;
}
} finally {
if (isLocked) {
lock.unlock(id);
}
}
}
}
package org.example;
public class MockWalletRpcServiceOne extends WalletRpcService {
@Override
public String moveMoney() {
System.out.println("这里是WalletRpcService模拟服务");
return "asb";
}
}
package org.example;
public class RedisDistributedLock {
public static RedisDistributedLock getSingletonIntance() {
return new RedisDistributedLock();
}
boolean lockTransction(String id) {
return true;
}
boolean unlockTransction(String id) {
return true;
}
}
package org.example;
public class STATUS {
static final int TO_BE_EXECUTD = 0;
static final int EXECUTED = 1;
static final int EXPIRED = 2;
static final int FAILED = 3;
}
package org.example;
public class TransactionLock {
public boolean lock(String id) {
return RedisDistributedLock.getSingletonIntance().lockTransction(id);
}
public boolean unlock(String id) {
return RedisDistributedLock.getSingletonIntance().unlockTransction(id);
}
}
package org.example;
public class WalletRpcService {
public String moveMoney() {
System.out.println("这里是WalletRpcService第三方服务");
return "asb";
}
}
单元测试用例
package org.example;
import org.junit.jupiter.api.Test;
import javax.transaction.InvalidTransactionException;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TransactionTest {
@Test
public void testExecute() throws InvalidTransactionException {
Long buyerId = 123L;
// 修改点3:出入service,重写service中的方法,直接返回模拟结果,不依赖第三方服务
WalletRpcService walletRpcService = new MockWalletRpcServiceOne();
// 修改点2:模拟lock,重写方法,模拟返回的结果
TransactionLock mockLock = new TransactionLock() {
public boolean lock(String id) {
System.out.println("这里是模拟的lock");
return true;
}
public boolean unlock(String id) {
System.out.println("这里是模拟的unlock");
return true;
}
};
// walletRpcService 可以通过构造方法注入或者通过set方法注入
Transaction transaction = new Transaction(null, buyerId, walletRpcService) {
// 这里必须是protect以上的级别。private不可
// 修改点1:重写isExpired,返回期望的内容
protected boolean isExpired() {
System.out.println("这里是外部的isExpired方法");
return false;
}
};
transaction.setTransactionLock(mockLock);
boolean executedResult = transaction.execute();
assertTrue(executedResult);
}
}
版权归原作者 zhaoronghui1314 所有, 如有侵权,请联系我们删除。