在编写代码的时候,大部分时间想的都是如何实现功能,很少会考虑到代码的可测试性。

又因为大部分公司没有要求写单元测试,完成的功能都是通过服务模拟的方式测试,更加不会考虑代码的可测试性了。

常见的可测试性不好的代码,几种情况(取自极客时间王铮设计模式之美)

未决行为,例如时间、随机数等全局变量,要考虑用例的执行顺序,或者有些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);

}

}

文章链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: