基于Java的软件测试(上)

在软件测试中,有一些概念我们需要理解:

* Unit Testing - 单元测试
* Smoke Testing - 冒烟测试
* Stress Testing - 压力测试
* Regression Testing - 回归测试

下面一一进行讲解:

冒烟测试

冒烟测试这个名词最早出现在水管工程中,指的是在工程完成之后,用无毒无害的气体灌入管道,然后查看冒烟情况,看看是否有泄漏的地方:

基于Java的软件测试(上)_第1张图片

图片来源 - http://www.york.ca/NR/rdonlyres/xorewdmahb6qj35zoaqvbtlknaadc4xm2p5zph2gwk5ntz2aledpygswshsuh6rjm3n5mm3btpyaanu7s2mjrio7nc/SmokeTestingSewer.jpg

后来,这个词被引入到软件工程领域,形成了软件测试中最基础的一种测试形式。它指的是:当软件开发完成,发布了某个版本之后,针对这一版本,进行一下基础的可用性测试。比如手工的把它启动起来,各种功能用一用,各个按钮点一点,输入一些样本数据,看看功能是否都好使。

可以说,冒烟测试是一种比较原始的测试方法,但是它是各种其它软件测试方法的发展基础。

单元测试

单元测试将系统划分成模块,定义明确的输入输出,然后去测试输入输出是否合格。

因为单元测试要求每一个测试必须有明确的输入输出,对待测试的功能或者函数进行明确的描述和定义,因此可以形成测试代码,进行复用,并且可以将测试本身做为一个系统工程进行开发。可以说,单元测试是软件测试工程的基石,也是各种测试工具的理论基础。

测试驱动开发(TDD)

在单元测试的基础上,形成了Test Driven Development的软件开发方式,简称TDD,即“测试驱动开发”。

在传统情况下,一般是先开发软件,再进行测试。但是在TDD的理念下面,测试要先行[1]。这就需要软件的设计与接口比较明确,然后测试代码和系统的实现代码全部围绕着设计和接口规范展开。下面是TDD的流程图:


基于Java的软件测试(上)_第2张图片

图片来源 - http://upload.wikimedia.org/wikipedia/en/9/9c/Test-driven_development.PNG

先写测试代码,然后写项目实际代码,使得测试可以通过。这样的开发方式被证明非常有效,可以大大提高软件的开发效率,并且使得项目代码的质量持续可控。

回归测试

回归测试英文叫做Regression Testing。Regression的意思是“退化”,顾名思义,Regression Testing的目的就是要发现那些原来已经测试通过的功能中的新BUG。比如我们的项目有一个注册功能,在老版本的测试中,全部测试通过了,但是在最近新发布的版本当中,这个功能由于最近变更的代码出现了新的问题。回归测试的目的在于发现这类问题。

回归测试的方法有手工和自动两种。比如刚才讲到的冒烟测试,就可以做为回归测试的一种手段:每次新产品发布后,让很多人去试用产品中的功能,然后看看能不能发现一些新引入的BUG。

这样的方式比较低效,一般情况下,在真实的产品当中,我们一般还是会使用工具来进行回归测试,进行自动化的测试,这就需要我们将功能拆分为明确的功能单元和模块,变成单元测试,然后用一些工具去自动执行这些单元测试,从而达到回归测试的目的。

压力测试

压力测试把目光的焦点放在系统的性能上面,目标是测试系统是否能够在性能方面达到设计要求。比如一个Web项目要保证同时满足200人在线使用,这就是压力测试要进行量化测试的目标。

我们可以使用人工的方法去进行压力测试,比如找二百人同时用一个Web项目,看看项目是不是还可以正常使用。但是这样的方法既粗糙也无法量化,因此现代化的测试手段要规范得多。

进行压力测试,首先要对压力指标进行量化,比如:项目A当200人在线时,服务器CPU使用率应为x%或以下,占用内存不超过y MB,每用户系统响应不超过z ms。

此外对操作的行为本身也要进行明确的要求,比如:用户在线的定义为在10秒钟内使用A功能,B功能及C功能。。。

将待测试的内容量化后,便可以使用自动化的工具进行测试。

---

以上是软件测试领域中的一些基本概念,接下来我们要针对Java项目,讲讲各种测试工具的使用。首先我们来看看比较流行的单元测试工具JUnit:

JUnit

JUnit是最为广泛使用的单元测试工具,我们可以创建一个maven[2]项目,默认情况下自动使用JUnit做为测试工具:

mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=javaProj -DartifactId=javaProj


如果一切顺利可以看到项目成功生成:

基于Java的软件测试(上)_第3张图片

然后,我们可以看一下这个项目的结构:

基于Java的软件测试(上)_第4张图片

可以看到里面有个默认的JUnit单元测试AppTest.java,打开这个代码看看内容:

package javaProj;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/[b][/b]
 * Unit test for simple App.
 */
public class AppTest 
    extends TestCase
{
    /[b][/b]
     * Create the test case
     *
     * @param testName name of the test case
     */
    public AppTest( String testName )
    {
        super( testName );
    }

    /[b][/b]
     * @return the suite of tests being tested
     */
    public static Test suite()
    {
        return new TestSuite( AppTest.class );
    }

    /[b][/b]
     * Rigourous Test :-)
     */
    public void testApp()
    {
        assertTrue( true );
    }
}


在JUnit当中,有Test Suite和Test Case的概念。其中Test Suite可以用来包含多个Test Case,方便管理,而真正执行测试本身的则是TestCase。

在上面的代码中,只有一个单元测试testApp(),并且测试逻辑非常简单:

assertTrue(true);


assertTrue是JUnit提供给我们的测试方法,用于检查括号内的逻辑是否为真,比如:

assertTrue(1 + 1 == 2);


