【转】JUnit 入门

原文地址: http://www.cnblogs.com/Peiyuan/articles/511494.html



测试的重要性毋庸再说,但如何使测试更加准确和全面,而且独立于项目以外而且避免硬编码, JUnit 给了咱们一个很好的解决方案。

1、引子
    首先假设有一个项目类 SimpleObject 以下:


    public class SimpleObject {
        public List methodA (){
            .....
        }
    }


    其中定义了一个 methodA 方法返回一个对象,好,如今咱们要对这个方法进行测试,看他是否是返回一个 List 对象,是否是为空,或者长度是否是符合标准等等。咱们写这样一个方法判断返回对象是否不为 Null
    public void assertNotNull( Object object ){
        //判断object是否不为null
        ....
    }
    这个方法在 JUnit 框架中称之为一个断言, JUnit 提供给咱们了不少断言,好比 assertEqual , assertTrue ... ,咱们能够利用这些断言来判断两个值是否相等或者二元条件是否为真等问题。
    接下来咱们写一个测试类
    import junit.framework.*;
    public class TestSimpleObject extends TestCase {
        public TestSimpleObject( String name ){
            super( name);
        }
        public void testSimple (){
            SimpleObject so = new SimpleObject();
            assertNotNull( so . methodA());
        }
    }
    而后咱们能够运行 JUnit 来检测咱们的测试结果,这样咱们在不影响 Project 文件的前提下,实现了对 Project 单元的测试。

2、 JUnit 框架的结构
    经过前面的引子,其实咱们已经了解了 JUnit 基本的结构:
    1 import 声明引入必须的 JUnit
    2 、定义一个测试类从 TestCase 继承
    3 、必需一个调用 super( String) 的构造函数
    4 、测试类包含一些以 test .. 开头的测试方法
    5 、每一个方法包含一个或者多个断言语句
    固然还有一些其余的内容,但知足以上几条的就已是一个 JUnit 测试了

3、 JUnit 的命名规则和习惯
    1 、若是有一个名为 ClassA 的被测试函数 , 那么测试类的名称就是 TestClassA
    2 、若是有一个名为 methodA 的被测试函数,那么测试函数的名称就是 testMethodA

4、 JUnit 自定义测试组合
    JUnit 框相架下,他会自动执行全部以 test .. 开头的测试方法( 利用 java 的反射机制) ,若是不想让他这么“智能”,一种方法咱们能够改变测试方法的名称,好比改为 pendingTestMethodA , 这样测试框架就会忽略它;第二种方法咱们能够本身手工组合咱们须要的测试集合,这个魔力咱们能够经过建立 test suite 来取得,任何测试类都可以包含一个名为 suite 的静态方法:
    public static Test suite();
    仍是以一个例子来讲明,假设咱们有两个名为 TestClassOne TestClassTwo 的测试类,以下:
    import junit.framework.*;
    public class TestClassOne extends TestCase {
        public TestClassOne( String method ){
            super( method);
        }
        public void testAddition (){
            assertEquals( 4 , 2 + 2);
        }
        public void testSubtration (){
            assertEquals( 0 , 2 - 2);
        }
    }

    import junit.framework.*;
    public class TestClassTwo extends TestCase {
        public TestClassTwo( String method ){
            super( method);
        }
        public void testLongest (){
            ProjectClass pc = new ProjectClass();
            assertEquals( 100 , pc . longest());
        }
        public void testShortest (){
            ProjectClass pc = new ProjectClass();
            assertEquals( 1 , pc . shortest( 10));
        }
        public void testAnotherShortest (){
            ProjectClass pc = new ProjectClass();
            assertEquals( 2 , pc . shortest( 5));
        }
        public static Test suite (){
            TestSuite suite = new TestSuite();
            //only include short tests
            suite . addTest( new TestClassTwo( "testShortest"));
            suite . addTest( new TestClassTwo( "testAnotherShortest"));
        }
    }
    首先看 TestClassTwo ,咱们经过 suite 显式的说明了咱们要运行哪些 test 方法,并且,此时咱们看到了给构造函数的 String 参数是作什么用的了:它让 TestCase 返回一个对命名测试方法的引用。接下来再写一个高一级别的测试来组合两个测试类:
    import junit.framework.*;
    public class TestClassComposite extends TestCase {
        public TestClassComposite( String method ){
          super( method);
        }
        static public Test suite (){
          TestSuite suite = new TestSuite();
          //Grab everything
          suite . addTestSuite( TestClassOne . class);
          //Use the suite method
          suite . addTest( TestClassTwo . suite());
          return suite;
        }
    }
    组合后的测试类将执行 TestClassOne 中的全部测试方法和 TestClassTwo 中的 suite 中定义的测试方法。
   
