介绍
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
举个“栗子”:工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。
对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
使用效果
我们编写代码时,一定会反复调试保证它能够编译通过。如果是编译没有通过的代码,没有任何人会愿意交付给自己的老板。但代码通过编译,只是说明了它的语法正确;我们却无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。
幸运的是,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码,而没有任何的后顾之忧。
什么时候测试?单元测试越早越好,早到什么程度?极限编程(Extreme Programming,或简称XP)讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适。从经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的直接返回一个合适值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
误解
它浪费了太多的时间???
观点一:一旦编码完成,开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到实际的系统开始启动工作了。这在外表上看来是一项明显的进步,而象单元测试这样的活动也许会被看作是通往这个阶段点的道路上的障碍, 推迟了对整个系统进行联调这种真正有意思的工作启动的时间。
观点二:我是个很棒的程序员, 我是不是可以不进行单元测试?在每个开发组织中都至少有一个这样的开发人员,他非常擅长于编程,他们开发的软件总是在第一时间就可以正常运行,因此不需要进行测试。你是否经常听到这样的借口?
事实其实是:在真实世界里,每个人都会犯错误。即使某个开发人员可以抱着这种态度在很少的一些简单的程序中应付过去。但真正的软件系统是非常复杂的。真正的软件系统不可以寄希望于没有进行广泛的测试和Bug修改过程就可以正常工作。
在实践工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的。一旦完成了这些单元测试工作,很多Bug将被纠正,在确信他们手头拥有稳定可靠的部件的情况下,开发人员能够进行更高效的系统集成工作。这才是真实意义上的进步,所以说完整计划下的单元测试是对时间的更高效的利用。而调试人员的不受控和散漫的工作方式只会花费更多的时间而取得很少的好处。
成本效率
一个特定的开发组织或软件应用系统的测试水平取决于对那些未发现的Bug的潜在后果的重视程度。这种后果的严重程度可以从一个Bug引起的小小的不便到发生多次的死机的情况。这种后果可能常常会被软件的开发人员所忽视(但是用户可不会这样),这种情况会长期的损害这些向用户提交带有Bug的软件的开发组织的信誉,并且会导致对未来的市场产生负面的影响。相反地,一个可靠的软件系统的良好的声誉将有助于一个开发组织获取未来的市场。
经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的Bug,并且修改它们的成本也很低。在软件开发的后期阶段,Bug的发现并修改将会变得更加困难,并要消耗大量的时间和开发费用。无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。在提供了经过测试的单元的情况下,系统集成过程将会大大地简化。开发人员可以将精力集中在单元之间的交互作用和全局的功能实现上,而不是陷入充满很多Bug的单元之中不能自拔。
那如何进行单元测试呢(这里以Java为例)
1. 概念
java单元测试是最小的功能单元测试代码, 单元测试就是针对单个java方法的测试
java程序的最小功能单元是方法
2. 单元测试的优点
-
main方法进行测试的缺点:
- 只能有一个main()方法, 不能把测试代码分离出来
- 无法打印出测试结果和期望结果.例如: expected: 3628800, but actual: 123456
-
单元测试的优点:
- 确保单个方法正常运行
- 如果修改了方法代码, 只需要保其对应的单元测试通过就可以了
- 测试代码本省就可以作为示例代码
- 可以自动化运行所有测试并获得报告
3. Junit单元测试
JUnit是一个开源的java语言的单元测试框架
专门针对java语言设计, 使用最广泛, JUnit是标准的单元测试架构
3.1 JUnit特点
- 使用断言(Assertion)测试期望结果
- 可以方便的组织和运行测试
- 可以方便的查看测试结果
- 常用的开发工具IDEA, Eclipse都集成了JUnit
- 可以方便的继承到maven中
3.2 maven依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<!-- junit的版本有3.x, 4.x, 5.x 5.x还没有发布, 现在都用是4.x -->
</dependency>
3.3 在IDE中使用快捷键进行单元测试
测试类的使用目录必须是如下, 测试类规定标准是在test目录中进行测试
localhost:javatest lingjing$ tree -d -L 3
.
├── src
│ ├── main
│ │ └── java
│ └── test
│ └── java
IDE的快捷键是:ctrl+shift+t --> create new test
然后选择对应的方法进行测试就好了
3.4 断言
3.4.1 断言的例子
断言的使用, 必须先引入必须的包: IDE自动创建的会自动引入
import static org.junit.Assert.*;
例子: 在main包中的编写的一个正则表达式的类
public class Calculator {
public int calculate(String expression) {
String[] ss = expression.split("\\+");
System.out.println(expression + " => " + Arrays.toString(ss));
int sum = 0;
for (String s: ss) {
sum += Integer.parseInt(s.trim());
}
return sum;
}
}
测试类:
import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
@Test
public void calculate() {
assertEquals(3, new Calculator().calculate("1 + 2"));
assertEquals(3, new Calculator().calculate("1 + 2 + 3"));
}
}
测试类执行结果如下:
1 + 2 => [1 , 2]
1 + 2 + 3 => [1 , 2 , 3]
java.lang.AssertionError:
Expected :3
Actual :6
<Click to see difference>
at javatest.CalculatorTest.calculate(CalculatorTest.java:12)
第一个方法: 1 + 2 => [1 , 2], 最终的结果3是正确的, 所有没有任何报错, 正常显示
第二个方法: 1 + 2 + 3 => [1 , 2 , 3], 最终报错, 并提示在代码的位置: CalculatorTest.java:12, 并且罗列出Expected和Actual的值, 清楚的显示了结果的对比情况, 和代码出现的位置
3.4.2 断言的常用方法
assertEquals(100, x): 断言相等
assertArrayEquals({1, 2, 3}, x): 断言数组相等
assertEquals(3.1416, x, 0.0001): 浮点数组断言相等
assertNull(x): 断言为null
assertTrue(x > 0): 断言为true
assertFalse(x < 0): 断言为false;
assertNotEquals: 断言不相等
assertNotNull: 断言不为null
3.5 使用@Before和@After
- 在@Before方法中初始化测试资源
- 在@After方法中释放测试资源
- @BeforeClass: 初始化非常耗时的资源, 例如创建数据库
- @AfterClass: 清理@BeforeClass创建的资源, 例如创建数据库
3.5.1 对于每一个@Test方法的执行顺序
注意:** 单个@Test方法执行前会创建新的XxxTest实例, 实例变量的状态不会传递给下一个@Test方法, 单个@Test方法执行前后会执行@Before和@After方法
-
执行类的构造函数
-
执行@Before方法
-
执行@Test方法
-
执行@After方法
3.5.2 代码实例:
写一个整体的测试类如下:
如果运行整个类, 运行结果如下:
BeforeClass()
new SequenceTest()
Before()
testA()
After()
new SequenceTest()
Before()
testB()
After()
new SequenceTest()
Before()
testC()
After()
AfterClass()
如果运行单个@Test类
BeforeClass()
new SequenceTest()
Before()
testA()
After()
AfterClass()
3.6 异常测试
异常测试可以通过@Test(expected=Exception.class), 对可能发生的每种类型的异常进行测试
- 如果抛出了指定类型的异常, 测试成功
- 如果没有抛出指定类型的异常, 或者抛出的异常类型不对, 测试失败
例子:
运行如下代码: 正常通过
// 运行如下代码, 正常运行, 确实发生了ArithmeticException异常, 代码通过
@Test(expected = ArithmeticException.class)
public void testException() {
int i = 1 / 0;
}
运行如下代码: 有报错信息
@Test(expected = ArithmeticException.class)
public void testException() {
int i = 1 / 1;
}
执行结果如下:
java.lang.AssertionError: Expected exception: java.lang.ArithmeticException
3.7 参数化测试
@RunWith: 当类被@RunWith注释修饰, 或者类继承了一个被该注解类修饰的类, JUnit将会使用这个注解所指明的运行器(runner)来运行测试, 而不是JUni默认的运行器
要进行参数化测试,需要在类上面指定如下的运行器:
@RunWith (Parameterized.class)
然后,在提供数据的方法上加上一个@Parameters注解,这个方法必须是静态static的,并且返回一个集合Collection。
JUnit4中参数化测试要点:
1. 测试类必须由Parameterized测试运行器修饰
2. 准备数据。数据的准备需要在一个方法中进行,该方法需要满足一定的要求:
1)该方法必须由Parameters注解修饰
2)该方法必须为public static的
3)该方法必须返回Collection类型
4)该方法的名字不做要求
5)该方法没有参数
例子:
@RunWith(Parameterized.class)
public class Testa {
@Parameterized.Parameters
public static Collection<?> data() {
return Arrays.asList(new Object[][] { { "1+2", 3 }, { "1+2+5", 8 }, { "123+456", 579 }, { " 1 + 5 + 10 ", 16 } });
}
Calculator calc;
@Parameterized.Parameter(0)
public String input;
@Parameterized.Parameter(1)
public int expected;
@Before
public void setUp() {
calc = new Calculator();
}
@Test
public void testCalculate() {
int r = calc.calculate(this.input);
assertEquals(this.expected, r);
}
}
执行结果:
1+2 => [1, 2]
1+2+5 => [1, 2, 5]
123+456 => [123, 456]
1 + 5 + 10 => [ 1 , 5 , 10 ]
3.8 超时测试
@Test(timeout=1000)可以设置超时时间
timeout单位是毫秒