前言
在慕课网上听了一位老师的JUnit的基础讲解,感觉对于新人来说还是很友好,故总结下来做此笔记
什么是单元测试
你的程序主要是由一个个的 Class 组成的,一个类或一个对象当然也是一个单元,而比类更小的单元是类的方法(函式)。如果你的类中的基本单元——如某些方法不能正常工作,在某些输入条件下会得出错误的执行结果,那么如何保证你的类/对象乃至整个应用软件或系统作为一个整体能正常工作呢?所以,简单说,单元测试(优先)的目的就是首先保证一个系统的基本组成单元、模块(如对象以及对象中的方法)能正常工作。
为什么要进行单元测试
看上去像是增加了代码量,毕竟光是业务代码就已经花了我们大部分时间精力,但是实际上却是减少了我们之后可能对代码修改的几率,尽可能的实现我们一次性编写就能成功的这个梦想。
工具
本次笔记采用JUnit4来进行测试。JUnit4比起JUnit3而言使用起来更加容易方便,而且目前为止要比JUnit5稳定,网上的参考文档也更多。编译器采用的eclipse。
什么是JUnit
基于测试驱动开发的测试框架,是xUnit系列的一个子系列。说简单点就是一个用于java测试的工具箱,里面有很多方法和规范可以供我们直接使用。
官网
- 第1条是关于如何在项目中配置JUnit,包括具体的jar包,在maven中的配置之类的,但由于本次实验所使用的编译器是Eclipse, Eclipse很好的集成了JUnit框架,所以我们也就不用再单独下载。
- 第2条是提供一套样例,方便初学者依葫芦画瓢。
- 第3条是下载的JUnit4的版本。
- 第4条JUnit的java API文档。里面详细讲述了各种方法。
- 第5条列举了jUnit使用的语法规范
- 第6条是指第三方扩展。
基本使用步骤
创建被测试类
- 在eclipse里新建一个工程,取名JUnitDemo,并新建一个名叫Number的类,包名写上my.demo。
- 给这个Number类里写上一些测试用方法,为了方便我们写加减乘除即可。
public class Number {
public int add(int a, int b) {
return a+b;
}
public int subtraction(int a ,int b) {
return a-b;
}
public int division(int a,int b) {
return a/b;
}
public int multiplication(int a,int b) {
return a*b;
}
}
- 先将jUnit所需的jar包引入项目。右键项目->Build Path->Add Libraries。
之后会弹出一个框,选择其中的JUnit
然后选择JUnit4,最后Finish即可。 - 创建测试类。为了方便我们以后对测试类进行管理,建议在在和src平级的地方新建一个文件夹叫做“test”,并在里面创建测试代码,test文件里的目录结构和src的建议保持一致。
右键被测试类->new->Other
之后在文本框中输入junit,然后选中JUnit Test Case
弹出一个创建框,点击browse按钮选择路径为test包后,点击next
随后勾选该类,选择所有方法
测试类就创建完成了
断言
JUnit测试中使用“断言”来进行测试。断言是编写测试用例的核心实现方式,即对比期望值和测试的结果是否相同,以此来判断测试是否通过。断言的核心方法有以下几种
方法名 | 作用 |
---|---|
assertArrayEquals(expecteds, actuals) | 查看两个数组是否相等 |
assertEquals(expected, actual) | 查看两个对象是否相等。类似于字符串比较使用的equals()方法 |
assertNotEquals(first, second) | 查看两个对象是否不相等 |
assertNull(object) | 查看对象是否为空。 |
assertNotNull(object) | 查看对象是否不为空。 |
assertSame(expected, actual) | 查看两个对象的引用是否相等。类似于使用“==”比较两个对象 |
assertNotSame(unexpected, actual) | 查看运行结果是否为true。 |
assertFalse(condition) | 查看运行结果是否为false。 |
assertThat(actual, matcher) | 查看实际值是否满足指定的条件 |
fail() | 让测试失败 |
注解
注解与注释不同,注释是由“//”等开头,而注解是“@”开头。两者的区别是注解不会对程序本身有任何影响,其作用仅仅是方便我们更好的理解代码。而注解是对程序本身又影响的,它相当于是一个可执行的代码。
JUnit常用注解及其作用有:
注解名 | 作用 |
---|---|
@Before | 初始化方法 |
@After | 释放资源 |
@Test | 测试方法,在这里可以测试期望异常和超时时间 |
@Ignore | 忽略的测试方法 |
@BeforeClass | 针对所有测试,只执行一次,且必须为static void |
@AfterClass | 针对所有测试,只执行一次,且必须为static void |
@RunWith | 指定测试类使用某个运行器 |
@Parameters | 指定测试类的测试数据集合 |
@Rule | 允许灵活添加或重新定义测试类中的每个测试方法的行为 |
@FixMethodOrder | 指定测试方法的执行顺序 |
测试代码的编写
初级使用我们采用assertEquals(excepted, actual)来进行举例。
public class NumberTest {
@Test
public void testAdd() {
assertEquals(5, new Number().add(2, 3)); //判断2+3是不是等于5
}
@Test
public void testSubtraction() {
assertEquals(4, new Number().subtraction(8, 4)); //判断8-4是不是等于4
}
@Test
public void testDivision() {
assertEquals(2, new Number().division(10, 5)); //判断10/5是不是等于2
}
@Test
public void testMultiplication() {
assertEquals(12, new Number().multiplication(3, 4)); //判断3*4是不是等于12
}
}
如果我们想要测试单独的某个方法,需要右键该方法->Run as->JUnit Test,
如果是想要测试一个类中的全部方法,则右键类名->Run as ->JUnit Test
之后会出现一个测试窗体,如果全部是绿条,则表明所测试的对象是无误的。测试成功。
错误案例
当绿条变成红色的时候,代表测试对象有误,其中包括两种错误,一个是Failure,另一个是Error。Failure的出现代表程序运行的实际值和预期值不符,而Error的出现代表代码本身有误或者有隐藏的bug。
这里我们编写了两个错误代码,一个是测试2+3等不等于4,另一个是测试6/0等不等于3。右边的测试框中显示了测试结果,一个Failure,一个是Error。
Failure方法的错误描述是java.lang.AssertionError: expected:<4> but was:<5>,表示预期是4,但实际值是5.
Error的错误描述是java.lang.ArithmeticException: / by zero。意思是除数不能是0。
@Test
public void testAdd() {
assertEquals(4, new Number().add(2, 3)); //判断2+3是不是等于4
}
@Test
public void testDivision() {
assertEquals(3, new Number().division(6, 0)); //判断6/0是不是等于3
}
JUnit运行流程
再次新建一个测试类,这次取名为NumberTest2,并勾选框中的四个选项
之后会自动生成代码,需要我们手动补充,补充后的代码如下:
public class NumberTest2 {
@BeforeClass
public static void setUpBeforeClass() throws Exception {
System.out.println("beforeClass()");
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
System.out.println("afterClass()");
}
@Before
public void setUp() throws Exception {
System.out.println("before()");
}
@After
public void tearDown() throws Exception {
System.out.println("after()");
}
@Test
public void test() {
System.out.println("test()");
}
@Test
public void test2() {
System.out.println("test2()");
}
}
执行程序后查看程序执行顺序
从以上结果我们得出结论
1. 被@BeforeClass注解修饰的方法第一个执行
2. 被@Before注解修饰的方法会在每个@Test方法执行前执行一次
3. 被@After注解修饰的方法会在每个@Test方法执行后执行一次
4. 被@AfterClass注解修饰的方法最后一个执行
利用以上特性我们可以有效的实现初始化以及结尾清除功能
进阶使用
@Test的两个参数
@Test有两个可控的参数,分别为timeout和expected
新建一个测试类取名为NumberTest3,并在里面添加如下代码
public class NumberTest3 {
//第一个test()用来阐述timeout参数
@Test(timeout=1)
public void test() {
while(true) {
System.out.println("running.....");
}
}
//test2()用来阐述excepted参数
@Test(expected=ArithmeticException.class)
public void test2() {
assertEquals(3, new Number().division(6, 0));
}
}
test()方法中写入的是一个死循环,按照常理来说应该是死循环直到内存溢出,现在我们对test()进行测试,控制台的结果是
可以看到,程序自动停止了。这就是timeout参数的作用,规定程序在n毫秒之内结束,如果测试代码没有在n毫秒内结束,在第n毫秒时会强制停止。test()上的timeout规定在了1毫秒,所以死循环也就只执行了1毫秒。
test2()函数中写入的是一个断言,判断6/0是否等于3,按照之前的测试来看,测试应该是报Error,因为除数不能为0,但我们加上expected之后,程序测试结果如下
绿条表示测试成功。
expected参数的含义是指期望抛出什么异常(也就是说加上这个参数后,测试的目的就变成了看它是否会抛出指定异常,如果抛出了就是绿条,没抛出指定异常就变红条)
测试套件
测试套件出现的目的是为了方便我们一次运行多个类的测试。加入我们有Test1,Test2,Test3……n个测试类,我们想要全部测试他们的功能,总不可能一个类一个类的去右键点击Run as junit Test吧,因此测试套件的作用就是我们可以只执行一次run as,就可以测试所有的我们想测试的类的测试方法。可以类比工具箱,我们在搬运的时候,肯定不是一件工具一件工具的来回拿放,而是把工具放在工具箱里,一齐进行搬运。
先在test里创建三个测试类,也就是“工具”,里面分别有一个test方法,该方法的内容是打印函数名称
最后创建一个“工具箱”
注意,这个“工具箱”类里有如下几个特点
1. 这整个类是个空类,什么都不要写,它存在的意义就仅仅只是为了我们一次性进行多类测试
2. 两个注解放在类名上面,且第一个注解里的内容固定为“Suite.class”
3. 第二个注解里的内容是说明具体有哪些“工具”需要被一次性测试。里面放了几个类,就会测试几个。
参数化设置
参数化设置的目的是方便我们进行多组数据测试。
在test里新建一个测试类,取名MultipTest,其代码如下
//该类必须加上注释 @RunWith(Parameterized.class),表示这个类是参数化的测试类
@RunWith(Parameterized.class)
public class MultipTest {
//参数的类型和数量取决于test()方法,这里因为测试的是add()方法
//因此有一个预期值,两个加数,总共三个参数。
int expected = 0;
int input1 = 0;
int input2 = 0;
public MultipTest(int expected,int input1, int input2) {
this.expected = expected;
this.input1 = input1;
this.input2 = input2;
}
//JUnit规定了测试数据组必须是一个静态的collection,且里面得是一个数组。
@Parameters
public static Collection<Object[]> t(){
return Arrays.asList(new Object[][] {
{3,1,2},
{4,2,2}
});
}
@Test
public void testAdd() {
assertEquals(expected, new Number().add(input1, input2));
}
}
运行结果如图
发现有两条数据,这两条数据分别对应我们往collection里放的数据。