|
前段时间面试被问及很多关于单元测试的相关知识,然后大佬着重给我强调了关于单元测试的重要性,最近一
直各方面学习关于单元测试的知识以作补充强化。
什么是单元测试
单元测试不是集成测试
JUnit4
JUnit的其他功能
测量代码覆盖率
尽可能将测试数据外部化
TestNG
JUnit
使用断言而不是Print语句
构建具有确定性结果的测试
除了正面情景外还要测试负面情景和边缘情况
什么是单元测试
单元测试是为了测试某一个代码单元而写的测试代码。这里的“代码单元”我通常将它认为一个具体类中的某个
方法;所以,也可以这样理解:
单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码;
常见的Java单元测试框架有:JUnit、TestNG等;
单元测试不是集成测试
单元测试只是测试一个方法单元,它不是测试一整个流程;
举例:一个Login页面,上面有两个输入框和一个button。两个输入框分别用于输入用户名和密码。点击button
以后,有一个UserManager会去执行performlogin操作,然后将结果返回,更新页面。
那么我们给这个东西做单元测试的时候,不是测这一整个login流程。这种整个流程的测试:给两个输入框设置
正确的用户名和密码,点击login button, 最后页面得到更新。叫做集成测试,而不是单元测试。
JUnit4
一个测试方法主要包括三个部分:
setup
执行操作
验证结果
一个测试类的很多测试方法可能需要相同的setup,所以为我们提供了便捷方法。对于JUnit4,是通过 @Before
来实现的:
- public class CalculatorTest {
- Calculator mCalculator;
- @Before
- public void setup() {
- mCalculator = new Calculator();
- }
- @Test
- public void testAdd() throws Exception {
- int sum = mCalculator.add(1, 2);
- assertEquals(3, sum); //为了简洁,往往会static import Assert里面的所有方法。
- }
- @Test
- public void testMultiply() throws Exception {
- int product = mCalculator.multiply(2, 4);
- assertEquals(8, product);
- }
- }
复制代码
如果一个方法被 @Before 修饰过了,那么在每个测试方法调用之前,这个方法都会得到调用。
对应于 @Before 的,有一个 @After ,作用估计你也猜得到,那就是每个测试方法运行结束之后,会得到运行
的方法。比如一个测试文件操作的类,那么在它的测试类中,可能 @Before 里面需要去打开一个文件,而每
个测试方法运行结束之后,都需要去close这个文件。这个时候就可以把文件close的操作放在 @After 里面,让
它自动去执行。
@BeforeClass 和 @AfterClass 。 @BeforeClass 的作用是,在跑一个测试类的所有测试方法之前,会执行一次被
@BeforeClass 修饰的方法,执行完所有测试方法之后,会执行一遍被 @AfterClass 修饰的方法。这两个方法可
以用来setup和release一些公共的资源,需要注意的是,被这两个annotation修饰的方法必须是静态的。
对于第三部“验证结果”,则一般是通过一些assert方法来完成的。JUnit为我们提供的assert方法,多数都在 Asse
rt 这个类里面。最常用的那些如下:
assertEquals(expected, actual)
验证expected的值跟actual是一样的,如果是一样的话,测试通过,不然的话,测试失败。如果传入的是object,
那么这里的对比用的是equals();
assertEquals(expected, actual, tolerance)
这里传入的expected和actual是float或double类型的,大家知道计算机表示浮点型数据都有一定的偏差,所以哪
怕理论上他们是相等的,但是用计算机表示出来则可能不是,所以这里运行传入一个偏差值。如果两个数的差
异在这个偏差值之内,则测试通过,否者测试失败。
assertTrue(boolean condition)
验证contidion的值是true;
assertFalse(boolean condition)
验证contidion的值是false;
assertNull(Object obj)
验证obj的值是null;
assertNotNull(Object obj)
验证obj的值不是null;
assertSame(expected, actual)
验证expected和actual是同一个对象,即指向同一个对象
assertNotSame(expected, actual)
验证expected和actual不是同一个对象,即指向不同的对象;
fail()
让测试方法失败;
注意:上面的每一个方法,都有一个重载的方法,可以在前面加一个String类型的参数,表示如果验证失败的
话,将用这个字符串作为失败的结果报告。
比如:
assertEquals(“Current user Id should be 1”, 1, currentUser.id());
当 currentUser.id() 的值不是1的时候,在结果报道里面将显示”Current user Id should be 1”,这样可以让测试
结果更具有可读性,更清楚错误的原因是什么。
比较有意思的是最后一个方法, fail() ,你或许会好奇,这个有什么用呢?其实这个在很多情况下还是有用的,
比如最明显的一个作用就是,你可以验证你的测试代码真的是跑了的。
此外,它还有另外一个重要作用,那就是验证某个被测试的方法会正确的抛出异常。
JUnit的其他功能
Ignore一些测试方法;
很多时候,因为某些原因(比如正式代码还没有实现等),我们可能想让JUnit忽略某些方法,让它在跑所有测
试方法的时候不要跑这个测试方法。要达到这个目的也很简单,只需要在要被忽略的测试方法前面加上 @Ignor
e 就可以了,如下:
- public class CalculatorTest {
- Calculator mCalculator;
- @Before
- public void setup() {
- mCalculator = new Calculator();
- }
- // Omit testAdd() and testMultiply() for brevity
- @Test
- @Ignore("not implemented yet")
- public void testFactorial() {
- }
- }
复制代码
验证方法会抛出某些异常
有的时候,抛出异常是一个方法正确工作的一部分。比如一个除法函数,当除数是0的时候,它应该抛出异常,
告诉外界,传入的被除数是0,示例代码如下:
- public class Calculator {
- // Omit testAdd() and testMultiply() for brevity
- public double divide(double divident, double dividor) {
- if (dividor == 0) throw new IllegalArgumentException("Dividor cannot be 0");
- return divident / dividor;
- }}
复制代码
那么如何测试当传入的除数是0的时候,这个方法应该抛出 IllegalArgumentException 异常呢?
在Junit中,可以通过给 @Test annotation传入一个expected参数来达到这个目的,如下:
- public class CalculatorTest {
- Calculator mCalculator;
- @Before
- public void setup() {
- mCalculator = new Calculator();
- }
- // Omit testAdd() and testMultiply() for brevity
- @Test(expected = IllegalArgumentException.class)
- public void test() {
- mCalculator.divide(4, 0);
- }
- }
复制代码
@Test(expected = IllegalArgumentException.class) 表示验证这个测试方法将抛出 IllegalArgumentException
异常,如果没有抛出的话,则测试失败。
测量代码覆盖率
代码覆盖率衡量(以百分比表示)了在运行单元测试时执行的代码量。通常,高覆盖率的代码包含未检测到
的错误的几率要低,因为其更多的源代码在测试过程中被执行。测量代码覆盖率的一些最佳做法包括:
使用代码覆盖工具,如Clover,Corbetura,JaCoCo或Sonar。使用工具可以提高测试质量,因为这些工具可
以指出未经测试的代码区域,让你能够开发开发额外的测试来覆盖这些领域。
每当写入新功能时,立即写新的测试覆盖。
确保有测试用例覆盖代码的所有分支,即if / else语句。
下面的 concat 方法接受布尔值作为输入,并且仅当布尔值为true时附加传递两个字符串:
- < class="hljs typescript">public String concat(boolean append, String a,String b) {
- String result = null;
- If (append) {
- result = a + b;
- }
- return result.toLowerCase();
- }
复制代码
以下是上述方法的测试用例:
- <class="hljs less">@Test
- public void testStringUtil() {
- String result = stringUtil.concat(true, "Hello ", "World");
- System.out.println("Result is "+result);
- }
复制代码
|
|