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

这篇具有很好参考价值的文章主要介绍了单元测试:如何编写可测试的代码?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

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

单元测试用例文章来源地址https://www.toymoban.com/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;
        // 修改点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);
    }
}

到了这里,关于单元测试:如何编写可测试的代码?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 如何让chatgpt十分正确的帮咱们编写代码文档和单元测试

      有多少次你专注于编程而忘记了写函数、方法、类的非常简单的代码文档?我不是在问单元测试. 直到我发现ChatGPT可以做到这一点: 除了代码文档,它在编写单元测试方面也做得很好。此外,在最后,我可以要求他为其他想使用我的代码的贡献者生成一个用户友好的文档。

    2024年02月15日
    浏览(31)
  • 04 单元测试:怎样提升最小可测试单元的质量?

    上一篇文章“03 微服务架构下的测试策略” 我讲到了**微服务架构下的测试策略和质量保障体系**,今天我来讲讲测试策略中的最底层测试——单元测试。 单元测试的价值 单元测试是一种白盒测试技术,通常由开发人员在编码阶段完成,目的是验证软件代码中的每个单元(方

    2024年01月17日
    浏览(35)
  • 软件测试中如何编写单元测试用例(白盒测试)

    目录 前言: 一、 单元测试的概念 二、开始测试前的准备 三、开始测试 四、完成测试 前言: 单元测试是软件测试中一种重要的测试方法,它是在代码级别进行测试,通过对每个模块或功能进行独立测试来保障代码的正确性和可靠性。单元测试可以有效地避免产生隐藏的代

    2024年02月09日
    浏览(34)
  • Spring Boot中如何编写优雅的单元测试

    单元测试是指对软件中的最小可测试单元进行检查和验证。在Java中,单元测试的最小单元是类。通过编写针对类或方法的小段代码,来检验被测代码是否符合预期结果或行为。执行单元测试可以帮助开发者验证代码是否正确实现了功能需求,以及是否能够适应应用环境或需求

    2024年02月11日
    浏览(34)
  • 如何创建自己的Spring Boot Starter并为其编写单元测试

    当我们想要封装一些自定义功能给别人使用的时候,创建Spring Boot Starter的形式是最好的实现方式。如果您还不会构建自己的Spring Boot Starter的话,本文将带你一起创建一个自己的Spring Boot Starter。 创建一个新的 Maven 项目。第三方封装的命名格式是 xxx-spring-boot-starter ,例如:

    2024年03月15日
    浏览(30)
  • Service层代码单元测试以及单元测试如何Mock

    接着上一篇文章:单元测试入门篇,本篇文章作为单元测试的进阶篇,主要介绍如何对Springboot Service层代码做单元测试,以及单元测试中涉及外调服务时,如何通过Mock完成测试。 现在项目都流行前后端代码分离,后端使用springboot框架,在service层编写接口代码实现逻辑。假设

    2023年04月08日
    浏览(37)
  • 单元测试的哲学:如何确保代码质量

    单元测试是软件开发过程中的一种重要方法,它的目的是通过对代码的自动化测试来确保其正确性和可靠性。在过去的几十年里,单元测试逐渐成为软件开发的标准做法,并且在各种规模的项目中得到了广泛应用。然而,在实践中,很多开发人员并没有充分利用单元测试的潜

    2024年02月20日
    浏览(31)
  • 记单元测试的时候Mockito RedisTemplate的时候 报setIfAbsent null

    mock方法这样写 RedisTemplateString, Object redisTemplate = mock(RedisTemplate.class); when(mockRedisUtils.getRedisTemplate()).thenReturn(redisTemplate); ValueOperationsString, Object valueOperations = mock(ValueOperations.class); when(redisTemplate.opsForValue()).thenReturn(valueOperations); when(valueOperations.setIfAbsent(any(String.class), any(String

    2024年02月12日
    浏览(25)
  • 【单元测试】--编写单元测试

    一、编写第一个单元测试 编写第一个单元测试通常包括以下步骤。以下示例以C#和NUnit为例: 创建测试项目 : 在Visual Studio中,创建一个新的Class Library项目,这将是你的单元测试项目。 在解决方案资源管理器中,右键点击项目,选择 “管理 NuGet 包”,然后搜索并安装NUnit框

    2024年02月07日
    浏览(29)
  • 单元测试:优雅编写Kotlin单元测试

    一、MockK简介 MockK是一款功能强大、易于使用的Kotlin mocking框架。在编写 单元测试 时,MockK能够帮助我们简化代码、提高测试覆盖率,并改善测试的可维护性。除了基本用法外,MockK还提供了许多额外的功能和灵活的用法,让我们能够更好地模拟对象行为、验证函数调用,并在

    2024年02月10日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包