Java后端开发单元测试

测试概览

测试是用于促进鉴定软件正确性、完整性、安全性和软件质量的过程。在开发的过程中测试是必不可少的,测试一般分为四个阶段:单元测试集成测试系统测试验收测试;对于后端开发人员而言,需要会单元测试和集成测试。测试的方法一般分为三种:白盒测试黑盒测试灰盒测试

白盒测试

白盒测试是清楚软件的内部结构,清楚其源代码逻辑,是用于验证源代码和代码逻辑的正确性的。其中,单元测试就属于白盒测试。

黑盒测试

黑盒测试是不清楚软件的内部结构,不清楚源代码逻辑,是用于验证软件的功能、和兼容性方面问题的。其中,系统测试和验收测试都属于黑盒测试。

灰盒测试

灰盒测试顾名思义,兼顾了白盒与黑盒测试的特点,既要关注软件的内部结构,又要考虑软件的外在表现。其中,集成测试就属于灰盒测试。

单元测试

单元测试:是针对程序的最小的功能单元(方法),编写测试代码对其正确性进行测试。

JUnit测试框架进行单元测试

JUnit是目前最流行的Java测试框架之一,提供了一些功能,方便程序进行单元测试(第三方公司提供);在使用JUnit这样的测试框架之前,我们一般是通过编写测试类通过main方法对代码进行测试的,这样做从功能上而言,是可以达到一样的效果,但实际上还是存在一些问题:

  1. 测试代码和源代码没有区分,写在一起,难以维护。
  2. main方法测试多个功能,假如一个功能测试失败,程序直接停止,影响后面的功能测试。
  3. 无法自动化测试,得到测试报告。

这些是用main方法测试的局限,但是使用了JUnit框架进行测试,就可以弥补这些不足:JUnit的测试代码和源代码是分开的,便于维护;JUnit可以根据需要自动化测试,功能测试之间相互不会影响;JUnit可以自动分析测试结果,产出测试报告,测试更加高效。 所以说推荐使用JUnit测试框架进行单元测试。

使用JUnit测试框架

JUnit是第三方提供的测试框架,在Maven项目中,需要在pom.xml文件中引入其maven坐标:

然后在test/java目录下,创建一个测试类,然后编写对应的测试方法(一般而言,测试类都叫xxxxxTest),并且在方法(方法一般叫testXxxxx)上使用@Test注解(这是JUnit提供的注解)表明这是一个测试方法。 注意,Junit中单元测试的方法必须声明为public void,否则无法测试! 根据以上规则,举一个例子:现在有一个UserService类,其中有两个方法,一个是根据用户的身份证判断用户的年龄,一个是根据用户的身份证判断用户的性别:

public class UserService {
    /**
     * 根据身份证号码,计算出用户的年龄
     * @param idCard 身份证号码
     * @return 用户年龄
     */
    public Integer getAge(String idCard) {
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        String birthday = idCard.substring(6, 14);
        LocalDate parse = LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyyMMdd"));
        return Period.between(parse, LocalDate.now()).getYears();
    }

    /**
     * 根据身份证号码判断用户性别
     * @param idCard 身份证号码
     * @return 性别
     */
    public String getGender(String idCard) {
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        return Integer.parseInt(idCard.substring(16, 17)) % 2 == 1 ? "男" : "女";
    }
}

编写一个基于JUnit测试框架的测试类和测试方法测试这两个方法:

  1. 首先在test/java中创建测试类:
    public class UserServiceTest {
    
        }
    }
    
  2. 在测试类中编写测试方法,并在方法上添加@Test注解,标识这是一个基于Junt测试框架的测试方法:
    public class UserServiceTest {
    
        @Test
        public void testGetAge() {
            UserService userService = new UserService();
            Integer age = userService.getAge("100000200410141011");
            System.out.println(age);
        }
    }
    
  3. 然后运行测试方法,获取测试结果:点击方法旁边的箭头,即可运行测试方法:

 JUnit框架可以自动获取测试结果,绿色代表测试成功:

现在可以看到,UserService中的getAge方法已经绿了,并且还标明了测试通过,这是否意味着这个方法就没有问题,代表测试通过了呢?其实并不是这样的。

断言

