Skip to content


单元测试HelloWorld

本文通过一个简单的例子说明单元测试的基本方法和jmock的基本技巧。JMOCK的使用请参考JMock使用FAQ,和JMock cookbook.

根据定义单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。所以我们首先来看看需要测试的类和其接口。

step 1 了解被测试代码

被测试类为HelloUnitTestServiceImpl ,它实现了IHelloUnitTestService接口。接口代码如下:


public interface IHelloUnitTestService {
public Object sayHelloToUnitTest(Object argument1,Object argument2);
}

实现类代码如下:


public class HelloUnitTestServiceImpl implements IHelloUnitTestService {
IFirstDependency  firstDependencyObj;
ISecondDependency secondDepedencyObj;

/**
* 需要被测试的方法。该方法有连个参数。实现中依赖到两个对象firstDependencyObj和secondDepedencyObj
*
*/
public Object sayHelloToUnitTest(Object argument1, Object argument2) {
if (argument1 == null) {
throw new IllegalArgumentException("第一个参数不能为空,否则报错");
}

//通过第一个依赖的对象对第一个参数进行运算
Object theFirstResult = firstDependencyObj.doSomeThingWith(argument1);
//通过第二个依赖的对象对第二个参数进行运算
Object theSecondResult = secondDepedencyObj.doSomeOtherThingWith(argument2);
//通过自己的私有方法对两个运算结果进行处理
Object sayHelloResult = doSomeThingPrivately(theFirstResult, theSecondResult);

//返回结果
if (sayHelloResult instanceof String)
return sayHelloResult;
else
return "结果不是String,所以我来代替!";
}

/**
* 该是私有方法对两个参数进行简单的toString相加的运算
* @param theFirstResult
* @param theSecondResult
* @return
*/
private Object doSomeThingPrivately(Object theFirstResult, Object theSecondResult) {
Object result = "Hello, " + theFirstResult.toString() +" "+ theSecondResult.toString()+"!";
return result;
}   //省略setter方法

值得说明的是,实现类需要通过两个依赖的类来处理sayHelloToUnitTest()方法的逻辑。即:


IFirstDependency  firstDependencyObj;
ISecondDependency secondDepedencyObj;

对于基于spring框架的应用来说,我们知道这两个依赖项,将会被spring容器通过setter方法注入到HelloUnitTestServiceImpl类。

step 2 创建JUnit测试用例

首先我们通过eclipse中的Junit的向导,新建一个JunitTestCase。

Wizard

在弹出的向导中设置好配置项。

请选择”New JUnit 3 test”

Wizard2

在Source folder中请通过”Browse”按钮选择源代码的文件夹。如:java.test, src/java/test等。

Wizard3

单击下一步后,选中需要测试的方法或者”单元”。

Wizard4

单击”Finish”按钮后,eclipse就将为我们生成单元测试用例类了。

step 3 编写单元测试用例代码

首先需要声明被测试类,以及JMock的context对象Mockery。

然后在setUp方法中对helloUnitTestService和context进行初始化。

注意,setUp方法继承与Junit框架中的TestCase基类。setUp方法是用来在多个测试方法运行过程中进行初始化,和数据准备之类的一个生命周期方法。

setUp-test-tearDown

如果不是很清楚,可以学习JUnit的Tutorial,网上有很多这样的教程。

所以setUp方法可以写成这样:


@Override
protected void setUp() throws Exception {
super.setUp();
context = new Mockery();
helloUnitTestService = new HelloUnitTestServiceImpl();
}

对于本例来说被测试的方法为sayHelloToUnitTest,它的正常运行决定与两个方面:

  1. 两个依赖项必须被正确的注入,否则将抛出空指针异常。在单元测试过程中为了测试的速度和性能,以及测试的粒度等诸多原因不可能使用spring容器的方式注入(有疑问可以和我沟通)。
  2. 两个传入参数必须满足测试用例的需要,即两个入参必须正确的配置并能模拟真实情况下代码被执行的路劲。

基于以上两点,我们的对策是。

