JUnit重装上阵

来源:互联网 发布:沈阳网络外包 编辑:程序博客网 时间:2024/05/02 02:22
                                                                        JUnit重装上阵
                                                                         作者:Ralf Stuckert
                                                                                                               12/07/2006
 
我们必须承认,JUnit是Java世界使用最广泛的(单元)测试工具.我们也有其他功能强大的测试框架,例如TestNG(它的功能非常全面),但是它们还没有被广泛的接受,JUnit却被广泛接受.在第4个版本里,Kent Beck 和 Erich Gamma引入了近几年来的第一次API修改.在2005年,当这个修改的第一个版本发布时,你在当前的工作环境很难使用它,因为缺乏工具的支持.而现在,绝大多数的工具和IDEs开始支持JUnit 4,所以现在是尝试使用它的时候了.本文描述了JUnit 3.8x和JUnit 4之间的不同.
 
到底有了哪些更新?
让我开门见山的告诉你:JUnit 4是完全基于annotations的.,你现在明白了这意味着什么?首先,你不必继承TestCase类.其次,你的测试方法名称不必非要以前缀test开始.所有这一切你必须做的是使用@Test注释来标注你的测试方法.看看下面的例子:
import junit.framework.TestCase;
import org.junit.Test;
 
public class CalculatorTest extends TestCase {
 
    @Test
    public void testadd() {
        ....
    }
}
确实,在某些情况下,不需要继承TestCase可能非常有用.但是我习惯继承的assert...方法呢?它被你使用Java5的另外一个特性给解决了,这就是static imports.
import org.junit.Test;
import static org.junit.Assert.*;
 
public class CalculatorTest {
 
    @Test
    public void add() {
        ...
        assertEquals( 4, calculator.add( 1, 3 ) );
    }
}
创建测试环境
所以,可以看出,写一个测试和JUnit 4没有什么不同.等等,但是setUp()tearDown()方法发生了什么变化?我们往下看,现在你可以使用@Before 和 @After来装饰所有的方法.
public class CalculatorTest {
 
    @Before
    public void prepareTestData() { ... }
 
    @After
    public void cleanupTestData() { ... }
}
你可能使用多个@Before 和 @After方法,但是必须注意到,当它们执行的时候,没有任何的东西来说明它们的执行顺序.
public class CalculatorTest {
 
    @Before
    public void prepareTestData() { ... }
 
    @Before
    public void setupMocks() { ... }
 
    @After
    public void cleanupTestData() { ... }
}
值得注意的是,继承的@Before 和 @After方法是对称的执行的.这意味着一个继承CalculatorTest的测试类ScientificCalculatorTest按如下顺序执行:
CalculatorTest#Before
ScientificCalculatorTest#Before
ScientificCalculatorTest#After
CalculatorTest#After
有一个大多数人都忽略的特性是为整个的一组测试定义setup/teardown.当你不需要为每一个测试运行setup而又不得不花费时间的时候,这个特性特别有用,例如建立数据库连接.这可以使用@BeforeClass 和 @AfterClass标注来解决.
public class CalculatorTest {
 
    @BeforeClass
    public static void setupDatabaseConnection() { ... }
 
    @AfterClass
    public static void teardownDatabaseConnection() { ... }
}
它和使用@Before and @After的规格一样.这意味着你可以有多个@BeforeClass 和 @AfterClass方法,同样,也可以从父类中继承得到.注意,这些方法必须是静态的.
测试Exceptions
检测我们的代码产生出正确的结果是一个方面,但是有关错误处理呢?当事情出错的时候,通常抛出一个exception.测试你的代码执行正确的违例情况和实际功能一样重要(或者更加重要).并且这是单元测试(框架)必须提供的一个重大的功能.你可以在一个可控的条件下(可能需要一些mocks的帮助)驱动代码到一个失败的情况里,然后测试它是否可以抛出想要的exception.在JUnit 3.8x里,测试exception的模式为:
public class CalculatorTest {
 