当一个方法像上面的例子一样被标明测试通过,只能说明该方法不存在语法问题,没有抛出异常,但是并不意味着方法本身的逻辑没有问题,比如我故意将getGender方法的逻辑修改错误:

/**
 * 根据身份证号码判断用户性别
 * @param idCard 身份证号码
 * @return 性别
 */
public String getGender(String idCard) {
    if (idCard == null || idCard.length() != 18) {
        throw new IllegalArgumentException("无效的身份证号码");
    }
    return Integer.parseInt(idCard.substring(16, 17)) % 2 == 0 ? "男" : "女";
}

我国身份证的第17位是奇数的是男性,是偶数的是女性,此处故意将其改错,然后使用身份证号码"100000200410141011"进行测试,按理来说,这个身份证号码对应的应该是一个男性,但是当我们测试这个方法将会发现一些不对:

我们发现本来应该判断得男性的身份证号码经过我们编写的错误的getGender方法居然被判断为了女性,这说明我们的getGender方法是有问题的;但是JUnit测试居然通过了,说明只单纯依靠运行是无法正确测试的,此时就需要使用到断言来增加测试的可靠性。

JUnit框架提供了一些辅助方法,帮助我们确定被测试的方法是否按照预期的效果正常工作,这样的方式被叫做断言。在JUnit中实现断言,需要使用Assertions工具类中提供的静态方法,以下是一些常见的断言方法:

注:这些方法形参中的最后一个参数msg,表示错误提示信息,可以不指定。(Assertions中有对应的重载方法),通过断言可以增加测试的可靠性:

@Test
public void testGetGenderWithAssert() {
    UserService userService = new UserService();
    String gender = userService.getGender("100000200410141011");
    Assertions.assertEquals("男", gender);
}

此时,根据身份证号码"100000200410141011",期望的gender是男,但是由于我们方法逻辑的错误,所以说实际得到的gender是女,assertEquals方法将报错,测试方法不通过:

根据控制台的输出,预期得到的gender是男,实际得到的gender是女,预期和实际不符所以说该方法报错,这样我们就应该去检查以下getGender方法的逻辑,修改逻辑错误后再次测试:

修改逻辑之后,预期和实际得到一样的结果,单元测试就通过了。用于断言的方法还有很多,此处就不一一演示。

JUnit常见注解

JUnit测试框架中还提供了一些注解,用于增强其功能,常见的一些注解如下图所示:

@Before...和@After...

首先先演示一下@BeforeEach和@AfterEach;@BeforeAll和@AfterAll这四个注解,由于@BeforeEach和@BeforeAll会在每一个测试方法之前执行一次(在所有测试方法之前执行一次),所以说这两个注解一般是用于资源准备或初始化工作的;同理@AfterEach和@AfterAll会在每一个测试方法之后执行一次(在所有测试方法之后执行一次),所以说这两个注解一般是用于资源释放或清理工作的:

// @BeforeAll和@AfterAll只能修饰static方法
@BeforeAll
public static void beforeAll() {
    System.out.println("before all");
}

@AfterAll
public static void afterAll() {
    System.out.println("after all");
}

@BeforeEach
public void beforeEach() {
    System.out.println("before each");
}

@AfterEach
public void afterEach() {
    System.out.println("after each");
}

通过点击测试类旁边的箭头,运行整个测试类(测试类中的所有测试方法依次运行),查看这四个注解的运行结果:

和预想的一样:@BeforeAll方法是在所有的测试方法之前运行的,只会运行一次;@BeforeEach方法是在每一个测试方法运行前运行的,可以运行多次;@AfterEach方法是在每一个测试方法运行之后运行的,可以运行多次;@AfterAll方法是在所有的测试方法运行之后运行的,只会运行一次(其中有些方法是通过Assertions断言的,所以说没有输出,实际上是运行了该方法的)。

参数化测试注解@ParameterizedTest

参数化测试注解,可以让单个测试运行多次,每次测试时仅参数不同,这样就可以大大提高测试效率。参数化测试需要配合使用@ParameterizedTest注解和@ValueSource注解,@ValueSource注解为参数化测试提供参数来源。

@ParameterizedTest
@ValueSource(strings = {"100000200410141011", "100000200410141031", "100000200410141051"})
public void testGetGenderWithAssertAndParameterized(String idCard) {
    UserService userService = new UserService();
    String gender = userService.getGender(idCard);
    Assertions.assertEquals("男", gender);
}