  1. 对于两个依赖项,我们采用JMock的框架来模拟mock对象。即按照mock的思想将需要的依赖项进行mock,得到mock对象,然后通过setter方法注入到被测试对象中,就可以进行JUnit的断言了。JMock的使用请通过JMock的相关资料进行学习,如JMock的 tutorial和JMock使用FAQ等。
  2. 对与传入的两个参数,我们进行手动的模拟。如:new出对象,并进行set设值。

完成上面两步后,我们还需要对mock的依赖项的行为进行模拟(或者设置期望)。我们知道mock出来的对象虽然可被成功的以setter的方式注入,但是mock对象的行为还是未被正确设置,此时若运行测试,只会意外中断。我们可以这样来看,当sayHelloToUnitTest运行时,当运行到firstDependencyObj.doSomeThingWith(argument1)的时候,由于firstDependencyObj对象(即mock对象)并不知道doSomeThingWith该如何处理,或者如何返回期望的返回值。所以测试将被迫在此中断,并会抛出JMock的异常来。

所以sayHelloToUnitTest方法做为被测试方法,在执行过程中对它需要调用的依赖对象的方法(firstDependencyObj.doSomeThingWith(argument1))需要通过JMock的设置期望的方式模拟行为。设置的方法为:


//设置期望
context.checking(new Expectations() {
{
allowing(firstDependencyObj).doSomeThingWith(argument1);
will(returnValue("I Love Beijing"));
allowing(secondDepedencyObj).doSomeOtherThingWith(argument2);
will(returnValue("Tiananmen Square"));
}
});

这里我们设置firstDependencyObj的doSomeThingWith放在遇到argument1( 假定argument1 = “我爱北京”;)参数时,将返回”I Love Beijing”。同样的方法我们设置secondDepedencyObj的doSomeOtherThingWith在argument2时的返回值。

最后测试用例的代码如下:


public class HelloUnitTestServiceImplTest extends TestCase {

public Mockery                  context;
public HelloUnitTestServiceImpl helloUnitTestService;

@Override
protected void setUp() throws Exception {
super.setUp();
context = new Mockery();
helloUnitTestService = new HelloUnitTestServiceImpl();
}

public void testSayHelloToUnitTestTest() {
//准备参数
final Object argument1 = "我爱北京";
final Object argument2 = "天安门";
//mock依赖
final IFirstDependency firstDependencyObj = context.mock(IFirstDependency.class);
final ISecondDependency secondDepedencyObj = context.mock(ISecondDependency.class);

//设置期望
context.checking(new Expectations() {
{
allowing(firstDependencyObj).doSomeThingWith(argument1);
will(returnValue("I Love Beijing"));
allowing(secondDepedencyObj).doSomeOtherThingWith(argument2);
will(returnValue("Tiananmen Square"));
}
});

//依赖注入mock对象
helloUnitTestService.setFirstDependencyObj(firstDependencyObj);
helloUnitTestService.setSecondDepedencyObj(secondDepedencyObj);

//调用被测试的方法
Object actualSayHelloResult = helloUnitTestService.sayHelloToUnitTest(argument1, argument2);

//验证返回值是否为预期
Object expectedSayHelloResult = "Hello, I Love Beijing Tiananmen Square!";
assertEquals(expectedSayHelloResult, actualSayHelloResult);

//验证mock对象的行为是否为期望
context.assertIsSatisfied();
}
step 4 执行用例

通过eclipse向导我们就可以方便的运行用例进行测试并得到测试结果。

run-junit

此外还可以通过antx test以及 mvn test等方法运行一组测试,以及获取代码覆盖率的报告。

step 5 其他说明
  • 若该方法有多个分支,可以在这个测试用例类中增加其他测试分支的方法。如通过 testSayHelloToUnitTestTestWhileDependencyThrowsException的方法名,重新编写代码来测试依赖项抛出异常情况下的逻辑分支。PS:抛出异常可以通过JMock来模拟出来。
  • 对于没有依赖关系的方法,可不使用JMock。
  • 正确了解被测试类,被测试方法的逻辑是书写单元测试用例代码的关键。
  • JUnit的使用,如何断言,以及JMock的使用技巧等可以通过网络途径来学习。
小结

本文通过一个简单例子展示了Junit测试用例的创建方法,以及JMock框架的基本使用方法,意在指引读者对单元测试有一个初步认识。单元测试在实际执行过程中还有很多经验和技巧需要学习和掌握的,希望此文能够抛砖引玉,吸引读者不断学习和掌握单元测试相关的技巧。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • LinkedIn
  • description
  • TwitThis
  • MySpace
  • StumbleUpon
  • Google
  • FriendFeed
  • Mixx
  • Reddit

Posted in 全部, 开发技术. Tagged with .

Test Double 分类


• A Dummy Object is a placeholder object that is passed to

the SUT as an argument (or an attribute of an argument) but is never

actually used.

作为对象参数的替身传给SUT,通常没有任何实际作用。

• A Test Stub is an object that replaces a real component on which the

SUT depends so that the test can control the indirect inputs of the SUT.

It allows the test to force the SUT down paths it might not otherwise

exercise.简言之,Test Stub可以控制间接输入,从而保证测试代码SUT的执行路径。

此外:子模式Responder injects valid values, Saboteur injects errors or exceptions

• A Test Spy, which is a more capable version of a Test Stub,

can be used to verify the indirect outputs of the SUT by giving the test a

way to inspect them after exercising the SUT.

Test Spy可以说成是有记录功能的Test Stub

• A Mock Object is an object that replaces a real component on which

the SUT depends so that the test can verify its indirect outputs。

和test Stub 不同的是,Mock object主要用来验证间接输出。

• A Fake Object (page 551) (or just "Fake" for short) is an object that

replaces the functionality of the real DOC with an alternative implementation

of the same functionality.

即对功能的简单实现,替换DOC。通常情况下使用Fake Object是因为测试DOC无法获取、太慢或者有副作用。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • LinkedIn
  • description
  • TwitThis
  • MySpace
  • StumbleUpon
  • Google
  • FriendFeed
  • Mixx
  • Reddit

Posted in 全部, 读书笔记. Tagged with .

Test Utility Method Location

TestUtilityMethodLocation

  
由图可知,测试工具方法的位置主要有三个地方:

1. SuperClass,测试父类,通过继承来访问测试工具方法。

2. TestClass,放到测试类自身中,通过this的形式访问 。

3. TestHelper,放到测试帮助类中,通过delegate的形式访问。

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • LinkedIn
  • description
  • TwitThis
  • MySpace
  • StumbleUpon
  • Google
  • FriendFeed
  • Mixx
  • Reddit

Posted in 全部, 开发技术, 读书笔记. Tagged with .

Test Smell

