0


单元测试:如何编写可测试的代码?

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

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

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

  • 未决行为,例如时间、随机数等
  • 全局变量,要考虑用例的执行顺序,或者有些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);
    }
}
标签: 单元测试 junit java

本文转载自: https://blog.csdn.net/zhaoronghui1314/article/details/127340118
版权归原作者 zhaoronghui1314 所有, 如有侵权,请联系我们删除。

“单元测试:如何编写可测试的代码?”的评论:

还没有评论