这个测试方法使用了参数化测试注解,并且在@ValueSource中提供了三个男性身份证号码(请特别注意:@ValueSource中提供的参数必须是同类的,比如全部是男或者全部是女,否则无论如何都无法通过Assertions断言!!!)

可以看到,通过一次测试,测试了三个数据,单个测试调用了多次,每一次只是参数不同,这样就极大的提高了我们的测试效率。

@DisplayName指定测试类、测试方法名

@DisplayName可以指定测试类、测试方法的名称(其默认名为类名和方法名),比如有些方法名字过长,那么就可以给其指定一个便于观察的名字(可以是中文):

@ParameterizedTest
@ValueSource(strings = {"100000200410141011", "100000200410141031", "100000200410141051"})
@DisplayName("断言和参数化测试getGender方法")
public void testGetGenderWithAssertAndParameterized(String idCard) {
    UserService userService = new UserService();
    String gender = userService.getGender(idCard);
    Assertions.assertEquals("男", gender);
}

 

单元测试企业开发规范

原则:编写测试方法时,要尽可能覆盖业务方法中所有的情况,特别需要注意边界值和一些特殊值! 比如说我们需要用户传递一个正常的合法的18位身份证号码(String类型,先不考虑其是否存在),那么在测试的时候,我们就要多方面考虑有可能会传递的身份证号码:比如传递一个null值,传递一个空字符串,传递一个不足18位的字符串,传递一个超过18位的字符串…… 这些都是比较典型的错误值,都是需要测试的;不单单需要考虑特殊值,还需要考虑测试一些正常的男性(女性)身份证号码,看看是否能够通过测试:

import com.wzb.service.UserService;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
 * 用户测试类
 */
public class UserTest {
    private UserService userService;

    /**
     * 每一个测试方法运行前,都自动创建一个UserService对象
     */
    @BeforeEach
    public void setUp() {
        userService = new UserService();
    }

    @Test
    @DisplayName("获取性别:null")
    public void testGetGenderWithNull() {
        Assertions.assertThrows(IllegalArgumentException.class, new NamedExecutable() {
            @Override
            public void execute() throws Throwable {
                userService.getGender(null);
            }
        });
    }

    @Test
    @DisplayName("获取性别:空串")
    public void testGetGenderWithEmpty() {
        Assertions.assertThrows(IllegalArgumentException.class, new NamedExecutable() {
            @Override
            public void execute() throws Throwable {
                userService.getGender("");
            }
        });
    }

    @Test
    @DisplayName("获取性别,长度不足")
    public void testGetGenderWithShort() {
        Assertions.assertThrows(IllegalArgumentException.class, new NamedExecutable() {
            @Override
            public void execute() throws Throwable {
                userService.getGender("1000002004101");
            }
        });
    }

    @Test
    @DisplayName("获取性别,长度过长")
    public void testGetGenderWithLong() {
        Assertions.assertThrows(IllegalArgumentException.class, new NamedExecutable() {
            @Override
            public void execute() throws Throwable {
                userService.getGender("1000002004101410110000000");
            }
        });
    }

    @DisplayName("获取性别:男")
    @ParameterizedTest
    @ValueSource(strings = {"100000200410141011", "100000200410141031", "100000200410141051"})
    public void testGetGenderWithMan(String idCard) {
        String gender = userService.getGender(idCard);
        Assertions.assertEquals("男", gender);
    }

    @DisplayName("获取性别:女")
    @ParameterizedTest
    @ValueSource(strings = {"100000200410141021", "100000200410141041", "100000200410141061"})
    public void testGetGenderWithWoman(String idCard) {
        String gender = userService.getGender(idCard);
        Assertions.assertEquals("女", gender);
    }

    @AfterAll
    public static void end() {
        System.out.println("测试完毕");
    }
}

在这个测试类中,首先使用了@BeforeEach注解,在每一个测试方法运行之前,都会创建一个UserSerivice对象,避免重复创建对象,减少代码量;然后对于不同的特殊值(null,空,长度不同)进行测试,结合Assertions中的assertThrows方法对于这些错误值抛出的异常类型进行判断;最后结合@ParameterizedTest注解和@ValueSource注解进行参数化测试,分别对男女两种情况提供几组数据进行测试;最后在所有测试方法执行完毕后输出测试完毕进行提示。