  1. 行为Smell
    Assertion Roulette
    Erratic Test –Fragile Test
    Frequent Debugging
    Manual Intervention
    Slow Tests
  2. 工程Smell
    Buggy Tests
    Developers Not Writing Tests
    High Test Maintenance Cost
    Production Bugs
  3. 代码Smell
    Obscure Test
    Conditional Test Logic
    Hard-to-Test Code
    Test Code Duplication
    Test Logic in Production

Assertion Roulette:断言复杂,影响测试用例的可读性,不利于维护

Erratic Test(Fragile Test):脆弱测试,测试用例非常容易因为代码变更而失败。具体原因有:

  1. 接口敏感Interface Sensitivity
  2. 行为敏感Behavior Sensitivity
  3. 数据敏感Data Sensitivity
  4. 上下文敏感Context Sensitivity

Frequent Debugging:频繁debug,这种情况指明测试用例很难作为bug的定位器,或者对bug的定位提供有效的帮助。

Manual Intervention: 手工干预,这种情况指测试用例的执行需要手工干预,例如需要手工介入gui的测试,数据准备,资源销毁等。这些与测试自动化的原则违背。

Slow Tests:慢速测试,这种情况指测试用例的执行效率很低,常常消耗大量的时间。慢速的测试往往导致开发人员执行测试用例的积极性下降。同时也给测试自动化带来麻烦。

Buggy Tests:有漏洞的测试,测试用例自身存在bug。这种情况下测试用例常常在执行过程中失败,而检查代码后发现生产代码并不存在问题。例如:测试用例排除了生产代码捕获error错误,而fail。

Developers Not Writing Tests: 开发人员不写测试,这种情况导致的原因往往是开发人员抱怨没有足够的时间编写测试用例,或者测试代码很难编写。

High Test Maintenance Cost: 高测试维护成本,这种情况导致的原因跟很多因素有关,例如:测试代码重复、测试用例很难编写、脆弱测试等。

Production Bugs: 生产环境bug多,这种情况一般指生产环境发现的bug比测试阶段和单元测试阶段的要多。这意味着存在一下情况:存在测试未覆盖的代码或者需求、测试未能有效的执行、未进行有效的单元测试、存在永不失败的测试用例。

Obscure Test: 晦涩的测试,这种情况下测试用例很难理解。一般引起的原因为:测试用例测试的功能和代码过多,使用的通用固件(fixture)太大、测试中存在不相关的信息、硬编码测试数据、间接测试。

Conditional Test Logic: 存在条件判断的测试逻辑,这种情况指测试用例中存在条件判断。导致很难理解测试用例的真正意图。一般引起这个问题的原因是:条件判断的验证逻辑(conditional verification logic)、弹性测试用例、复杂的测试条件。

Hard-to-Test Code: 代码测试困难,这个很容易理解,测试用例无法简单高效的书写。一般情况下这和SUT的的 高耦合有关。有时异步代码的测试以及未考虑可测试性的设计也会引起代码的测试困难。

Test Code Duplication: 测试代码冗余。这个就不说了,多半情况下是Ctrl+C和Ctrl+V的滥用造成的。也存在测试代码未考虑整体重用性的可能。

Test Logic in Production: 生产代码中包含测试逻辑。 导致这个问题的罪魁祸首就是存在测试环境无法测试的情况,为了测试覆盖率,不得不把测试逻辑放入SUT中。这样无形中增加了SUT的复杂度。

ref:xUnit Patterns

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • LinkedIn
  • description
  • TwitThis
  • MySpace
  • StumbleUpon
  • Google
  • FriendFeed
  • Mixx
  • Reddit

Posted in 全部, 开发技术, 读书笔记. Tagged with .

Twitter 因为“明天”被“墙”了

怎样的国度啊!只能说明20年前的事确实太恐怖、太残忍、太不光彩吧,也许那才是当代中国与民主最接近的时代,五四运动已经离我们远去,可是“留斯”不是。我们需要纪念这样的精神。因为时代呼唤民主、自由、博爱!

此地无银三百两

simpsonschina.jpg

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • LinkedIn
  • description
  • TwitThis
  • MySpace
  • StumbleUpon
  • Google
  • FriendFeed
  • Mixx
  • Reddit

Posted in 网络生活.

An interesting ref from OOAD 3rd edition

Sir Isaac Newton secretly admitted to some friends:
He understood how gravity behaved, but not how it worked!

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • LinkedIn
  • description
  • TwitThis
  • MySpace
  • StumbleUpon
  • Google
  • FriendFeed
  • Mixx
  • Reddit

Posted in 杂七杂八.