JUnit in Action 2nd Edition 第三章 精通Junit(1)

第三章 精通JUnit

到目前为止,我们做了JUnit综述和演示如何使用(第一章)。我们也看到了JUnit内部的核心类和方法,它们如何同其它的交互(第二章)。

我们现在通过介绍实实在在的组件和测试来深度挖掘它,在这一章中,我们使用控制器设计模式实现一个小的应用,我们然后使用JUnit测试这个应用的每一个部分,当编写和组织测试时我们也将看到JUnit的最佳实践。

3.1 介绍控制器组件

Core Java EE Patterns》中描述控制器controller作为一个组件,“同客户端交互,控制

和管理每一个请求的处理”。然后告诉我们它用于表示层和业务层模式。

通常,一个控制器有如下行为:

n 接受请求

n 在请求上执行公共的计算

n 选择一个合适的请求处理程序

n 路由请求,因此处理程序能够执行相关的业务逻辑

n 可以为错误和异常提供一个顶层的处理程序

你会发现在各种各样的应用中控制器普遍地使用。例如,在一个表示层模式,一个web控制器接受HTTP请求和提取HTTP参数,cookiesHTTP头文件,可能使用HTTP元素容易地访问应用程序的其余部分。一个web控制器基于请求元素决定调用合适的业务逻辑组件。也许在HTTP会话持久性数据的帮助下。数据库,或者一些其他资源。Apache Struts框架就是一个web控制器的例子。

控制器另一常见的运用是在业务层模式中处理应用程序。许多业务应用支持多个表示层,HTTP客户端可以处理web应用。Swing客户端可以处理desktop应用,在这些表示层下,常常有一个应用控制器或者状态机。程序员实现许多Enterprise JavaBeanEJB)应用以此方式,EJB层有自己的控制器,通过一个业务外观模式或者委派模式连接不同的表示层。

它给出了控制器的多种用途,就不会惊讶于控制器出现在大量的企业架构模式中,包括Page ControllerFront controller,Application Controller

控制器的设计是你实现这些经典模式的第一步,让我们完成一个简单控制器的代码。看如何工作,然后尝试一些测试。如果你想跟着自己的感觉与运行测试。本章所有的源代码都在SourceForge中可以找到(http://junitbook.sf.net)。附录A中有关于这些源代码设置信息。

3.1.1接口设计

仔细检查控制器的描述,有四个对象较突出:RequestResponseRequestHandlerControllerController接受Request,调度RequestHandler,然后返回Response对象。动手描述,你能编写一些简单的启动接口,就像Listing3.1所示。

Listing 3.1 RequestResponseRequestHandlerController interface

public interface Request{

String getName();

}

public interface Response{}

public interface RequestHandler{

Response process(Request request) throws Excepion;

}

public interface Controller{

Response processRequest(Request request);

void addHandler(Request request,RequestHandler requestHandler);

}

1.首先,定义一个Request接口,包含一个单独的getNmae方法,返回唯一请求名,因此你能区分每一个请求。因此你开发这个组件,你将需要其他的方法,但是你能添加他们当你进行编写时。

2.下一步,指定一个空接口,设计编码,你仅需要返回一个Response对象,Response包含的东西你能稍后处理。现在,你需要一个Response类型能插入一个签名。

3.下一步是定义一个RequestHandler接口,它能处理Request(请求)和返回你的Response(响应)。RequestHandler是一个辅助部件设计,执行大多的脏数据工作。它可能调用类抛出任何类型的异常。Exceptionprocess方法抛出的。

4.定义一个顶层的方法来处理进入的请求。接受这个请求后,控制器发送它到合适的

RequestHandler上,通知processRequest不要声明任何异常,方法在栈顶能够捕获处理所有的内部错误。假如它不抛出一个异常,错误将常常进入Java Virtual MachineJVM)或者servlet 容器中。JVM或者容器将然后出现用户使用的空白页中的一个。最好你为自己编码。

