利用ChatGPT协助编写单元测试

2023-11-04

ChatGPT自从2022年推出以来受到很多人的喜欢,此篇博客重点介绍如何修改Prompt来自动生成较理想的单元测试。如下图所示的一段代码,该class中有一个public方法toLocale(),其余都是private方法,toLocale()方法会调用private的方法。(备注:下面的方法特地写了比较多的分支逻辑,来验证chatGPT编写的单元测试的覆盖率情况)

package com.github.secondCourse;
import java.util.Locale;
public class LocaleUtils {
    private static final String EMPTY = "";
    public Locale toLocale(final String str) {
        if (str == null) {
            return null;
        }
        if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank
            return new Locale(EMPTY, EMPTY);
        }
        if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions
            throw new IllegalArgumentException("Invalid locale format: " + str);
        }
        final int len = str.length();
        if (len < 2) {
            throw new IllegalArgumentException("Invalid locale format: " + str);
        }
        final char ch0 = str.charAt(0);
        if (ch0 == '_') {
            if (len < 3) {
                throw new IllegalArgumentException("Invalid locale format: " + str);
            }
            final char ch1 = str.charAt(1);
            final char ch2 = str.charAt(2);
            if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
                throw new IllegalArgumentException("Invalid locale format: " + str);
            }
            if (len == 3) {
                return new Locale(EMPTY, str.substring(1, 3));
            }
            if (len < 5) {
                throw new IllegalArgumentException("Invalid locale format: " + str);
            }
            if (str.charAt(3) != '_') {
                throw new IllegalArgumentException("Invalid locale format: " + str);
            }
            return new Locale(EMPTY, str.substring(1, 3), str.substring(4));
        }

        return parseLocale(str);
    }

    private Locale parseLocale(final String str) {
        if (isISO639LanguageCode(str)) {
            return new Locale(str);
        }

        final String[] segments = str.split("_", -1);
        final String language = segments[0];
        if (segments.length == 2) {
            final String country = segments[1];
            if (isISO639LanguageCode(language) && isISO3166CountryCode(country) ||
                    isNumericAreaCode(country)) {
                return new Locale(language, country);
            }
        } else if (segments.length == 3) {
            final String country = segments[1];
            final String variant = segments[2];
            if (isISO639LanguageCode(language) &&
                    (country.length() == 0 || isISO3166CountryCode(country) || isNumericAreaCode(country)) &&
                    variant.length() > 0) {
                return new Locale(language, country, variant);
            }
        }
        throw new IllegalArgumentException("Invalid locale format: " + str);
    }

    private boolean isISO639LanguageCode(final String str) {
        return isAllLowerCase(str) && (str.length() == 2 || str.length() == 3);
    }

    private boolean isISO3166CountryCode(final String str) {
        return isAllUpperCase(str) && str.length() == 2;
    }

    private boolean isNumericAreaCode(final String str) {
        return isNumeric(str) && str.length() == 3;
    }

    private boolean isAllLowerCase(final CharSequence cs) {
        if (cs == null || isEmpty(cs)) {
            return false;
        }
        final int sz = cs.length();
        for (int i = 0; i < sz; i++) {
            if (!Character.isLowerCase(cs.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    private boolean isAllUpperCase(final CharSequence cs) {
        if (cs == null || isEmpty(cs)) {
            return false;
        }
        final int sz = cs.length();
        for (int i = 0; i < sz; i++) {
            if (!Character.isUpperCase(cs.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    private boolean isEmpty(final CharSequence cs) {
        return cs == null || cs.length() == 0;
    }

    private boolean isNumeric(final CharSequence cs) {
        if (isEmpty(cs)) {
            return false;
        }
        final int sz = cs.length();
        for (int i = 0; i < sz; i++) {
            if (!Character.isDigit(cs.charAt(i))) {
                return false;
            }
        }
        return true;
    }
}

下面是原来为这个class编写的单元测试,运行测试,覆盖率在80%左右。

public class LocalUtilsTest {
    private LocaleUtils localeUtils;
    @Rule
    public ExpectedException exception = ExpectedException.none();
    @Before
    public void setUp() {
        localeUtils= new LocaleUtils();
    }

    @Test()
    public void should_return_null_when_str_is_null() {

        assertThat(localeUtils.toLocale(null)).isEqualTo(null);
    }

    @Test()
    public void should_call_isEmpty_when_str_is_empty() {
        assertThat(localeUtils.toLocale("").getLanguage().isEmpty());
        assertThat(localeUtils.toLocale("").getCountry().isEmpty());
    }

    @Test
    public void should_throw_exception_when_str_is_not_valid() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: #");
        localeUtils.toLocale("#");
    }

    @Test
    public void should_throw_exception_when_strLength_is_less_2(){
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: a");
        localeUtils.toLocale("a");
    }

    @Test
    public void should_throw_exception_when_strLength_is_less_3() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: _a");
        localeUtils.toLocale("_a");
    }
    @Test
    public void should_throw_exception_when_strLength_is_3_and_is_lowercase() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: _Aa");
        localeUtils.toLocale("_Aa");
    }

    @Test
    public void should_return_locale_when_strLength_is_3() {
      assertThat(localeUtils.toLocale("_AB").getCountry()).isEqualTo("AB");
    }
    @Test
    public void should_throw_exception_when_strLength_is_4() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: _ABC");
        localeUtils.toLocale("_ABC");
    }

    @Test
    public void should_throw_exception_when_str_3_is_not_valid(){
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: _ABC_");
        localeUtils.toLocale("_ABC_");
    }

    @Test
    public void should_return_locale_when_strLength_is_5() {
        assertThat(localeUtils.toLocale("_AB_DE").getCountry()).isEqualTo("AB");
    }

    @Test
    public void should_return_locale_when_str_is_ISO639LanguageCode_and_length_is_2() {
        assertThat(localeUtils.toLocale("ab").getLanguage()).isEqualTo("ab");
    }

    @Test
    public void should_return_locale_when_str_is_ISO639LanguageCode_and_length_is_3() {
        assertThat(localeUtils.toLocale("abc").getLanguage()).isEqualTo("abc");
    }

    @Test
    public void should_return_locale_include_language_country_when_str_is_abc_AB() {
        assertThat(localeUtils.toLocale("abc_AB").getLanguage()).isEqualTo("abc");
        assertThat(localeUtils.toLocale("abc_AB").getCountry()).isEqualTo("AB");
    }

    @Test
    public void should_return_locale_include_language_country_when_str_is_abc_123() {
        assertThat(localeUtils.toLocale("abc_123").getLanguage()).isEqualTo("abc");
        assertThat(localeUtils.toLocale("abc_123").getCountry()).isEqualTo("123");
    }

    @Test
    public void should_return_locale_include_language_country_variant_when_str_is_abc_123_ef() {
        assertThat(localeUtils.toLocale("abc_123_ab").getLanguage()).isEqualTo("abc");
        assertThat(localeUtils.toLocale("abc_123_ab").getCountry()).isEqualTo("123");
        assertThat(localeUtils.toLocale("abc_123_ef").getVariant()).isEqualTo("ef");
    }

    @Test
    public void should_throw_exception_when_str_is_abc_123_ef_d() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: abc_123_ef_d");
        localeUtils.toLocale("abc_123_ef_d");
    }
    @Test
    public void should_throw_exception_when_str_substring_is_not_ISO3166CountryCode() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: abc_aB");
        localeUtils.toLocale("abc_aB");
    }
    @Test
    public void should_throw_exception_when_str_is_not_ISO639LanguageCode() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: abC");
        localeUtils.toLocale("abC");
    }
    @Test
    public void should_throw_exception_when_str_substring_is_not_NumericAreaCode() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: abc_");
        localeUtils.toLocale("abc_");
    }
    @Test
    public void should_throw_exception_when_parsed_variant_length_is_0() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid locale format: abc_AB_");
        localeUtils.toLocale("abc_AB_");
    }
}