    public void testDivisionByZero() {
        try {
            new Calculator().divide( 4, 0 );
        } catch (ArithmeticException e) {}
    }
}
在JUnit 4里,你可以使用@Test标注来声明一个期望的exception:
public class CalculatorTest {
 
    @Test(expected=ArithmeticException.class)
    public void testDivisionByZero() {
        new Calculator().divide( 4, 0 );
    }
}
测试Timeout
单元测试可以短时间运行,使得你可以不时的运行它.但是有时你可能会有一些测试消耗更长的时间,特别是当网络连接被引入的时候.所以随时你有理由怀疑你的测试能够按时结束,你可以确定测试会在运行一段特别的时间后被取消掉.JUnit 3.8x,你不得不使用一个额外的库文件来达到目的,更坏的是,你可能会利用一个新的线程.现在,这个任务毫不需要费心;你所需要做的就是在@Test标注里给定一个timeout参数.
@Test(timeout=5000)
    public void testLengthyOperation() {
        ...
    }
如果timeout在测试完成之前发生,你将得到适当的失败信息:
java.lang.Exception: test timed out after 5000 milliseconds
忽略测试
可能有一些情况你希望测试运行者忽略一些测试.可能是你正在使用的第三方库的当前版本有一个bug,或者是基础框架使得你的测试不能成功运行.不管是什么原因(或多或少),在JUnit 3.8x中,你不得不将你的测试代码注销掉,或者将它们排除在测试组合之外.在JUnit 4中,你可以用@Ignore标注来装饰你的方法.
public class CalculatorTest {
 
    @Ignore("Not running because ")
    @Test
    public void testTheWhatSoEverSpecialFunctionality() {
    }
}
指向@Ignore标注的文字信息会报告给测试的使用者.即使它是可选择的,你也可以提供一个关于这个测试为什么被忽略的注释,这使得你不会忘记它.TestRunner编译到Eclipse的时候通过描述删除线来标记被忽略的测试.不幸的是,注释还没有被提供.
测试组合
在基于annotation的JUnit 4中,你也可以发现好的suite()方法.在以前版本是这样的:
public class AllTests extends TestCase {
 
    public static Test suite() {
        TestSuite suite = new TestSuite();
        suite.addTestSuite(CalculatorTest.class);
        suite.addTestSuite(AnotherTest.class);
        return suite;
    }
现在是这样的:
@RunWith(value=Suite.class)
@SuiteClasses(value={CalculatorTest.class, AnotherTest.class})
public class AllTests {
...
}
好了,都是些什么啊?嗯,JUnit 4引入了一个hook来指定不同的测试runners.在这种情况下,我们告诉JUnit运行组合runner来执行这个测试.@SuiteClasses标注被组合runner读取,它给定属于这个组合的测试.伙计,我确定旧的组合模式好一些.可能是这样,但是使用一种新的方式来定义组合也有一个优点:你可以给定在执行第一个组合之前和最后一个测试之后的@BeforeClass 和 @AfterClass方法.有了这个,你就能定义一个组合范围的setup.
参数化的测试
除了组合还有另外一个特别的runner:参数化.它使得你可以使用不同的数据组运行相同的测试.让我们来通过一个例子看看:我们将为计算给定数字n的factorial的方法写一个测试:runner,JUnit 4
@RunWith(value=Parameterized.class)
public class FactorialTest {
 
    private long expected;
    private int value;
 
   @Parameters
    public static Collection data() {
        return Arrays.asList( new Object[][] {
                             { 1, 0 },   // expected, value
                             { 1, 1 },
                             { 2, 2 },
                             { 24, 4 },
                             { 5040, 7 },
                             });
    }
 
    public FactorialTest(long expected, int value) {
        this.expected = expected;
        this.value = value;
    }
 