最后,控制器将是最好的设计元素,addHandler方法允许你继承Controller不用修改Java源代码。

设计模式实战:控制反转模式

一个简单的控制反转的例子是使用控制器注册一个处理程序。你可能知道这样一个模式,叫做好莱坞原则,“don't call uswe'll call you”。对象注册作为事件处理程序,当事件发生时,对已注册对象调用一个钩子方法,控制反转让架构管理事件生命周期同时,允许开发者给框架事件插入自定义的处理程序。

 

 

3.1.2实现基本类

继续采用LIsting3.1中的接口,Listing3.2展示一个简单控制器类的初稿。

Listing3.2 The generic Controller

[...]

import java.util.HashMap;

import java.util.Map;

public class DefaulController implements Controller{

private Map requestHandlers = new HashMap();

protected RequestHandler getHandler(Request request){

if(!this.requestHandler.containsKey(request.getNmae())){

String message = "Cannot find handler for request name" 

+ "[" + request.getName() + "]";

throw new RuntimeException(message);

}

return (RequestHandler) this.requestHandlers.get

(request.getName());

}

public Response processRequest(Request request){

Response response;

try{

response = getHandler(request).process(request);

}

catch (Exception exception){

response = new ErrorResponse(request,exception);

}

return response;

}

public void addHandler(Request request,RequestHander requestHandler){

if (this.requestHandlers.containsKey(request.getName())){

throw new RuntimeException("A request handler has " + 

"already been registered for request naem"

+ "[" + request. getName() + "]")

}

else{

this.requestHandlers.put(request.getName

(),requestHandler);

}

}

}

1.第一步,声明一个hashMap(java.util.HashMap)充当请求处理器的注册表。

2.下一步,添加一个protected方法,getHandler,获取给定请求的RequestHandler

3.如果一个RequestHandler没有注册,将抛出一个RuntimeException

(java.lang.RuntimeExcption)异常,这是一个偶然事件表示成一个编程错误,而不是由用户或

者外部系统产生的问题。Java不需要你去使用方法签名声明RuntimeException。但是你依旧能作为异常捕获它。一个改进是将添加一个指定的异常给控制器框架(例如

NoSuitableRequestHandlerExceprion).

4.你的有效方法然后返回合适的处理器给它的调用者

5.processRequest方法是Controller类的核心,这个方法指派合适的处理器给请求和回传处理器的Response。假如一个异常产生,它将在ErrorResponse类中捕获,如Listing3.3所示。

6.最后,检查是否该处理程序的名称已注册。如果有的话然后抛出一个异常,看这个实现,请注意请求对象签名的次数。但是你仅仅使用它的名字,这种事情常常发生,当代码编写之前一个接口已经定义。一种方法去避免过度设计一个接口最好实践测试驱动开发(TDD)。

Lisiting3.3 Special responseclass signaling an error

[...]

public class ErrorResponse implements Response{

private Request origianlRequest;

private Exception originalException;

public ErrorResponse(Request request,Exception exception){

this.originalRequest = request;

this.originalException = exception;

}

public Request getOriginalRequest(){

return this.originalRequest;

}

public Exception getOriginalException(){

return this.originalException;

}

}

 

此时,你有一个粗糙但是有效的控制器骨架,表3.1展示上面这部分源代码如何要求。

Table3.1 Resolving the base requirements form the component

Requirement

Resolution

Accept requests

public Response processRequest(Request request)

Select handler

this.requestHandlers.get(request.getName())

Route requests

response=getRequestHandler(request).process(request);

Error handling

Cubclass ErrorResponse

 

对于许多开发者下一步将使用骨架控制器粗略地制作一个桩程序。作为测试注入开发者,我们能编写一个测试套件给控制器不用小题大做使用一个桩程序。这是单元测试之美,我们能编写一个包验证它是否工作,所有外部的常规Java应用。

3.2开始测试

