在編寫代碼的時(shí)候,大部分時(shí)間想的都是如何實(shí)現(xiàn)功能,很少會(huì)考慮到代碼的可測(cè)試性。
又因?yàn)榇蟛糠止緵](méi)有要求寫單元測(cè)試,完成的功能都是通過(guò)服務(wù)模擬的方式測(cè)試,更加不會(huì)考慮代碼的可測(cè)試性了。
常見(jiàn)的可測(cè)試性不好的代碼,幾種情況(取自極客時(shí)間王錚設(shè)計(jì)模式之美)
- 未決行為,例如時(shí)間、隨機(jī)數(shù)等
- 全局變量,要考慮用例的執(zhí)行順序,或者有些mock框架是并發(fā)執(zhí)行的
- 靜態(tài)方法,比如耗時(shí)長(zhǎng),依賴外部資源、邏輯復(fù)雜、行為未決時(shí),需要進(jìn)行模擬
- 復(fù)雜繼承
- 高度耦合
單元測(cè)試編寫過(guò)程中,經(jīng)常會(huì)遇到下面幾類問(wèn)題。實(shí)際上是由于編寫的代碼可測(cè)試性差導(dǎo)致的。
1、單元測(cè)試時(shí),維護(hù)一個(gè)第三方的服務(wù),而且需要按照需要返回各種結(jié)果(成功的、失敗、異常),成本是比較高的。如果第三方程序不是自己維護(hù)的,想要做到,更是不可能的。
? ? ? 解決方案:新增一個(gè)serviceEx類,繼承正常運(yùn)行時(shí)調(diào)用的service類,然后在serviceEx中重寫方法,模擬自己想要的結(jié)果,供單元測(cè)試用例使用。而運(yùn)行的程序仍舊使用service類。
2、第三方的類,比如RedisDistributeLock這種類似工具類的鎖,要想確定其返回鎖成功或者失敗,也是很難做到的。
3、一些未決定行為,比如隨機(jī)數(shù)、當(dāng)前時(shí)間System.currentTimeMillis(),因其不確定性,在運(yùn)行單元測(cè)試時(shí),會(huì)導(dǎo)致結(jié)果不可控
原則:就是把不確定、調(diào)用不通的內(nèi)容進(jìn)行封裝然后通過(guò)繼承、重寫等方式,把封裝的內(nèi)容進(jìn)行替換,直接返回自己需要的內(nèi)容。
下面是示例代碼,解決以上三類問(wèn)題,僅供參考
想運(yùn)行示例可直接下載代碼,免費(fèi)https://download.csdn.net/download/zhaoronghui1314/86765041
不可測(cè)試代碼示例
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 {
// 修改點(diǎn)1:可以理解為第三方類,運(yùn)行單元測(cè)試時(shí),需要的lock狀態(tài)不方便得到
// 僅做示例,此代碼不可運(yùn)行。按下方修改后可運(yùn)行。
isLocked = RedisDistributedLock.getSingletonIntance().lockTransction(id);
if (!isLocked) {
return false;
}
if (status == STATUS.EXECUTED) {
return true;
} ;
// 修改點(diǎn)2:當(dāng)前時(shí)間未決定的,運(yùn)行單元測(cè)試時(shí),此處不可控。
long executionInvokedTimestamp = System.currentTimeMillis();
if (executionInvokedTimestamp - createTimestamp > 14) {
this.status = STATUS.EXPIRED;
return false;
}
// 修改點(diǎn)3:WalletRpcService是第三方服務(wù),運(yùn)行單元測(cè)試時(shí)不一定可以正常調(diào)用
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) {
// 僅做示例,此代碼不可運(yùn)行。按下方修改后可運(yùn)行。
RedisDistributedLock.getSingletonIntance().unlockTransction(id);
}
}
}
}
package com.zrh.jsd.temp;
public class WalletRpcService {
public String moveMoney() {
System.out.println("這里是WalletRpcService第三方服務(wù)");
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;
}
優(yōu)化之后可測(cè)試的代碼,包含單元測(cè)試用例文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-479025.html
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;
// 添加一個(gè)成員變量及其 set 方法。就可以將對(duì)象放到外面
private WalletRpcService walletRpcService;
private TransactionLock lock;
// 修改點(diǎn)2,提出方法,在test類中重寫此方法。
protected boolean isExpired() {
long executionInvokedTimestamp = System.currentTimeMillis();
System.out.println("=======方法內(nèi)部的isExpired==");
return executionInvokedTimestamp - createTimestamp > 14;
}
public void setTransactionLock(TransactionLock lock) {
this.lock = lock;
}
// 修改點(diǎn)3:WalletRpcService改為注入的方式,通過(guò)構(gòu)造傳入,避免在類中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 兜底執(zhí)行
}
if (status == STATUS.EXECUTED) {
return true;
}
// createTimestamp 臨時(shí)
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模擬服務(wù)");
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第三方服務(wù)");
return "asb";
}
}
單元測(cè)試用例文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-479025.html
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;
// 修改點(diǎn)3:出入service,重寫service中的方法,直接返回模擬結(jié)果,不依賴第三方服務(wù)
WalletRpcService walletRpcService = new MockWalletRpcServiceOne();
// 修改點(diǎn)2:模擬lock,重寫方法,模擬返回的結(jié)果
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 可以通過(guò)構(gòu)造方法注入或者通過(guò)set方法注入
Transaction transaction = new Transaction(null, buyerId, walletRpcService) {
// 這里必須是protect以上的級(jí)別。private不可
// 修改點(diǎn)1:重寫isExpired,返回期望的內(nèi)容
protected boolean isExpired() {
System.out.println("這里是外部的isExpired方法");
return false;
}
};
transaction.setTransactionLock(mockLock);
boolean executedResult = transaction.execute();
assertTrue(executedResult);
}
}
到了這里,關(guān)于單元測(cè)試:如何編寫可測(cè)試的代碼?的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!