所有的测试方法都是通过的,上文还提到,Junit测试框架可以自动化测试,得到测试报告,只需要在运行时选择使用覆盖率运行,就可以自动生成测试方法的覆盖率了:

覆盖率也可以一定程度上反映测试的合理、可靠性,所以说后端开发在进行单元测试的时候,需要尽量将覆盖率提高,测试得更加完善,便于后续的集成测试和系统测试。 

 

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/950602.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LAMP搭建

LAMP搭建 引子:本篇文章为LAMP的搭建流程,其中L(Ubuntu)、A(Apache)、M(Mysql)、P(PHP)。 一、L → Ubuntu Step 1:在Vmware Workstation中使…

LabVIEW 系统诊断

LabVIEW 系统诊断是指通过各种工具和方法检测、评估、分析和解决 LabVIEW 程序和硬件系统中可能存在的故障和性能问题。系统诊断不仅涵盖软件层面的调试与优化,还包括硬件交互、数据传输、实时性能等方面的检查和分析。一个成功的系统诊断能够显著提升LabVIEW应用程…

基于 GEE 提取白莲种植范围

目录 1 方法原理 1.1 步骤一 1.2 步骤二 1.3 步骤三 1.4 步骤四 2 完整代码 3 运行结果 近年来,随着乡村振兴战略的提出,我国的农业种植模式呈现出多元化的趋势。白莲具有易种植、经济效益高的特点,由此被广泛种植,本文介绍…

el-table 自定义表头颜色

第一种方法&#xff1a;计算属性 <template><div><el-table:data"formData.detail"border stripehighlight-current-row:cell-style"{ text-align: center }":header-cell-style"headerCellStyle"><el-table-column fixed…

c++类和对象---上

文章目录 类的介绍 类的声明 1.1 类名 1.2 成员变量 1.3 成员函数 1.4 访问权限 类的定义 2.1 成员变量的初始化 2.2 成员函数的实现 对象的创建和销毁 3.1 默认构造函数 3.2 析构函数 3.3 拷贝构造函数 3.4 对象的实例化 3.5 对象的销毁 成员访问控制 4.1 公有成员 4.2 私有…

上汽乘用车研发流程

目的 最近刚入职主机厂&#xff0c;工作中所提到各个阶段名称与之前在供应商那边不一致&#xff0c;概念有点模糊&#xff0c;所以打算学习了解一番 概念 术语 EP: enginerring prototype car 工程样车 Mule Car: 骡子车 Simulator Car&#xff1a;模拟样车 PPV&#xff1a;…

阿里云发现后门webshell,怎么处理,怎么解决?

当收到如下阿里云通知邮件时&#xff0c;大部分管理员都会心里一惊吧&#xff01;出现Webshell&#xff0c;大概是网站被入侵了。 尊敬的 xxxaliyun.com&#xff1a; 云盾云安全中心检测到您的服务器&#xff1a;47.108.x.xx&#xff08;xx机&#xff09;出现了紧急安全事件…

vite5.x配置https

旧版的vite直接在config里面配置https&#xff1a;true即可&#xff0c;新版的麻烦一些。 1.准备工作 需要安装openssl 下载地址&#xff1a;Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions 找到合适的版本安装&#xff0c;配置好环境变量&#x…

深度学习与计算机视觉 (博士)

文章目录 零、计算机视觉概述一、深度学习相关概念1.学习率η2.batchsize和epoch3.端到端(End-to-End)、序列到序列(Seq-to-Seq)4.消融实验5.学习方式6.监督学习的方式(1)有监督学习(2)强监督学习(3)弱监督学习(4)半监督学习(5)自监督学习(6)无监督学习(7)总结&#xff1a;不同…

在AI浪潮中,RSS3为何会被低估其价值?有何潜力

​​RSS3 简介&#xff1a; RSS3 是一个去中心化网络索引和结构化开放信息&#xff0c;使其对于下一个 Twitter、Google 和 OpenAI 来说易于访问且有价值。凭借独特的数据子层价值子层设计&#xff0c; RSS3 网络推动了开放信息从索引到消费等的全生命周期&#xff0c;并建立了…