合适的灵感让我们编写四个接口和两个开始类,假如我们现在不编写自动化测试,极限编程将找我们的麻烦。

列表3.2和列表3.3以一个简单的实现开始,我们在单元测试将做同样的事,我们可以探讨什么是尽可能简单的测试用例。

3.2.1测试DefaultController

DefaultController类的实例化的测试用例怎么实现,第一步在做任何有用的事是控制器构造它,因此我们开始,列表3.4展示引导测试代码,它构建DefaultController对象和设置测试框架。

Listing 3.4 TestDefaultControllera bootstrap iteration

[...]

import org.junit.core.Test;

import static org.junit.Assert.*;

public class TestDefaultController{

private DefaultController controller;

@Before

public void instantiate() throws Exception{

controller = new DefaultController();

}

@Test

public void testMethod(){

throw new RuntimeException("implement me");

}

}

1.测试类的名字需要以前缀Test开始,命名规则并不是必须的,但是一般都这么做,我们将类标记成测试类后,我们更容易地识别和在构建脚本中过滤它们。相对地,依赖你的本地语言,你可以更愿意使用Test作为类的前缀。

2.下一步,使用@Before注释实例化DefaultController的方法,这是JUnit框架调用测试方法之间的一个内建的扩展点。

3.插入一个虚拟的测试方法,以使有事件去运行。只要确认测试设施是工作的,你能开始添加真正的测试方法,虽然这个测试运行了,但是它依旧会失败,下一步将修补这个测试。

4.对还没有实现的测试代码最佳实践是抛出一个异常。它将阻止测试通过,提醒你必须实现这些代码。

现在你有一个引导测试,下一步决定首先测试什么。

JUnits details

@Before@After注释方法在每一个@Test方法执行之前和之后执行,不管测试失败或通过,它帮助你提取所有的公共逻辑,像实例化你的领域对象和设置它们在一些已知的状态。

你可以有很多这些你想要的方法,但要注意你将有超过一个@Before@After方法。它们的执行顺序是不用定义的。

JUnit也提供@BeforeClass@AfterClass注释你的方法在类级别,这个方法将被执行一次,在所有你的@Test方法之前或之后。再次,就像@Before@After注释一样,你可以有很多这些你想要的方法,它们的执行顺序是不用定义的。

你需要记住@Before/@After@BeforeClass/@AfterClass注释方法必须设置成public

@BeforeClass/@AfterClass注释方法必须设置成publicstatic

3.2.2添加一个处理器

现在已有一个引导测试,下一步是决定首先测试什么。我们使用DefaultController对象开始测试用例,因为这是这个练习的重点:创建一个控制器。编写一些代码确定它编译成功,但是如何看测试是否运行呢?

控制器的目的是处理请求和返回响应。但是在处理请求之前,设计调用添加一个

RequestHandler去做处理工作,因此,第一件事首先是:你将测试是否能添加一个

RequestHandler

在第一章中运行过这个测试和返回一个已知的结果。去看测试是否成功,你将比较期望值与测试对象的返回值是否一致,addHandler的签名是:

void addHandler(Request request,RequestHandler requestHandler)

添加一个RequestHandler,你需要一个已知的Request名字,去检查是否添加成功,你能使用DefaultController类中的getHandler方法,使用如下签名:

RequestHandler getHandler(Request request)

这样是可行的,因为getHandler方法访问权限是protected,测试类是位于同样的包中。这是一个原因去定义测试在同一个包下。

测试第一步,你能做如下事情:

n 添加一个RequestHandler,引用一个Request

n 取得一个RequestHandler和通过同样的Request

n 检查看是否得到同一个RequestHandler返回

n 测试来自哪里?

现在你知道你需要哪些对象,下一个问题是,“这些对象来自哪里?”,你因该向前并编写一些你将在应用中使用的对象。例如一个登录请求?

