【JUnit实战】为应用程序Controller设计单元测试
来源:互联网 发布:simulink 应用 知乎 编辑:程序博客网 时间:2024/05/01 23:37
在本章中,我们为一个简单但完整的应用程序controller创建了一个测试用例。测试用例并不是测试单个的组件,而是检验多个组例,如何一起工作。我们从一个可以用于任何类的简单测试用例开始.然后把新的测试逐个添加到测试用例中,直到所有初始的组件都被测试到。由于断言变得越来越复杂、因此我们通过Hamcrest匹配器找到了一种简化断言的方法。我们预期这个包会日益增长,所以我们为测试类创建了另一个源代码目录。因为测试源代码和领域源代码的目录都位于同一个包中,所以我们仍然可以测试受保护成员和默认的包成员。
1.被测部分:Controller模式
1.1 Controller简介
一般而言, Controller可以处理以下事务
- 接受请求
- 根据请求执行任意常用极端
- 选择 一个合适的请求处理器
- 路由请求,以便处理器可以执行相关的业务逻辑
- 可能提供一个顶层处理器来处理错误和异常
1.2 设计接口
Controller模式中涉及四个角色:
- Request
- Response
- RequestHandler
- Controller
Controller接受一个Request,分发给一个RequestHandler,并返回一个Response对象。
//首先,定义一个 Request 接口,这个接口只有一个返问请求的唯一名称的getName方法public interface Request{ String getName();}//其次指定一个空接口。要开始编写代码,你只需要返回一个 Response对象即可。Response 对象所封装的是你可以稍后处理的内容。public interface Response{}//接下来,定义一个能够处理 Request 并返回 Response 的 RequestHandle,RequestHandle是一个辅助组件,被设计用来处理大部分的“肮脏工作”。它可以调用各种类,这些类可能抛出任意类型的异常。Exception就是由process万法抛出的。public interface RequestHandler{ Response procees(Request request) throws Exception;}//定义一个顶层方法来处理收到的请求。在接受请求之后, controller将请求分发给相应的RequestHandler 。public interface Controller{ Response process(Request request); //add Handler 方法允许你扩展Controller,而无须修改 Java原代码。 void addHandler(Request request,RequestHandler requestHandler)}
controller的目的是处理一个请求并返回一个响应。但是,在你处理一个请求之前,设计要求添加一个RequestHandler来做这个处理。
package com.JUnittTest.mastery;import java.util.HashMap;import java.util.Map;public class DefaultController {// 请求处理器注册表,对每一个request注册对应的requestHandler private Map requestHandlers=new HashMap();// 声明一个受保护的方法,为接受的请求获取RequestHandler protected RequestHandler getHandler(Request request){ if(!this.requestHandlers.containsKey(request.getName())){ String message="Cannot find handler for request name "+"["+request.getName()+"]"; throw new RuntimeException(message); }// 向调用者返回相应的requestHandler return (RequestHandler)this.requestHandlers.get(request.getName()); }// 是Controller类的核心,把response分派给相应的requestHandler,并传回requestHandler的response public Response processRequest(Request request){ Response response; try {// getHandler(request)返回一个RequestHandler接口类型的对象// RequestHandler接口定义了process(request)方法,返回一个response对象 response=getHandler(request).process(request); } catch (Exception exception) { response=new ErrorResponse(request,exception); } return response; }// 检查requestHandler是否已经被注册 public void addHandler(Request request,RequestHandler requestHandler){// 如果被注册了就抛出一个异常 if(this.requestHandlers.containsKey(request.getName())){ throw new RuntimeException("A request handler has "+"already been registered for request name "+"["+request.getName()+"]"); }else{ this.requestHandlers.put(request.getName(), requestHandler); } }}
我们还需要额外再定义一个ErrorPesponse接口,不同于posponse接口,ErrorPesponse接口返回的是错误的posponse。
package com.JUnittTest.mastery;public interface Response { String getName();}
2. 设计单元测试
2.1 测试前部署 @Before @BeforeClass
@Before @After 注释方法会在每个@Test方法前后执行
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Before {}
@BeforeClass @AfterClass 注释方法会在只会在所有@Test方法前后执行一次
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface BeforeClass {}
从源代码分析,@Before @BeforeClass 注释非常简单,没有内部属性,只是起到对代码运行顺序的引导。(@After @AfterClass也是一样的)
一般在@BeforeClass @AfterClass注释方法中的代码完成测试环境的部署和拆除。
在@Before @After注释方法中的代码是对公共属性和对象的声明,一般是从@Test注释方法代码中重构得来的。
2.2 单元测试 @Test
分析@Test源代码可知,该注释拥有两个属性expected(),timeout()
expected是单元测试预期抛出异常的类对象,带有该属性的单元测试在抛出预期的异常后测试才会通过。timeout是用来测试单元运行时间的属性,单位为毫秒。要求单元执行时间不能超过设定值,否则测试失败。
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface Test { /** * Default empty exception */ static class None extends Throwable { private static final long serialVersionUID= 1L; private None() { } } /** * Optionally specify <code>expected</code>, a Throwable, to cause a test method to succeed iff * an exception of the specified class is thrown by the method. */ Class<? extends Throwable> expected() default None.class; /** * Optionally specify <code>timeout</code> in milliseconds to cause a test method to fail if it * takes longer than that number of milliseconds.*/ long timeout() default 0L; }
2.3 测试类
测试类存于何处:
- 放在包中作为公有类
- 作为测试用例类的内部类(类很简单,且不会有后续的改变)
本例中,我们采用第二种方式,被测代码有四个角色
- Request
- Response
- RequestHandler
- Controller
因此我们创建前三个角色为Controller的内部类,实现原有的方法:
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{ ... }}
2.4 两种测试对象
要创建一个单元测试,需要创建两种类型的对象:
- Domain Object: 领域对象(被测对象)
- Test Object:测试对象(与被测对象交互的对象)
在一个@Test注释方法中,只测试一个对象(Domain Object),其他对象(Test Object)与被测对象交互完成单元测试
2.5 单元用例设计
2.5.1 单元测试编写模式:
- 通过把环境设直成已知状态(如创建对象,获取资源)来创建测试. 测试前的状态通常称为 Test Fixture.
- 调用待测试的方法。
- 确认测试结呆,通常通过调用一个或更多的assert方法来实现
主干+分支
2.5.2 单元用例覆盖:主干+分支+可扩展性
先从程序的正向主干逻辑出发,覆盖最基本的逻辑单元。
然后通过分析条件语句,try/catch语句,找到分支路径进行逐一覆盖。
可扩展性主要通过测试单元运行时间是否达到要求来给出评估。
主干用例
测试点1:是否能添加一个RequestHandler
- 添加一个RequestHandler,引用一个Request
- 获取一个RequestHandler并传递同一个Request
- 检查获得的RequestHandler是否就是添加的那一个
@Test// 测试ProcessRequest方法的主干流程 public void testProcessRequest(){// 这部分代码被多个测试类复用,因此我们把它移动到@Before注释的方法中 /*Request request=new SampleRequest(); RequestHandler handler=new SampleHandler(); controller.addHandler(request, handler);*/// 调用被测方法,然后验证会不会返回方法指定的对象// 被测方法有参数列表,有返回值,测试方法仍然是无参且无返回值的方法 Response response=controller.processRequest(request); assertNotNull("Must not return a null response: ",response);// 让测试结果与预期结果的类进行比较,进一步确定返回值的正确性// assertEquals("Response should be of type SampleResponse",SampleResponse.class,response.getClass()); assertEquals(new SampleResponse(),response); }
测试点2:
@Test// 测试addHandler方法的主干流程 public void testAddHandler(){// 这部分代码被多个测试类复用,因此我们把它移动到@Before注释的方法中 /*Request request=new SampleRequest(); RequestHandler handler=new SampleHandler();// 引用一个Request,添加一个RequestHandler, controller.addHandler(request, handler);*/// 获取一个RequestHandler并传递同一个Request RequestHandler handler2=controller.getHandler(request);// 检查获得的RequestHandler是否就是添加的那一个 assertSame("Handler we set in controller should be the same handler we got",handler2,handler); }
分支用例
测试点3:
@Test// 测试ProcessRequest方法的异常处理流程 public void testProcessRequestAnswersErrorResponse(){// 使用SampleRequest带一个参数的构造方法为对象实例request初始化fixture SampleRequest request=new SampleRequest("testError"); SampleExceptionHandler handler=new SampleExceptionHandler(); controller.addHandler(request, handler); Response response=controller.processRequest(request);// 检查被测方法是否得到返回值 assertNotNull("Must not return a null response: ",response);// 检查返回值是不是预期的异常对象 assertEquals(ErrorResponse.class,response.getClass()); }
这里还需要添加一个测试类,用于模拟processRequest(Request request)抛出异常的情况。
// 用来测试processRequest(Request request)的异常处理流程创建的类// processRequest(Request request)方法中调用了接口RequestHandler的方法// 直接抛出一个异常看程序的try-catch语句块能否如预期捕捉到 private class SampleExceptionHandler implements RequestHandler{ @Override public Response process(Request request) throws Exception { throw new Exception("error processing request"); } }
测试点4:
@Test(expected=RuntimeException.class)// 测试getHandler方法的异常处理流程,预期希望抛出一个异常,则在@Test的属性expected属性中定义异常类,这样这条测试就不会因为有异常而阻塞 public void testGetHandlerNotDefined(){ SampleRequest request=new SampleRequest("testGetHandlerNotDefined"); //The following line is supposed to throw a RuntimeException controller.getHandler(request); }
测试点5:
@Test(expected=RuntimeException.class)// 测试addHandler方法的异常处理流程,预期希望抛出一个异常,则在@Test的属性expected属性中定义异常类,这样这条测试就不会因为有异常而阻塞 public void testAddRequestDuplicateName(){ SampleRequest request=new SampleRequest(); SampleHandler handler=new SampleHandler();// The following line is supposed to throw a RuntimeException controller.addHandler(request, handler); }
可扩展性测试
测试点6:
@Test(timeout=120) @Ignore(value="Skip for now")//@Ignore将会对本单元测试跳过执行 value声明跳过的原因 public void testProcessMultipleRequestsTimeout(){ Request request; Response response=new SampleResponse(); RequestHandler handler=new SampleHandler(); for(int i=0;i<99999;i++){ request=new SampleRequest(String.valueOf(i)); controller.addHandler(request, handler); response=controller.processRequest(request); assertNotNull(response); assertNotSame(ErrorResponse.class,response.getClass()); } }
完整的单元测试代码
package com.JUnittTest.mastery;import static org.junit.Assert.*;import org.junit.Before;import org.junit.Ignore;import org.junit.Test;public class TestDefaultController { private DefaultController controller; private Request request; private RequestHandler handler; @Before// 实例化DefaultController public void instantiate() throws Exception{ controller=new DefaultController(); request=new SampleRequest(); handler=new SampleHandler(); controller.addHandler(request, handler); } /*@Test// 对还没有实现的测试代码抛出一个异常 public void testMethod(){ throw new RuntimeException("implement me"); }*/ @Test// 测试ProcessRequest方法的主干流程 public void testProcessRequest(){// 这部分代码被多个测试类复用,因此我们把它移动到@Before注释的方法中 /*Request request=new SampleRequest(); RequestHandler handler=new SampleHandler(); controller.addHandler(request, handler);*/// 调用被测方法,然后验证会不会返回方法指定的对象// 被测方法有参数列表,有返回值,测试方法仍然是无参且无返回值的方法 Response response=controller.processRequest(request); assertNotNull("Must not return a null response: ",response);// 让测试结果与预期结果的类进行比较,进一步确定返回值的正确性// assertEquals("Response should be of type SampleResponse",SampleResponse.class,response.getClass()); assertEquals(new SampleResponse(),response); } @Test// 测试addHandler方法的主干流程 public void testAddHandler(){// 这部分代码被多个测试类复用,因此我们把它移动到@Before注释的方法中 /*Request request=new SampleRequest(); RequestHandler handler=new SampleHandler();// 引用一个Request,添加一个RequestHandler, controller.addHandler(request, handler);*/// 获取一个RequestHandler并传递同一个Request RequestHandler handler2=controller.getHandler(request);// 检查获得的RequestHandler是否就是添加的那一个 assertSame("Handler we set in controller should be the same handler we got",handler2,handler); } @Test// 测试ProcessRequest方法的异常处理流程 public void testProcessRequestAnswersErrorResponse(){// 使用SampleRequest带一个参数的构造方法为对象实例request初始化fixture SampleRequest request=new SampleRequest("testError"); SampleExceptionHandler handler=new SampleExceptionHandler(); controller.addHandler(request, handler); Response response=controller.processRequest(request);// 检查被测方法是否得到返回值 assertNotNull("Must not return a null response: ",response);// 检查返回值是不是预期的异常对象 assertEquals(ErrorResponse.class,response.getClass()); } @Test(expected=RuntimeException.class)// 测试getHandler方法的异常处理流程,预期希望抛出一个异常,则在@Test的属性expected属性中定义异常类,这样这条测试就不会因为有异常而阻塞 public void testGetHandlerNotDefined(){ SampleRequest request=new SampleRequest("testGetHandlerNotDefined"); //The following line is supposed to throw a RuntimeException controller.getHandler(request); } @Test(expected=RuntimeException.class)// 测试addHandler方法的异常处理流程,预期希望抛出一个异常,则在@Test的属性expected属性中定义异常类,这样这条测试就不会因为有异常而阻塞 public void testAddRequestDuplicateName(){ SampleRequest request=new SampleRequest(); SampleHandler handler=new SampleHandler();// The following line is supposed to throw a RuntimeException controller.addHandler(request, handler); } @Test(timeout=120) @Ignore(value="Skip for now")//@Ignore将会对本单元测试跳过执行 value声明跳过的原因 public void testProcessMultipleRequestsTimeout(){ Request request; Response response=new SampleResponse(); RequestHandler handler=new SampleHandler(); for(int i=0;i<99999;i++){ request=new SampleRequest(String.valueOf(i)); controller.addHandler(request, handler); response=controller.processRequest(request); assertNotNull(response); assertNotSame(ErrorResponse.class,response.getClass()); } } private class SampleRequest implements Request{// SampleRequest类有一个默认属性,作为其实例的fixture private static final String DEFAULT_NAME="Test"; private String name;// 无参的构造方法将默认属性载入初始化 public SampleRequest(){ this(DEFAULT_NAME); }// 带String参数的构造方法可以自定义对象实例的属性 public SampleRequest(String name) { this.name = name; } public String getName(){ return this.name; } } private class SampleHandler implements RequestHandler{ public Response process(Request request) throws Exception{ return new SampleResponse(); } } private class SampleResponse implements Response{ private static final String NAME="Test"; public SampleResponse() { // TODO Auto-generated constructor stub } //给SampleResponse创建标识,主要是为了测试而设计 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(); } }// 用来测试processRequest(Request request)的正常处理,创建的类// processRequest(Request request)方法中调用了接口RequestHandler的方法// 直接抛出一个异常看程序的try-catch语句块能否如预期捕捉到 private class SampleExceptionHandler implements RequestHandler{ @Override public Response process(Request request) throws Exception { throw new Exception("error processing request"); } }}
3. 测试结果
去掉testProcessMultipleRequestsTimeout的@Ignore注释后的结果
4. 引入Hamcrest匹配器
package com.JUnittTest.mastery;import static org.junit.Assert.assertTrue;import java.util.ArrayList;import java.util.List;import org.junit.Before;import org.junit.Test;import static org.junit.Assert.assertThat;import static org.hamcrest.CoreMatchers.anyOf;import static org.hamcrest.CoreMatchers.equalTo;import static org.junit.matchers.JUnitMatchers.hasItem;/** * A sample hamcrest test. * * @version $Id: HamcrestTest.java 553 2010-03-06 12:29:58Z paranoid12 $ */public class HamcrestTest { private List<String> values; @Before public void setUpList() { values = new ArrayList<String>(); values.add("x"); values.add("y"); values.add("z"); } @Test public void testWithoutHamcrest() { assertTrue(values.contains("one") || values.contains("two") || values.contains("three")); } @Test// 引入Hamcrest匹配器可以简化测试断言,使断言更具有可读性,Hamcrest语句是可以嵌套使用的 public void testWithHamcrest() { assertThat(values, hasItem(anyOf(equalTo("one"), equalTo("two"), equalTo("three")))); }}
引入Hamcrest匹配器之后,会显示下面的结果,而没有Hamcrest匹配器的话,只会显示Failure Trace,可读性比较差
5. JUnit最佳实践
- 一次只能单元测试一个对象
- 选择有意义的测试方法名字
- 在assert调用中解释失败的原因
- 一个单元测试等于一个@Test方法
- 测试任何可能失败的事物
- 让测试改善代码
- 是异常测试更易于阅读
- 总是为跳过的测试说明原因
- 相同的包,分离的目录
- 【JUnit实战】为应用程序Controller设计单元测试
- Junit对Spring Controller进行单元测试
- JUnit in java单元测试用例实战
- Spring对Controller、Service、Dao进行Junit单元测试总结
- Spring对Controller、Service、Dao进行Junit单元测试总结
- JUnit单元测试
- JUnit 单元测试
- 单元测试JUnit
- Junit单元测试
- Junit单元测试
- junit单元测试
- JUnit单元测试
- Junit单元测试
- JUnit单元测试
- junit单元测试
- Junit单元测试
- Junit 单元测试
- Junit单元测试
- 基于Token的WEB后台认证机制
- 运动目标检测--背景减法
- Unknown verification type [*] in stack map frame
- ITSmobile
- CH13,p242.练习二,控制上传文件大小
- 【JUnit实战】为应用程序Controller设计单元测试
- 05 讲: 闲聊之 Python 的数据类型
- Java RESTful框架的性能比较
- 信用证项下的贸易融资——背对背信用证
- 在披萨店里的思考
- Getting to know ITSmobile by Checking out the Internet Services
- PowerDesigner的使用_数据模型与表之间的互相生成
- Hibernate学习--映射文件xxx.hbm.xml详解
- 深度学习Deep Learning(03):权重初始化问题1_Sigmoid\tanh\Softsign激励函数