摘要: 软件测试是软件开发过程过程中难以分割的一部分,且随着软件规模的快速增大,其重要性也不断地凸显出来,可以说,没有软件测试的软件开发是不完整的开发过程。一个好的软件测试工具能够大大减少测试人员的工作量,并能在极大程度上推动软件开发过程高质量地进行。因此,掌握一两种当前社会主流的软件测试工具并能够独立高效的完成一个简单的软件测试过程对于一个大学生而言显得无比的重要。本文主要讲解的是模块测试工具Junity5从安装到使用的过程,并与其测试用例相结合,来分析该工具的适用范围与特点。
关键字: 测试工具,测试用例,使用
JUnit5是最新的JUnit框架,与之前的JUnit框架有许多不同,它由三个不同子项目的几个不同模块构成,它们分别是JUnit Platform、JUnit Jupiter和JUnit Vintage。
JUnit Platform是在 JVM 上启动测试框架的基础。它还定义了用于开发在平台上运行的测试框架的 API。此外,该平台还提供了一个控制台启动器,用于从命令行启动平台,并提供JUnit平台套件引擎,以便在平台上使用一个或多个测试引擎运行自定义测试套件。对 JUnit 平台的一流支持也存在于流行的 IDE和构建工具(请参见 Gradle、Maven 和 Ant)中。
JUnit Jupiter 是编程模型和扩展模型的组合,用于在JUnit 5中编写测试和扩展。Jupiter 子项目提供了一个在平台上运行基于Jupiter 的测试。
JUnit Vintage起到了一个向上兼容的作用,提供了一个用于在平台上运行基于JUnit 3和JUnit 4的测试,使得之前的版本也能够运行。它要求类路径或模块路径上存在 JUnit 4.12 或更高版本。
在项目的pom.xml配置文件中添加依赖
<dependency>
<!--添加junity5依赖-->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!--添加junity5的参数化依赖-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
在加载完毕后即可使用JUnity5进行测试。
JUnit5有其默认的命名规则,即:以Test开头或者结尾,两者满足一个即可。在IDEA中并没有针对文件名做限制,也就是说idea中的测试程序的命名并没有强制要求,即便本测试用例不符合命名规范,idea依旧会运行。
但在使用maven(控制台运行程序)构建时,则不会收集不满足规则要求的用例,换言之,不符合命名规则的测试用例将不会被执行。
与默认的命名规则对应的还有个人制定的命名规则,不过,在使用个人制定的命名规则时,需要在pom文件中配置相关的一些surefire插件
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<includes>
<include>/*Hogwarts*.java</include>
<include>/*TestCase.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
此时如果测试文件名含有Hogwarts/TestCase,那么也会被识别为测试用例,但一旦配置了测试用例,那么默认的Test规则就会失效。
断言是编写单元测试用例的核心方式,即期望值是多少,测试的结果是多少,以此来判断测试是否通过。在JUnit Jupiter中,所有的断言都是org.junit.jupiter.api.Assertions 类中static方法。
常见的断言有以下几种:
1.assertEquals(int expected, int actual)
该方法中第一个参数为预期值,第二个参数为实际值。通过比较两个参数是否相同得出的结果。
2.assertTrue(boolean condition)
该断言中只有一个参数,断言所期待的参数为true时,如果里面的值不为true,则判断断言执行失败。该方法中的参数可以是判断表达式或者是单纯的布尔类型。
3.assertNotNull([String message],Object actual)
该方法中,传入的参数只有一个。若传入参数为不为空,则一切正常,若为空打印message。
4.assertAll(String heading, Executable… executables)
当我们对大量的断言式进行判断时,当其中某一个断言判断结果为错,之后的每一个断言式无论正确与否都不会被执行。在这种情况下,为了能够对每一个输入的断言都进行测试我们可以采用assertAll方法。
该方法采用分组断言,如果一个用例存在多个断言,使用assertAll可以执行每个断言,且返回结果。第一个heading 参数是一个描述信息,代表了这个分组断言是针对什么场景,也可以不传,之后的是该组断言的内容。
首先介绍一下@ParameterizedTest注解。
通常,在进行测试时常会遇到这样的情况,同一个测试案例,改变的只是测试时候输入的参数不同。按照之前的做法,可能会是通过每个输入参数都写一个测试,或者将测试参数封装到集合中循环遍历执行测试。在Junit5中,提供了一种更加高效的方式来进行。该特性可以让我们运行单个测试多次,且使得每次运行仅仅是参数不同而已。
该注解可以声明测试类是-个参数化的测试类,从而将外界的数据传入测试程序当中,能够有效地将测试数据与运行程序分开,提高代码的可读性,同时能够方便测试人员对每一次的测试数据进行保存与整理,在极大程度上提高测试人员的工作效率。
(1)单参数数据输入
使用单参数注解@ValueSource传递数据内容,在传递参数的过程中,需要通过ValueSource定义的关键字进行类型声明,具体的类型名称如下图中左侧所示,右侧为其对应的参数类型。
图1:参数名称与原参数类型
该方法一般与前面的断言式或者用户自定义的测试方法同时使用,只为方便用户在输入参数是能够得到简化。
(2)多参数数据输入
① CsvSource
一般常用@CsvSource()注解来导入数据,但此时导入的数据其实只是注解括号当中的内容,所以其实并不适合大量数据的测试,在此只做简单介绍。CsvSource注解传递参数化数据,传递的参数格式是一个集合,可单可多。如果是多个参数,使用默认分隔符","分开。比如:
@CsvSource({“哈利,2”,“赫敏,22”,“罗恩,12”})
此时传入的数据实际上是三组,每组有通过“,”分成两个数据。其中的默认分隔符可以通过delimiterString指定分隔符,使用value指定数据源。
② CsvFileSource
为了能够在真正意义上把代码和数据分开,同时避免连接数据库这种麻烦的事情发生,Junit5测试框架提供了@CsvFileSource()注解方法,可以导入resource资源目录下的资源文件(此处指csv数据文件)为测试程序提供测试用例的用例数据。其默认的数据分隔符还是“,”,以换行符作为元祖之间的分隔符。默认的分隔符可以通过delimiterString进行修改。
TestNG是一个java中的开源自动化测试框架,其灵感来自JUnit和NUnit,TestNG还涵盖了JUnit4整个核心的功能,但引入了一些新的功能,使其功能更强大,使用更方便。
表1:Junit5与TestNG比较
特点 | Junit5 | TestNG | 总结 |
---|---|---|---|
注解 | 支持注解 | 支持注解 | 都基于注解,用法、本质类似 |
易用性 | JUnit5嵌入了多种模块,需要通过JUnit Platform和JUnitJupite来编写测试用例 | 单一模块是TestNG的特点 | TestNG更易用 |
数据提供方式 | 支持多种测试数据提供方式,例如方法、枚举、CSV数据、CSV文件等 | 仅支持 provider方法和testng xml文件方式 | JUnit在注入测试数据方面做得更好 |
IDE支持 | 被Eclipse和IntelliJ IDEA等主流IDE支持 | 被Eclipse和IntelliJ IDEA等主流IDE支持 | 两者都被主流IDE所支持 |
断言 | JUnit5提供了足够多的断言方法来比较预期结果与实际结果 | TestNG提供了足够多的断言方法来比较预期结果与实际结果 | 两者在断言方面是类似的 |
通过对JUnit框架以及对以前学过的TestNG框架进行回顾分析,我发现当前测试用例或许是我能力不够,或者是学习深度不够,对于一些复杂的测试方法。尤其是通过过函数来获取测试数据等方法依旧不够灵活。而且JUnit5框架的局限性确确实实也如其文档开头所介绍的一样,适合于单元测试。
不过对我而言,其简化了开发流程这一事实是能够得到肯定的。其最引人注意的地方莫过于可以通过csv数据文件等方式来导入测试数据,这在极大程度上促进了我们的测试开发,尤其是方便了我们避开连接数据库这一步骤。
参考文献
[1]. Stefan Bechtold, Sam Brannen, Johannes Link, Matthias Merdes, Marc Philipp,Christian Stein.JUnit5 User Guide[R].2022.
[2]. 百度查阅中文参考格式.
附录A
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* 断言测试
*
* @author MaxBrooks chentingxian1954677@163.com
* @version 2022/10/20 10:32
* @since JDK17
*/
public class AssertEqTest {
@Test
void hogwarts(){
//断言添加成功
//第一个参数表示预期结果,第二个参数表示实际结果
assertEquals(2,1 + 1);
System.out.println("断言测试");
}
}
附录B
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* @author MaxBrooks chentingxian1954677@163.com
* @version 2022/10/20 10:44
* @since JDK17
*/
public class AssertNotNullTest {
@Test
void nullDemo(){
System.out.println("断言传值为空的场景");
assertNotNull("4564");
}
@Test
void notNullDemo(){
System.out.println("断言传值不为空的场景");
assertNotNull(1);
}
}
附录C
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* AssertTrue
*
* @author MaxBrooks chentingxian1954677@163.com
* @version 2022/10/20 10:35
* @since JDK17
*/
public class AssertTrueTest {
@Test
void expressionDemo(){
System.out.println("断言表达式为True");
assertTrue(3 > 1);
}
@Test
void boolDemo(){
System.out.println("断言布尔类型");
assertTrue(true);
}
}
附录D
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author MaxBrooks chentingxian195467@163.com
* @version 2022/10/20 10:50
* @since JDK17
*/
public class AssertAllTest {
@Test
void hogwarts(){
System.out.println("断言测试");
assertEquals(2,1);
System.out.println("2,1");
assertEquals(3,1 + 1);
System.out.println("2,1");
assertEquals(1,2);
System.out.println("1,2");
}
@Test
void assertAllDemo(){
System.out.println("断言测试 AssertAll");
// 分组断言,如果一个用例存在多个断言,使用assertAll可以执行每个断言,且返回结果
// 第一个heading 参数是一个描述信息,代表了这个分组断言是针对什么场景,也可以不传
assertAll("All",
() -> assertEquals(2,1+1),
() -> assertEquals(3,1+1),
() -> assertEquals(4,1+1)
);
}
}
附录E
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author MaxBrooks chentingxian1954677@163.com
* @version 2022/10/20 11:19
* @since JDK17
*/
public class ValueSourceDemoTest {
// @Test 尽量不要将@Test和@ParameterizedTest结合使用,如果使用,这用例多执行一次
// 1.将Test注解换为ParameterizedTest注解
//声明测试类是-个参数化的测试类
@ParameterizedTest
// 2.传递测试数据
//使用单参数注解@VolueSource传递》数化的数据内容
//传递参数的过程中,需要通过VolueSource定 义的关键字进行类型声明
@ValueSource(strings = {"张三","李四","王五"})
// @ValueSource(ints = {1,3,5,7,9})
// 3.在测试方法上面添加形参,接受参数化的数据
void valueSourceDemo(String name){
System.out.println(name);
//断言数据的长度是否等于2
assertEquals(name.length(),2);
}
}
附录F
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
/**
* @author MaxBrooks chentingxian1954677@163.com
* @version 2022/10/20 11:29
* @since JDK17
*/
public class CsvSourceDemoTest {
@ParameterizedTest // 1.使用此注解声明是-个参数化的测试类
// 2.使用CsvSource注解传递参数化数据,传递的参数格式是一个集合,
//如果是多个参数,使用默认分隔符","分开
@CsvSource({"哈利,2","赫敏,22","罗恩,12"})
void csvSourceDemo (String name , Integer age){
System.out.println(name+ "的年龄是:"+age);
}
@ParameterizedTest
// 使用 delimiterString 指定分隔符,使用 value 指定数据源
@CsvSource(value = {"哈利-2","赫敏-22","罗恩-12"}, delimiterString = "-")
void csvSourceDemo02 (String name , Integer age){
System.out.println(name+ "的年龄是:"+age);
}
}
附录G
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
/**
* CsvFileSourceDemoTest
*
* @author MaxBrooks chentingxian1954677@163.com
* @version 2022/10/20 16:18
* @since JDK17
*/
public class CsvFileSourceDemoTest {
/**
* 使用默认的分隔符","
* @param name 名字 来自于data.csv(resource中)
* @param age 年龄
*/
@ParameterizedTest // 1.声明参数化测试类
// 2.使用 CsvFileSource 注解声明参数化数据, 使用关键字resources指定文件
// 注意:文件名前面需要加 /
@CsvFileSource(resources = "/data/data.csv")
// 给测试方法添加形参
void csvSourceDemo01 (String name , Integer age){
System.out.println(name+ "的年龄是:"+age);
}
/**
* 使用自定义的分隔符"|"
* @param name 名字 来自于data02.csv(resource中)
* @param age 年龄
*/
@ParameterizedTest
@CsvFileSource(resources = "/data02.csv", delimiterString = "|")
void csvSourceDemo02 (String name , Integer age){
System.out.println(name+ "的年龄是:"+age);
}
}