5、 JUnit 中测试类的环境设定和测试方法的环境设定
    每一个测试的运行都应该是互相独立的;从而就能够在任什么时候候,以任意的顺序运行每一个单独的测试。
    虽然这样是有好处的,但咱们若是在每一个测试方法里都写上相同的设置和销毁测试环境的代码,那显然是不可取的,好比取得数据库联接和关闭链接。好在 JUnit TestCase 基类提供了两个方法供咱们改写,分别用于环境的创建和清理:
    protected void setUp();
    protected void tearDown();

    一样道理,在某些状况下,咱们须要为整个 test suite 设置一些环境,以及在 test suite 中的全部方法都执行完成后作一些清理工做。要达到这种效果,咱们须要针对 suite 作一个 setUp tearDown ,这可能稍微复杂一点,它须要提供所需的一个 suite( 不管经过什么样的方法) 而且把它包装进一个 TestSetup 对象

    看下面这个例子:
    public class TestDB extends TestCase {
        private Connection dbConn;
        private String dbName;
        private String dbPort;
        private String dbUser;
        private String dbPwd;
       
        public TestDB( String method ){
            super( method);
        }
        //Runs before each test method
        protected void setUp (){
          dbConn = new Connection( dbName , dbPort , dbUser , dbPwd);
          dbConn . connect();
        }
        //Runs after each test method
        protected void tearDown (){
          dbConn . disConnect();
          dbConn = null;
        }        
        public void testAccountAccess (){
          //Uses dbConn
          ....
        }        
        public void testEmployeeAccess (){
          //Uses dbConn
          ....
        }
        public static Test suite (){
          TestSuite suite = new TestSuite();
          suite . addTest( new TestDB( "testAccountAccess"));
          suite . addTest( new TestDB( "testEmployeeAccess"));
          TestSetup wrapper = new TestSetup( suite ){
              protected void setUp (){
                  oneTimeSetUp();
              }
              protected void tearDown (){
                  oneTimeTearDown();
              }
          }
          return wrapper;
        }
        //Runs at start of suite
        public static void oneTimeSetUp (){
          //load properties of initialization
          //one-time initialize the dbName,dbPort...
        }
        //Runs at end of suite
        public static void oneTimeTearDown (){
          //one-time cleanup code goes here...
        }        
    }

    上面这段代码的执行顺序是这样的:
    1 oneTimeSetUp()
    2   setUp();
    3     testAccountAccess();
    4   tearDown();
    5   setUp();
    6     testEmployeeAccess();
    7   tearDown();
    8 oneTimeTearDown();

6、自定义 JUnit 断言
    一般而言, JUnit 所提供的标准断言对大多数测试已经足够了。然而,在某些环境下,咱们可能更须要自定义一些断言来知足咱们的须要。
    一般的作法是定义一个 TestCase 的子类,而且使用这个子类来知足全部的测试。新定义的共享的断言或者公共代码放到这个子类中。

7、测试代码的放置
    三种放置方式 :
    1 、同一目录——针对小型项目
      假设有一个项目类 , 名字为
      com . peiyuan . business . Account
      相应的测试位于
      com . peiyuan . business . TestAccount
      即物理上存在于同一目录
     
      优势是 TestAccount 可以访问 Account protected 成员变量和函数
      缺点是测试代码处处都是,且堆积在产品代码的目录中

    2 、子目录
      这个方案是在产品代码的目录之下建立一个 test 子目录
      同上,假设有一个项目类 , 名字为
      com . peiyuan . business . Account
      相应的测试位于
      com . peiyuan . business . test . TestAccount
     
      优势是能把测试代码放远一点,但又不置于太远
      缺点是测试代码在不一样的包中,因此测试类没法访问产品代码中的 protected 成员,解决的办法是写一个产品代码的子类来暴露那些成员。而后在测试代码中使用子类。
     
      举一个例子,假设要测试的类是这样的:
      package com . peiyuan . business;
      public class Pool {
          protected Date lastCleaned;
          ....
      }
      为了测试中得到 non - public 数据,咱们须要写一个子类来暴露它
      package com . peiyuan . business . test;
      import com.peiyuan.business.Pool;
      public class PoolForTesting extends Pool {
          public Date getLastCleaned (){
              return lastCleaned;
          }
          ....
      }
 
    3 、并行树
      把测试类和产品代码放在同一个包中,但位于不一样的源代码树,注意两棵树的根都在编译器的 CLASSPATH 中。
      假设有一个项目类 , 位于
      prod / com . peiyuan . business . Account
      相应的测试位于
      test / com . peiyuan . business . TestAccount
     
      很显然这种作法继承了前两种的优势而摒弃了缺点,而且 test 代码至关独立