删除上面的单元测试,尝试用ChatGPT来自动化为上面的class编写单元测试,如下图所示:左边是输入的prompt,右边是ChatGPT生成的代码。

生成的单元测试的名称不是用下滑线分割,但是我更喜欢用下滑线来分割单元测试名称,另外,默认是用Assert来进行断言,我更希望用AssertJ来作为断言库,那么可以在上面的promp的基础上进行修改,结果如下所示:除了修改单元测试名称和断言库外,上一版本生成的单元测试中对于异常的验证使用了assertThrows方法,实际该方法不存在,所以再次修改promp,让chatGPT用ExpectedException来编写异常情况的case。

经过上面的修改后,编写全新的prompt,让chatGPT再次生成新的单元测试,修改后的Prompt如下所示:,copy单元测试到IDE工具上,虽然得到的覆盖率有点低(如下所示),但可直接运行,无任何报错:

此时,再修改prompt添加了覆盖率的要求,此时,chatGPT对私有方法编写了单元测试,但同时也给出了提示信息“不建议对私有方法编写单元测试,应该直接调用公有方法进行覆盖”,具体如下所示:

另外,因为ChatGPT默认返回的tokens数量是4096,这包括输入的prompt的tokens个数和返回的response的tokens个数,所以,对于很长的代码,一次性生成完整的单元测试有难度,针对这种情况,建议在生成的基础版本上有针对的添加剩余的单元测试,即给ChatGPT更多的上下文信息来驱动生成单元测试。以下图为例,查看未覆盖的代码,针对性的给出prompt,让单元测试进一步完善。