    @Test
    public void factorial() {
        Calculator calculator = new Calculator();
        assertEquals(expected, calculator.factorial(value));
    }
}
参数化的runner所要做的是使用以@Parameters包装的方法提供的数据运行FactorialTest所有的测试(这里我们只有一个).在这种情况下,我们有5个数据项的List.每一个项由FactorialTest的构造器参数的数组组成.我们的factorial()测试将在assertEquals()中使用这些数据.最后,这意味着我们的测试将象如下数据那样运行5次:
factorial#0: assertEquals( 1, calculator.factorial( 0 ) );
factorial#1: assertEquals( 1, calculator.factorial( 1 ) );
factorial#2: assertEquals( 2, calculator.factorial( 2 ) );
factorial#3: assertEquals( 24, calculator.factorial( 4 ) );
factorial#4: assertEquals( 5040, calculator.factorial( 7 ) );
多功能的其他特性
新的JUnit类将要包括一个新的包:org.junit.旧的测试框架由于兼容方面的原因依然包括在包junit.framework里.另外一个精巧的改变是,使用抛出(java.lang.)AssertionError而不是junit.framework.AssertionFailedError.
另一个真正有用的是测试数组相等的新的断言,它首先测试数组的长度是否相等,然后使用equals()比较数组的项.
assertEquals(Object[] expected, Object[] actual)
JUnit 4不再支持基于UI的TestRunner,这个功能留给了IDE的开发者.但是这里仍然有命令行工具可供你手工的执行测试.你只需要调用类org.junit.runner.JUnitCore,传入你的测试类的全名:
java -cp ... org.junit.runner.JUnitCore CalculatorTest AnotherTest
隐蔽的惊奇之事
最后,这里有一些恶作剧.当我在尝试JUnit 4的时候,我最早的一个测试是关于琐碎的Calculator.add()方法.
public class Calculator {
 
    public long add(long number1, long number2) {
        return number1 + number2;
    }
}
 
public class CalculatorTest {
 
    @Test
    public void add() throws Exception {
        Calculator calculator = new Calculator();
        assertEquals(4, calculator.add(1, 3));
    }
}
很简单,是吧?但是当我运行测试的时候,我得到了:
java.lang.AssertionError: expected:<4> but was:<4>
什么?怎么会这样?这是因为自动装箱(autoboxing).在JUnit 4中,再也不需要为简单类型数据的特殊assertEquals();仅仅只有一个assertEquals(Object expected, Object actual).如果你传入两个ints,它们会被自动转化为Integer.现在,我们的问题逐渐清晰起来: Calculator.add()返回一个long类型数据,但是在默认情况下,数值的类型为int.所以通过自动装箱,我们得到如下:
        assertEquals(new Integer(4), new Long(calculator.add(1, 3)));
好了,这就是我们为什么没有得到想要的结果,但是这是一个问题吗?是,让我们看看实现Integer的equals():
    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }
不幸的是,我们不能将一个Integer和Long进行比较,所以equals()永远返回false.当你将你的旧的测试转移到JUnit 4的时候,这可能是一个问题.在JUnit 3.8x中,这个测试可以正确运行,因为有为各种简单类型准备的assertEquals(),所以在我们的例子中,编译器会选择assertEquals(long expected, long actual).要解决这个问题,你必须明确的使用long.
assertEquals( (long)4, calculator.add(1, 3) );
那又怎样?
JUnit 4给我们带来了一些新的富有想象力的特性:它是基于annotation的,测试setup有了显著的提高,为新的runners扩展了hooks,而且甚至为assertEquals()增加了大多数人想要的数组比较.其他的框架如TestNG引入这些特性已经很旧了!你是对的,但是JUnit仍然是使用最广泛的(Java)测试框架,所以引入更多的新的技术成果是很有价值的.JUnit的优势在于每一个主要的开发工具都自动的支持它,不需要安装插件,不需要嵌入到我的基础代码中.并且由于它的开放的基于annotation的架构,它的扩展已经开发出来.仅凭这几点就足够了,赶快试试吧!
知道哪条路和实际走那条路是不同的!
-        Morpheus
资源
  • JUnit.org
  • JUnit 4 extensions
  • TestNG
Ralf Stuckertcompeople AGIT顾问,这是一家位于德国法兰克福的欧洲IT服务公司.
 


原创粉丝点击