8、 Mock 的使用
    1 、基础
      截至目前,前面提到的都是针对基本的 java 代码的测试,可是假若遇到这样的状况:某个方法依赖于其余一些难以操控的东西,诸如网络、数据库、甚至是 servlet 引擎,那么在这种测试代码依赖于系统的其余部分,甚至依赖的部分还要再依赖其余环节的状况下,咱们最终可能会发现本身几乎初始化了系统的每一个组件,而这只是为了给某一个测试创造足够的运行环境让他能够运行起来。这样不只仅消耗了时间,还给测试过程引入了大量的耦合因素。
      他的实质是一种替身的概念。
      举一个例子来看一下:假设咱们有一个项目接口和一个实现类。以下:
      public interface Environmental {
            public long getTime();
      }
     
      public class SystemEnvironment implements Environmental {
            public long getTime (){
              return System . currentTimeMillis();
            }
      }
      再有一个业务类,其中有一个依赖于 getTime 的新方法
      public class Checker {
            Environmental env;
            public Checker( Environmental anEnv ){
              env = anEnv;
            }
            public void reminder (){
              Calendar cal = Calendar . getInstance();
              cal . setTimeInMillis( env . getTime());
              int hour = cal . get( Calendar . HOUR_OF_DAY);
              if( hour >= 17 ){
                  ......
              }
            }
      }  
      由上可见 , reminder 方法依赖于 getTime 为他提供时间,程序逻辑实在下午 5 点以后进行提醒动做,但咱们作测试的时候不可能等到那个时候,因此就要写一个假的 Environmental 来提供 getTime 方法,以下:
      public class MockSystemEnvironment implements Environmental {
            private long currentTime;
            public long getTime (){
              return currentTime;
            }
            public void setTime( long aTime ){
              currentTime = aTime;
            }
      }
      写测试的时候以这个类来替代 SystemEnvironment 就实现了替身的做用。

    2 MockObject
      接下来再看如何测试 servlet ,一样咱们须要一个 web 服务器和一个 servlet 容器环境的替身,按照上面的逻辑,咱们须要实现 HttpServletRequest HttpServletResponse 两个接口。不幸的是一看接口,咱们有一大堆的方法要实现,呵呵,好在有人已经帮咱们完成了这个工做,这就是 mockobjects 对象。
import junit.framework.*;
import com.mockobjects.servlet.*;

public class TestTempServlet extends TestCase {
  public void test_bad_parameter() throws Exception {
    TemperatureServlet s = new TemperatureServlet();
    MockHttpServletRequest request =   new MockHttpServletRequest();
    MockHttpServletResponse response =   new MockHttpServletResponse();
   
    //在请求对象中设置参数
    request . setupAddParameter( "Fahrenheit" , "boo!");
    //设置response的content type
    response . setExpectedContentType( "text/html");
    s . doGet( request , response);
    //验证是否响应
    response . verify();
    assertEquals( "Invalid temperature: boo!\ n" ,
    response . getOutputStreamContents());
  }

  public void test_boil() throws Exception {
    TemperatureServlet s = new TemperatureServlet();
    MockHttpServletRequest request =
    new MockHttpServletRequest();
    MockHttpServletResponse response =
    new MockHttpServletResponse();

    request . setupAddParameter( "Fahrenheit" , "212");
    response . setExpectedContentType( "text/html");
    s . doGet( request , response);
    response . verify();
    assertEquals( "Fahrenheit: 212, Celsius: 100.0\ n" ,
    response . getOutputStreamContents());
  }

}
    3 EasyMock
    EasyMock 采用“记录 ----- 回放”的工做模式,基本使用步骤:
    * 建立 Mock 对象的控制对象 Control
    * 从控制对象中获取所须要的 Mock 对象。
    * 记录测试方法中所使用到的方法和返回值。
    * 设置 Control 对象到“回放”模式。
    * 进行测试。
    * 在测试完毕后,确认 Mock 对象已经执行了刚才定义的全部操做

    项目类:
package com . peiyuan . business;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>Title: 登录处理</p>
* <p>Description: 业务类</p>
* <p>Copyright: Copyright (c) 2006</p>
* <p>Company: </p>
* @author Peiyuan
* @version 1.0
*/
public class LoginServlet extends HttpServlet {