修改Prompt,针对性的补充未覆盖的单元测试,修改后的prompt和自动生产的单元测试结果如下所示:可以看到单元测试中生成了len==3的case,另外还生成了len大于4的case,而对于边界值校验来说,真正需要的len是等于5和小于5且不等于3的情况,例如len==4的case,所以,在自动生成的基础上稍微修改下input就可以达到这个效果。

总结而言,在prompt中基础的输入信息是"用junit,assertjs编写单元测试,且单元测试方法名称用下划线分割,方法名称以should开头,异常验证部分使用Junit中的ExpectedException",在基础prompt上,再结合实际情况输入针对性信息,即可借助chatGPT编写单元测试。

另外,需要注意一点:chatGPT有tokens的限制,所以,对于比较大的class,需要分段输入给chatGPT,否则返回的response结果有限。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

利用ChatGPT协助编写单元测试 的相关文章

随机推荐

  • 80端口被占用时的终极解决方法

    摘要 之前在某次安全测试时 遇到一个80端口被占用的坑 将解决方法共享出来 使用netstat ano 命令查看是哪个进程正在占用80端口 之前在某次安全测试时 遇到一个80端口被占用的坑 将解决方法共享出来 使netstat ano 命令
  • linux nexus 启动失败_学习笔记之——nexus(四)记一次nexus故障

    一 故障描述 nexus服务器最初配置为4C8G 随着业务量的暴增 终于在某一天不堪重负 OOM了 排查后 增加内存到16G 再次启动 然后看似正常 然后却发生了诡异的事件 二 排查过程 问题一 查看日志 发现日志报错如下 报错日志1 经确
  • imx6ull视频监控项目,从kernel,buildroot,nginx,ffmpeg实现摄像头推流,vlc及web拉流

    写这一篇目的是记录自己使用buildroot 构建根文件系统 实现摄像头推流到VLC 及 web端 主要介绍的是 1 buildroot构建根文件系统 2 ffmpeg 及nginx 的配置 3 Linux内核构建 4 如何将摄像头的视频在
  • 软件项目管理课程授课教案

    序 软件项目管理概述 第一篇 软件项目初始 第1章 软件项目初始过程 第二篇 软件项目计划 第2章 软件项目范围计划 第3章 软件项目进度计划 第4章 软件项目成本计划 第5章 软件项目质量计划 第6章 软件项目人力资源计划 第7章 软件项
  • 灵创系统服务器,服装ERP-灵创软件-ICSCM供应链管理系统

    服装ERP 灵创软件 ICSCM供应链管理系统 一 产品概述 ICSCM供应链管理系统是基于品牌营运商和产品生产商之间紧密沟通 风险利润分担的大前提下诞生的一套全B S结构的系统 它是一种致力于在企业与供应商之间建立和维持长久的紧密合作伙伴
  • STM32系列(HAL库)——F103C8T6获取DHT11温湿度串口打印

    本文参考此篇博客并在其基础上进行了修改 STM32F103驱动DHT11温湿度传感器 STM32MXcube HAL 在此特别鸣谢原文博主 1 软件准备 1 编程平台 Keil5 2 CubeMX 3 XCOM 串口调试助手 2 硬件准备
  • Manifest.json文档说明

    Manifest json文件是5 移动App的配置文件 用于指定应用的显示名称 图标 应用入口文件地址及需要使用的设备权限等信息 是扩展的配置文件 指明了扩展的各种信息 一个manifest json格式如下 必须的字段3个 name M
  • Spring bean生命周期详解

    Spring Bean的完整生命周期从创建Spring容器开始 直到最终Spring容器销毁Bean 这其中包含了一系列关键点 Spring bean生命周期 四个阶段 Bean的实例化阶段 Bean的设置属性阶段 Bean的 初始化阶段
  • token续期

    需求 项目前后端分离 采用token jwt生成 方式作为登录及接口验证 自然而然就会涉及token超时 影响用户体验的问题 要解决的就是如果用户一直点击页面 就不应该出现超时及重新登录 只有用户在设置的超时时间内 一次页面操作都没有 才定
  • 大白话给你说清楚什么是过拟合、欠拟合以及对应措施

    开始我是很难弄懂什么是过拟合 什么是欠拟合以及造成两者的各自原因以及相应的解决办法 学习了一段时间机器学习和深度学习后 分享下自己的观点 方便初学者能很好很形象地理解上面的问题 同时如果有误的地方希望大家在评论区留下你们的砖头 我会进行纠正
  • 计算机故障诊断知识,故障诊断

    利用各种检查和测试方法 发现系统和设备是否存在故障的过程是故障检测 而进一步确定故障所在大致部位的过程是故障定位 故障检测和故障定位同属网络生存性范畴 要求把故障定位到实施修理时可更换的产品层次 可更换单位 的过程称为故障隔离 故障诊断就是
  • UE4Material_材质属性(1)

    材质中的属性 物理材质 Phys Material 物理材质 与该材质关联的物理材质 物理材质 Physical Material 提供了物理属性的定义 例如碰撞 弹力 以及其他基于物理的方面会保留多少能量 物理材质 Physical Ma
  • 一篇教会你,Redis主从、哨兵、 Cluster集群。

    前言 大家好 今天跟小伙伴们一起学习Redis的主从 哨兵 Redis Cluster集群 Redis主从 Redis哨兵 Redis Cluster集群 1 Redis 主从 面试官经常会问到Redis的高可用 Redis高可用回答包括两
  • TCP的三次握手及四次挥手总结(从抓包角度理解)

    目录 TCP报文首部 TCP连接 传输及断开过程图 TCP状态图 三次握手过程理解 四次挥手过程理解 从抓包来理解TCP建立连接 数据传输以及断开连接的过程 建立连接过程 数据传输过程 连接断开过程 为什么连接的时候是三次握手 关闭的时候却
  • Keras查看model weights .h5 文件的内容

    Keras的模型是用hdf5存储的 如果想要查看模型 keras提供了get weights的函数可以查看 for layer in model layers weights layer get weights list of numpy
  • 多进程浏览器框架

    为什么浏览器采用多进程模型 转载于 http www wtoutiao com p s57age html Google Chrome源码剖析 一 多线程模型 转载于 http www ha97 com 2908 html 主流浏览器多进程
  • 【广州华锐互动】无人值守变电站AR虚拟测控平台

    无人值守变电站AR虚拟测控平台是一种基于增强现实技术的电力设备巡检系统 它可以利用增强现实技术将虚拟信息叠加在真实场景中 帮助巡检人员更加高效地完成巡检任务 这种系统的出现 不仅提高了巡检效率和准确性 还降低了巡检成本和风险 传统的变电站巡
  • TPM功能介绍

    文章来源 TPM功能介绍 百度文库 http wenku baidu com link url bQMQyb0A3gto0CCC2CN5ojpUrgHsh8BMXmejpFaqLS52v 013bXPHoRr36r0F0UrgPr8U6rv
  • MATLAB——FFT(快速傅里叶变换)

    基础知识 FFT即快速傅里叶变换 利用周期性和可约性 减少了DFT的运算量 常见的有按时间抽取的基2算法 DIT FFT 按频率抽取的基2算法 DIF FFT 1 利用自带函数fft进行快速傅里叶变换 若已知序列 x 4 3
  • 利用ChatGPT协助编写单元测试

    ChatGPT自从2022年推出以来受到很多人的喜欢 此篇博客重点介绍如何修改Prompt来自动生成较理想的单元测试 如下图所示的一段代码 该class中有一个public方法toLocale 其余都是private方法 toLocale