单元测试的重点是一次测试一个对象,在面向对象环境中,像Java,你设计对象同其它对象交互,创建一个单元测试,因此,你需要两种类型的对象:你测试的领域对象(domain object)和测试对象(test objects)同测试下的对象进行交互。

定义:DEFINITION

Domain object:在单元测试上下文中,就域对象而言,用于对比和比较应用程序中使用的对象和测试程序中使用的对象。在测试中任何对象被认为是一个域对象。

假如你需要另一个域对象,像登录请求,测试失败,罪魁祸首将很难确定,你可能不能去告诉这个问题是否是一个控制器或者请求,因此,在一系列测试的第一步,你将使用唯一的类是DefaultController,其它的一切将是指定的测试类。

JUnit最佳实践:单元测试的重点是一次测试一个对象

单元测试一个重要的方面是它们是细粒度的。单元测试独立检查你创建的每一个对象,这样只要问题发生就能隔离。假如你在测试中放置多个对象,你不能预测当改变发生时对象间如何交互,你可以在可预测的测试对象下测试对象,另一种软件测试形式,集成测试,检查对象之间的相互作用,第四章有更多关于其他类型的测试。

Where do test classes live

测试类将放置在哪里?Java提供几个选择,如下其中的一个:

n 在包中将它们标记类成public

n 在测试类中将它们设置成内部类

如果类是简单的可能保持那种形式,它们很容易作为内部类去编写它们,这个类的实例很简单,Listing3.5演示内部类添加到TestDefaultController类中。

Listing3.5Test classes as inner classes

public class TestDefaultController{

private class SampleRequest implements Request{

public String getName(){

return "Test";

}

}

private class SampleHandler implements RequestHandler{

public Response process(Request request) throws Exception{

return new SampleResponse();

}

}

private class SampleResponse implements Response{

//empty

}

}

1.首先,设置一个请求对象,返回已知的名字(Test)。

2.下一步,实现一个SampleHandler,接口调用process方法,因此你不得不那样编码。现在你没有测试process方法,因此它返回一个SampleResponse对象满足签名。继续定义一个空的SampleResponse以便可以实例化。

Listing3.5中,我们看Listing3.6演示一个添加RequestHandler测试。

Listing3.6 TestDefaultController.tetAddHandler

[...]

import static org.junit.Assert.*;