    /* (非 Javadoc)
    * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
    */
    protected void doPost( HttpServletRequest request , HttpServletResponse response) throws ServletException , IOException {
        String username = request . getParameter( "username");
        String password = request . getParameter( "password");
        // check username & password:
        if( "admin" . equals( username) && "123456" . equals( password)) {
            ServletContext context = getServletContext();
            RequestDispatcher dispatcher = context . getNamedDispatcher( "dispatcher");
            dispatcher . forward( request , response);
        }
        else {
            throw new RuntimeException( "Login failed.");
        }
    }
}

    测试类:
package com . peiyuan . business;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;


import org.easymock.MockControl;
import junit.framework.TestCase;
/**
* <p>Title:LoginServlet测试类 </p>
* <p>Description: 基于easymock1.2</p>
* <p>Copyright: Copyright (c) 2006</p>
* <p>Company: </p>
* @author Peiyuan
* @version 1.0
*/
public class LoginServletTest extends TestCase {

    /**
    * 测试登录失败的状况
    * @throws Exception
    */
    public void testLoginFailed() throws Exception {
        //首先建立一个MockControl
        MockControl mc = MockControl . createControl( HttpServletRequest . class);
        //从控制对象中获取所须要的Mock对象
        HttpServletRequest request = ( HttpServletRequest) mc . getMock();
        //“录制”Mock对象的预期行为
        //在LoginServlet中,前后调用了request.getParameter("username")和request.getParameter("password")两个方法,
        //所以,须要在MockControl中设置这两次调用后的指定返回值。
        request . getParameter( "username"); // 指望下面的测试将调用此方法,参数为"username"
        mc . setReturnValue( "admin" , 1); // 指望返回值为"admin",仅调用1次
        request . getParameter( "password"); // 指望下面的测试将调用此方法,参数为" password"
        mc . setReturnValue( "1234" , 1); // 指望返回值为"1234",仅调用1次
        //调用mc.replay(),表示Mock对象“录制”完毕
        mc . replay();
        //开始测试
        LoginServlet servlet = new LoginServlet();
        try {
            //因为本次测试的目的是检查当用户名和口令验证失败后,LoginServlet是否会抛出RuntimeException,
            //所以,response对象对测试没有影响,咱们不须要模拟它,仅仅传入null便可。
            servlet . doPost( request , null);
            fail( "Not caught exception!");
        }
        catch( RuntimeException re) {
            assertEquals( "Login failed." , re . getMessage());
        }
        // verify:
        mc . verify();
    }
   
    /**
    * 测试登录成功的状况
    * @throws Exception
    */
    public void testLoginOK() throws Exception {
        //首先建立一个request的MockControl
        MockControl requestCtrl = MockControl . createControl( HttpServletRequest . class);  
        //从控制对象中获取所须要的request的Mock对象
        HttpServletRequest requestObj = ( HttpServletRequest) requestCtrl . getMock();
        //建立一个ServletContext的MockControl
        MockControl contextCtrl = MockControl . createControl( ServletContext . class);
        //从控制对象中获取所须要的ServletContext的Mock对象
        final ServletContext contextObj = ( ServletContext) contextCtrl . getMock();
        //建立一个RequestDispatcher的MockControl
        MockControl dispatcherCtrl = MockControl . createControl( RequestDispatcher . class);
        //从控制对象中获取所须要的RequestDispatcher的Mock对象
        RequestDispatcher dispatcherObj = ( RequestDispatcher) dispatcherCtrl . getMock();
        requestObj . getParameter( "username"); // 指望下面的测试将调用此方法,参数为"username"
        requestCtrl . setReturnValue( "admin" , 1); // 指望返回值为"admin",仅调用1次
        requestObj . getParameter( "password"); // 指望下面的测试将调用此方法,参数为" password"
        requestCtrl . setReturnValue( "123456" , 1); // 指望返回值为"1234",仅调用1次
        contextObj . getNamedDispatcher( "dispatcher");
        contextCtrl . setReturnValue( dispatcherObj , 1);
        dispatcherObj . forward( requestObj , null);
        dispatcherCtrl . setVoidCallable( 1);
        requestCtrl . replay();
        contextCtrl . replay();
        dispatcherCtrl . replay();
        //为了让getServletContext()方法返回咱们建立的ServletContext Mock对象,
        //咱们定义一个匿名类并覆写getServletContext()方法
        LoginServlet servlet = new LoginServlet() {
            public ServletContext getServletContext() {
                return contextObj;
            }
        };
        servlet . doPost( requestObj , null);
    }
}
相关文章
相关标签/搜索