如果测试失败了,则会抛出异常:

基于Java的软件测试(上)_第5张图片

注意,这里我们用的JUnit版本为3,在JUnit3当中,约定单元测试的名称必须以test开头,JUnit3才会去执行它。下面我们写个新的单元测试:

package javaProj;

import junit.framework.TestCase;

public class Test1 extends TestCase {

	private int i = 3;

	public void testAdd() {
		assertEquals(1 + 2, i);
		i++;
	}

	public void testAdd2() {
		assertEquals(3, i);
	}

}


这个测试包含两个unit,分别是:

* testAdd()
* testAdd2()

两个测试都是测试i的值是否为3,但是在testAdd方法中,将i的值加了1,那么testAdd2是否会成功呢?我们可以运行测试看下结果:

基于Java的软件测试(上)_第6张图片

发现两个测试都成功了。也就是说,Test1这个测试类,在执行每一个测试功能的时候,会重新实例化,所有的非static的值会被初始化。这是JUnit的设计,其目的是为了保证每一个测试的无关性。

接下来我们看看JUnit4:

JUnit 4

我们继续使用javaProj这个项目,但是要把pom.xml中的JUnit版本更换为4:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.10</version>
  <scope>test</scope>
</dependency>


然后我们撰写一个新的测试:

package javaProj;

import static org.junit.Assert.*;
import org.junit.Test;

public class Test1 {

	@Test
	public void oneAddOneEqualsTwo() {
		assertEquals(2, 1 + 1);
	}

}


从上面的代码中可以看出来,JUnit4为我们带来一些新的功能:

* 测试方法不必再以test开头。
* 将单元测试用@Test标记。
* Test1不需要扩展TestCase了。

接下来我们用JUnit4来实践TDD:

测试驱动开发TDD(Test Driven Development)

在TDD的模式中,测试与开发要遵守设计和接口规范进行。因此我们首先定义功能接口:

package javaProj;

public interface Calculator {
	
	public int add(int a, int b);

}


要实现的功能是一个计算器,包含一个add方法,用于将两个int数相加并返回结果。接下来我们撰写测试:

package javaProj;

import static org.junit.Assert.assertEquals;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class CalculatorTest {

	private int max;
	private int a;
	private int b;
	private Calculator calc;

	@Before
	public void prepareSomeNumbers() {
		this.max = Integer.MAX_VALUE;
		this.a = 1;
		this.b = 1;
		this.calc = new CalculatorImpl();
	}

	@After
	public void testFinished() {
		System.out.println("Done.");
	}

	@Test
	public void testAdd() {
		assertEquals(2, calc.add(a, b));
	}

	@Test
	public void testOverflow() {
		assertEquals((long) max + max, (long) calc.add(max, max));		
	}
}


在这个测试中有两个新的元素:@Before与@After -- 有的时候,我们在测试之前需要初始化一些数据,测试完成后要清除环境或是执行一些代码,在这种情况下,可以使用@Before与@After。

此外,我们写了两个单元测试:

* testAdd() -- 测试功能是否正常。
* testOverflow() -- 测试Calculator的add方法,在相加两个Java的Integer最大值时,是否会溢出。

此外,这个测试中用到了项目中还未实现的class:

this.calc = new CalculatorImpl();


因此,为了让这个测试通过,我们要实现Calculator接口的功能:

package javaProj;

public class CalculatorImpl implements Calculator {

	@Override
	public int add(int a, int b) {
		return a + b;
	}

}


有了实现方法,我们便可以执行测试,结果如下:

基于Java的软件测试(上)_第7张图片

注意(1)中的代码,我们将int数转成long进行比较,但是add方法在两个最大的int数相加的时候就已经溢出了,因此测试失败。(2)中看到结果是java抛出了异常AssertionError。在(3)中我们看到两个"Done",这也印证了JUnit的设计思想:每个测试在执行时,重新实例化测试类,保证测试环境还原到实始状态[3]。

因此,这个异常应该是我们所期待的,因此我们需要将测试逻辑修正一下,让抛出异常成为正确的测试结果:

@Test(expected = AssertionError.class)
public void testOverflow() {
	assertEquals((long) max + max, (long) calc.add(max, max));		
}


注意到我们添加了expected方法,表示AssertionError正是期待的测试结果,此时重新执行测试:

基于Java的软件测试(上)_第8张图片

可以看到测试结果是全部通过了。

小结

在本文的上篇中,我们了解了测试的基本概念,学习了JUnit3和4的基本使用方法,在下篇中,我将介绍另一个测试工具TestNG,并讲一些实际的项目测试中的技巧及方法。

注释

fn1. 严格的来讲TDD的模式中要求测试先行,即先写测试代码,然后撰写项目实际代码,让这个测试代码可以通过测试。但是在实际操作中,我们可以灵活应用,不必死板。

fn2. 有关Maven,请参考蓝点上面的 掌握Maven - http://bluedash.net/t/qqxp7 这篇文章。

fn3. 如果有需要,对于static数据,要在@After标记的方法中手工将其还原成初始值。

参考资料

Unit Testing - http://en.wikipedia.org/wiki/Unit_testing

Smoke Testing - http://en.wikipedia.org/wiki/Smoke_testing

Stress Testing - http://en.wikipedia.org/wiki/Stress_testing

Regression Testing - http://en.wikipedia.org/wiki/Regression_testing

Software performance testing - http://en.wikipedia.org/wiki/Software_performance_testing

Test-driven_development - http://en.wikipedia.org/wiki/Test-driven_development

JUnit 4 Tutorial 2 – Expected Exception Test - http://www.mkyong.com/unittest/junit-4-tutorial-2-expected-exception-test/

你可能感兴趣的:(java,test,JUnit,测试,TestNG)