public class TestDefaultController{

@Test

public void testAddHandler(){

Request request = new SampleRequest();

RequestHandler handler = new SampleHandler();

controller.addHandler(request,handler);

RequestHandler handler2 = conrtoller.getHandler(request);

assertSame("Hander we set in conrotller should be teh same 

handler we get",handler2,handler);

}

}

1.给测试方法设置一个明显的名字,使用@Test注释测试方法。

2.记住实例化测试对象

3.这段代码是测试的重点:Controller(测试下的对象)添加测试处理器,注意

DefaultController对象是被@Before注释的方法实例化的

4.在新的变量名下回读处理器

5.检查是否得到相同的对象

JUnit最佳实践:选择有意义的测试方法名

你能知道用@Test注释的方法是一个测试方法。你也必须通过阅读方法名字能理解它是一个测试方法。虽然JUnit没有要求任何特定的规则来命名测试方法,一个好的规则是使用testxxx结构开始命名测试方法,xxx是要测试域方法的名字。当你添加其它测试与相同的测试名冲突时,移动到testxxxyyy结构,yyy描述测试的不同之处。不要害怕你测试方法的名字过长或者冗长。你将在本章最后看到,有时不太明显,一个测试是测试方法通过查找它的断言方法时,命名你的测试方法使用一个描述性的方式,必要时添加注释。

虽然它简单,但是这个单元测试证实了关键的前提用于存储和检索RequestHandler的机制是灵活且好的。假如addHandler或者getRequest在将来失败了,测试将迅速地甄别问题所在。

像这样创建许多测试,你将注意以下模式:

1.将测试环境设置成一个已知的状态(创建对象,获得资源)。预测试状态参考test fixture(测试夹具)

2.调用测试下的方法

3.确认测试结果,常常调用一个或多个断言(assert)方法。

 

3.2.3处理一个请求

我们看看控制器的核心测试目标,处理请求。按照惯例,我们在Listing3.7展示这个测试并回顾它。

Listing3.7 testProceesRequest method

import static org.junit.Assert.*;

public class TestDefaultController{

[...]

@Test

public void testProcessRequest(){

Request request = new SampleRequest();

RequestHandler handler = new SampleHandler();

controller.addHandler(request,handler);

Response response = conroller.processRequest(request);

assertNotNull("Must not return a null response",response);

assertEquals("Response should be of type SampleResponse",

SampleResponse.class,response.getClass());

}

}

1.首先,使用@Test注释测试方法,然后给出测试取一个简单统一的名字

2.设置测试对象和添加测试处理器

3.Response对象代码从Listing3.6分离出来,然后调用processRequest方法

4.验证返回的Response对象是否为空。这很重要,因为你在Response对象上调用getClass方法,如果Response对象为空,它将验证失败,并抛出一个令人畏惧的空指向异常

(NullPointerException),使用assertNotNull(Sring,Object)签名假如测试失败的话,错误显示是有意义和容易去理解的。但是如果使用assertNotNull(Object)签名,JUnit运行器能够显示一个栈跟踪java.lang.AssertionError异常而没有错误信息,这将使诊断更加困难。

5.最后,比较测试结果和期望结果(SampliResponse)是否一致。

 

JUnit最佳实践:在断言调用中解释失败原因

无论什么时候使用JUnit assert*方法,确认使用这样的签名形式,使用一个字符串(String)作为第一个参数,这个参数让你提供一个有意义的描述,假如断言失败的话显示Junit测试运行器信息,不要使用这样的参数,当失效发生时它很难去理解。

 

设置逻辑分解

因为两个测试做了同样的设置,你能复制代码进入@Before注释中,同一时刻,你又不想复制它到一个新的@Before方法中,因为你不确定将首先执行哪一个方法,可以得到一个异常。你能将代码移动到同样的@Before方法中。

随着你添加更过的测试方法,你可能需要调整如何做,在@Before方法中,现在,排除重复的代买尽可能帮助你编写更多的测试。Listing3.8,显示改进的TestDefaultController类,改近的地方以粗体显示。

Listing3.8 TestDefaultController After some refactoring

[...]

public class TestDefaultController{

private DefaultController controller;

private Request request; 

private RequestHandler handler;

@Before

public void initialize()throws Exception{

controller = new DefaultController();

request = new SampleRequest();

handler = new Samplehandler();

controller.addHandler(request,handler);

}

private class SampleRequest implements Request{

//Same as in listing 3.1

}

private class SampleHandler implements RequestHandler{

//Same as in listing 3.1

}

private class SampleResponse implements Response{

//Same as in listing 3.1

}

@Test

public void testAddHandler(){

RequestHandler handler2 = controller.gethandler(request);

assertSame(handler2,handler);

}

@Test

public void testProcessRequest(){

Response response =controller.processRequest(request);

assertNotNull("Must not return a null response",response);

assertEquals("response should be of type 

 

SampleResponse",SampleResponse.class,response.getClass());

}

}

1.我们移动测试RequestRequestHandler对象的初始化到initialize()方法中。

2.initialize()保存着testAddhandlertestProcessRequest中的重复代码。

3.因此,我们编写一个新的@Before注释方法以便添加处理器到控制器,因为@Before方法在每个单独@Test方法之前执行,我们确认我们完整地设置了DefaultConroller对象。

定义(definition):代码重构Refactor,改进已存在代码的设计。

 

注意在一个测试方法中,不要试图通过测试多个操作去共享设置代码,如Listing3.9所示。

Listing3.9 Anti-example:don't conbine test methods.

public class TestDefaultController{

@Test

public void tetsADdAndProcess(){

Request request = new SampleRequest();

Requesthandler handler = new Samplehandler();

controller.addhandler(request,handler);

RequestHandler handler2 = controller.gerhandler(request);

assertEquals(handler2,handler);

// DO NOT COMBINE TEST METHODS THIS WAY

Response response = conrtoller.processRequest(request);

assertNotNull("Must not return a null response",response);

assertEquals(SampleResponse.class,response.getCalss());

}

}

 

JUnit最佳实践:一个单元测试对应一个@Test方法

不要将多个测试塞入一个方法中。这将导致测试方法复杂化,变得越来越难以阅读和理解。更糟糕的是,测试方法中会有更过的逻辑,将增加不运行和调试的风险,滑坡将结束鞭子额测试来测试你的测试。

在程序中,当出现工作或者失败的时候,单元测试将给你信心,如果将多个单元测试放入一个测试方法中,它将变得更加困难地观察哪里出错。当测试共享同样的方法时,一个失败的测试会使夹具处于可预知的状态。在方法中其它的测试嵌入可能不会运行或者正确的运行,你的测试结果的图片通常是不完整的或误导的。

因为所有的测试方法在一个测试类中共享同样的夹具,JUnit能产生一个自动的测试套件,它很容易地替换每一个单元测试用它们自己的方法。假如你需要使用同样的代码块在多个测试间,把它们提取成一个实用方法,每一个测试方法都能调用它,更好地,假如所有的方法能共享代码,将它放入到夹具中。

另外常用的陷阱是编写测试方法并不包含断言声明,当你执行这些测试时,观察JUnit标记它们成功,但这是一个成功测试的假象,总是使用断言调用。只有一种情况不使用断言是可以接受的,当一个异常抛出表示一个错误条件时。

为了更好的结果,你的测试方法应当简明地、集中你的域方法。每个测试方法必须尽可能地清晰和集中,这就是为什么JUnit提供使用@Before@After@AfterClass@BeforeClass注释:因此你能共享夹具而不用组合测试方法。

3.2.4改进testPeocessRequest

Listing3.7中编写testProcessRequest方法时,我们希望响应返回的值是期望的回应,实现确认返回的对象是我们期望的对象。但是我们希望知道的是返回的响应是否与期望的响应相等。

这个响应可能是一个不同的类,重要的是这个类识别他自己是正确的响应。

assertSame方法确认两个引用是同一个对象,assertEquals方法同equals方法一样。从基类

Object继承而来,如果两个不同的对象有同样的身份,需要提供自己定义的ID。对于像一个响应对象,你能指派每个响应它自己的令牌。

空实现的SampleResponse没有名字属性可以测试,为了得到想要的测试,不得不首先实现更多的Response类。Listing3.10展示加强的SampleResponse类。

Listing3.10 A refactored SampleResponse

public class TestDefaultController{

private class SampleResponse implements Resonse{

private static final String NAME = "Test";

public String getName(){

return NAME;

}

public boolean equals(Object object){

boolean result = false;

if (object instanceof SampleREsponse){

result = ((SampleResponse) object).getName().equals(getName());

}

return result;

}

public  int hashCode(){

return NAME.hashCode();

}

}

}

现在SampleResponse有一个身份(表现getName())和它自己equals方法,你能修改这个测试方法。

@Test

public void testPeocessRequest(){

Response response = controller.processRequest(request);

assertNotNull("Must not return a null response",response);

assertEquals(new SampleREsponse(),response);

}

我们使用SampleResponse类介绍身份的概念在测试中的目的。但是测试告诉你应该有存在合适的Response类,你需要修改Response接口如下;

public interface Response{

String getName();

}

就像你看到这样,测试能告诉或者指导你一个好的设计,但是这不是测试真正的目的,不要忘记测试常常用于保护我们在代码中介绍错误。这样做我们需要测试每一个条件,在我们应用可执行的情况下,我们开始探讨异常条件在下一章中。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Junit)