BMS应用软件开发 — 3 电池系统的组成

目录 1 电池的基本拓扑 2 已经被淘汰的CTM 3 早已经普及的CTP 4 集成度更高的CTC 5 刚性更好的CTB 1 电池的基本拓扑 相比于燃油车&#xff0c;虽然电动车在结构空间上灵活度更高&#xff0c;空间利用率也更好&#xff0c;但现有条件下无法像燃油车一样快速补能&#xff…

UE5 打包要点

------------------------- 1、需要环境 win sdk &#xff0c;大约3G VS&#xff0c;大约10G 不安装就无法打包&#xff0c;就是这么简单。 ----------------------- 2、打包设置 编译类型&#xff0c;开发、调试、发行 项目设置-地图和模式&#xff0c;默认地图 项目…

高等数学学习笔记 ☞ 一元函数微分的基础知识

1. 微分的定义 &#xff08;1&#xff09;定义&#xff1a;设函数在点的某领域内有定义&#xff0c;取附近的点&#xff0c;对应的函数值分别为和&#xff0c; 令&#xff0c;若可以表示成&#xff0c;则称函数在点是可微的。 【 若函数在点是可微的&#xff0c;则可以表达为】…

Redis查询缓存

什么是缓存&#xff1f; 缓存是一种提高数据访问效率的技术&#xff0c;通过在内存中存储数据的副本来减少对数据库或其他慢速存储设备的频繁访问。缓存通常用于存储热点数据或计算代价高的结果&#xff0c;以加快响应速度。 添加Redis缓存有什么好处&#xff1f; Redis 基…

3D立体无人机夜间表演技术详解

3D立体无人机夜间表演技术是一种结合了无人机技术、灯光艺术和计算机编程的创新表演形式。以下是该技术的详细解析&#xff1a; 一、技术基础 1. 无人机技术&#xff1a; 无人机通常采用四旋翼设计&#xff0c;具有强大的飞行控制能力&#xff0c;可以实现前飞、后飞、悬停、…

MATLAB深度学习实战文字识别

文章目录 前言视频演示效果1.DB文字定位环境配置安装教程与资源说明1.1 DB概述1.2 DB算法原理1.2.1 整体框架1.2.2 特征提取网络Resnet1.2.3 自适应阈值1.2.4 文字区域标注生成1.2.5 DB文字定位模型训练 2.CRNN文字识别2.1 CRNN概述2.2 CRNN原理2.2.1 CRNN网络架构实现2.2.2 CN…

H2数据库在单元测试中的应用

H2数据库特征 用比较简洁的话来介绍h2数据库&#xff0c;就是一款轻量级的内存数据库&#xff0c;支持标准的SQL语法和JDBC API&#xff0c;工业领域中&#xff0c;一般会使用h2来进行单元测试。 这里贴一下h2数据库的主要特征 Very fast database engineOpen sourceWritten…

Android 10.0 授权app获取cpu温度和电池温度功能实现

1.前言 在10.0的系统定制化开发中&#xff0c;在开发某些产品的老化应用的时候&#xff0c;需要app获取cpu温度和电池 温度等功能&#xff0c;有些产品带温度传感器&#xff0c;大部分的产品都不包含温度传感器&#xff0c;所以就需要读取 sys下的相关节点来获取相关温度值 2.…

IDEA 撤销 merge 操作(详解)

作为一个开发者&#xff0c;我们都知道Git是一个非常重要的版本控制工具&#xff0c;尤其是在协作开发的过程中。然而&#xff0c;在使用Git的过程中难免会踩一些坑&#xff0c;今天我来给大家分享一个我曾经遇到的问题&#xff1a;在使用IDEA中进行merge操作后如何撤销错误的合…

WD5105同步降压转换器:9.2V-95V宽电压输入,4.5A大电流输出,95%高效率,多重保护功能

概述 • WD5105同步降压转换器 • 封装形式&#xff1a;QFN-20封装 • 应用场景&#xff1a;适用于车载充电器、电动车仪表、电信基站电源、电源适配器等 性能特点 • 输入电压范围&#xff1a;9.2V至95V • 输出电流&#xff1a;可提供4.5A连续负载电流 • 效率&#xff1a;高…