Jade Dungeon

Mockito

Mockito

起步

<dependency>
	<groupId>org.mockito</groupId>
	<artifactId>mockito-all</artifactId>
	<version>1.8.5</version>
	<scope>test</scope>
</dependency>

创建mock对象不能对final,Anonymous ,primitive类进行mock。

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;

public class SimpleTest {

	@Test
	public void simpleTest() {

		// 创建mock对象,参数可以是类,也可以是接口 

		List<String> list = mock(List.class);

		// Stubbing阶段:
		// 指定用什么参数调用什么方法,应该返回什么结果。
		// 当调用`get(0)`时,返回值是`helloworld`
		
		when(list.get(0)).thenReturn("helloworld");

		// 调用阶段就是实际调用
		
		String result = list.get(0);
		
		// Verify阶段:
		// 检查上一行调用的方法,传入的参数,
		// 调用的次数等场景是不是和Stubbing阶段定义的严格匹配

		verify(list).get(0);

		// 接下来对返回值的操作就是JUnit的事情了
		
		Assert.assertEquals("helloworld", result);
	}

}

对象被mock了以后,所有的方法都被覆盖为只返回null的默认实现。 另一种很有用的方法是Mockito.CALLS_REAL_METHODS给抽象方法都加上默认的实现 (不覆盖已经实现的方法),就不用为了测试抽象类中已经实现的方法自己动手写了一个 子类了:

abstract class MyAbs {
	public String methodUnderTest() {
		return "hello";
	}

	protected abstract void methodIDontCareAbout();
}

@Test
public void shouldFailOnNullIdentifiers() {
	MyAbs my = Mockito.mock(MyAbs.class, Mockito.CALLS_REAL_METHODS);
	Assert.assertEquals("hello", my.methodUnderTest());
}

常用方法

指定预期结果

对象被mock了以后,所有的方法都被覆盖为只返回null (或空集合,或0等基本类型实例)的默认实现。

when()方法可以按指定的方法名和指定的参数,定义返回值:

when(list.get(0)).thenReturn("helloworld");

doReturn()功能一样,语法风格上不同(注意get(1)操作在参数的外面):

doReturn("secondhello").when(list).get(1);
  • 对于 static 和 final 方法, Mockito 无法对其when()thenReturn()操作。
  • 当我们连续两次为同一个方法使用 stub 的时候,他只会只用最新的一次。

指定迭代器式的返回结果

对于迭代器,有三种方式都是等价的:

第一种方式

when(i.next()).thenReturn("Hello").thenReturn("World");  

第二种方式

when(i.next()).thenReturn("Hello", "World");  

第三种方式

when(i.next()).thenReturn("Hello");  
when(i.next()).thenReturn("World");  

指定没有返回值的方法

doNothing()用来模拟没有返回值的方法。

doNothing().when(obj).notify();

或:

when(obj).notify();

指定抛出异常

thenThrow()doThrow()可对方法设定返回异常:

when(list.get(1)).thenThrow(new RuntimeException("test excpetion"));

抛异常的void方法:

doThrow(new RuntimeException()).when(i).remove();

例如,第一次调用时什么都不做,第二次调用时抛异常:

doNothing().doThrow(new RuntimeException("void exception")).when(list).clear();  

list.clear();  
list.clear();  

// 验证list.clear()方法已经被调用过了两次
verify(list,times(2)).clear();  

用迭代器迭代时,第一次什么都不做,第二次抛异常:

doNothing().doThrow(new RuntimeException()).when(i).remove();

抛出自定义异常

在用mockito来模拟异常的时候,当要抛出自定义的异常, 而非RuntimeException等自定义异常时,常常会出现如下错误信息:

Checked exception is invalid for this method!

以前我可以通过来绕过这个错误:

doThrow(CheckedException.class).when(service).show(anyObject)

但是当抛出的异常要携带某些信息时,就不能不给异常传参了,于是就得这么写:

doThrow(new CheckedException("error")).when(service).show(anyObject)

接着,你会看到编译通过,但运行时报上面的异常

我们来看下上面异常的意思,大概是说检查到这个异常对这个方法无效。 检查了这个方法后,发现这个方法并没有显示声明或抛出这个我们自定义的异常。 必须在方法声明后加上throw CheckedException

验证方法已经被调用

基本的验证方法verify方法验证mock对象是否有没有调用mockedList.add("once")方法。 不关心其是否有返回值,如果没有调用测试失败。

验证是否调用了get(0)

verify(list).get(0);

验证调用次数

单单通过结果来判断正确与否还是不够的,还要判断是否按我指定的步骤执行一系列操作。

  • times():方法指定调用的次数,没有指定的话默认就是time(1)
  • never(): 没有被调用,相当于 times(0)
  • atLeast(N): 至少被调用 N 次
  • atLeastOnce(): 相当于 atLeast(1)
  • atMost(N): 最多被调用 N 次
List<String> mockedList = mock(List.class);

// using mock
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

基本的验证方法verify方法验证mock对象是否有没有调用mockedList.add("once")方法。 不关心其是否有返回值,如果没有调用测试失败。

verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");// 默认调用一次,times(1)可以省略

verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

// never()等同于time(0),一次也没有调用
verify(mockedList, times(0)).add("never happened");

// atLeastOnece/atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("twice");
verify(mockedList, atMost(5)).add("three times");

查询多余的方法调用

verifyNoMoreInteractions()方法可以传入多个mock对象作为参数,用来验证传入的这些mock对象是否存在没有验证过的调用方法。本例中传入参数mock,测试将不会通过,因为我们只verify了mock对象的get(2)方法,没有对get(0)和get(1)进行验证。为了增加测试的可维护性,官方不推荐我们过于频繁的在每个测试方法中都使用它,因为它只是测试的一个工具,只在你认为有必要的时候才用。

查询没有交互的mock对象

verifyZeroInteractions()也是一个测试工具,源码和verifyNoMoreInteractions()的实现是一样的,为了提高逻辑的可读性,所以只不过名字不同。在例子中,它的目的是用来确认mock2对象没有进行任何交互,但mock2执行了get(0)方法,所以这里测试会报错。由于它和verifyNoMoreInteractions()方法实现的源码都一样,因此如果在verifyZeroInteractions(mock2)执行之前对mock.get(0)进行了验证那么测试将会通过。

超时验证

Mockito提供对超时的验证,但是目前不支持在下面提到的顺序验证中使用。进行超时验证和上述的次数验证一样,也要在verify中进行参数的传入,参数为timeout(int millis),timeout方法中输入的是毫秒值。下面看例子:

验证someMethod()是否能在指定的100毫秒中执行完毕

verify(mock, timeout(100)).someMethod();

结果和上面的例子一样,在超时验证的同时可进行调用次数验证,默认次数为1

verify(mock, timeout(100).times(1)).someMethod();

在给定的时间内完成执行次数

verify(mock, timeout(100).times(2)).someMethod();

给定的时间内至少执行两次

verify(mock, timeout(100).atLeast(2)).someMethod();

另外timeout也支持自定义的验证模式,

verify(mock, new Timeout(100, yourOwnVerificationMode)).someMethod(); 

验证方法调用的顺序

Mockito同样支持对不同Mock对象不同方法的调用次序进行验证。进行次序验证是,我们需要创建InOrder对象来进行支持。例:

创建mock对象

List<String> firstMock = mock(List.class);
List<String> secondMock = mock(List.class);

调用mock对象方法

firstMock.add("was called first");
firstMock.add("was called first");
secondMock.add("was called second");
secondMock.add("was called third");

创建InOrder对象

inOrder方法可以传入多个mock对象作为参数,这样便可对这些mock对象的方法进行调用顺序的验证InOrder inOrder = inOrder( secondMock, firstMock );

验证方法调用

接下来我们要调用InOrder对象的verify方法对mock方法的调用顺序进行验证。注意,这里必须是你对调用顺序的预期。

InOrder对象的verify方法也支持调用次数验证,上例中,我们期望firstMock.add("was called first")方法先执行并执行两次,所以进行了下面的验证inOrder.verify(firstMock,times(2)).add("was called first")。其次执行了secondMock.add("was called second")方法,继续验证此方法的执行inOrder.verify(secondMock).add("was called second")。如果mock方法的调用顺序和InOrder中verify的顺序不同,那么测试将执行失败。

InOrder的verifyNoMoreInteractions()方法

它用于确认上一个顺序验证方法之后,mock对象是否还有多余的交互。它和Mockito提供的静态方法verifyNoMoreInteractions不同,InOrder的验证是基于顺序的,另外它只验证创建它时所提供的mock对象,在本例中只对firstMock和secondMock有效。例如:

inOrder.verify(secondMock).add("was called second");
inOrder.verifyNoMoreInteractions();

在验证secondMock.add("was called second")方法之后,加上InOrder的verifyNoMoreInteractions方法,表示此方法调用后再没有多余的交互。例子中会报错,因为在此方法之后还执行了secondMock.add("was called third")。现在将上例改成:

inOrder.verify(secondMock).add("was called third");
inOrder.verifyNoMoreInteractions();

测试会恢复为正常,因为在secondMock.add("was called third")之后已经没有多余的方法调用了。如果这里换成Mockito类的verifyNoMoreInteractions方法测试还是会报错,它查找的是mock对象中是否存在没有验证的调用方法,和顺序是无关的。

参数匹配器

Matchers类已经定义了很多参数匹配器,如:any()anyInt()anyObject()anyString()anySet()anyMap()等。

List<String> list = mock(List.class);
when(list.get(anyInt())).thenReturn("hello", "world");

String result = list.get(0) + list.get(1);

// 验证list.get()方法已经被调用过了两次
verify(list, times(2)).get(anyInt());

Assert.assertEquals("helloworld", result);

如果使用参数匹配器,那么所有的参数都要使用参数匹配器, 不管是stubbing还是verify的时候都一样:

Map<Integer, String> map = mock(Map.class);

// anyString()替换成"hello"就会报错
when(map.put(anyInt(), anyString())).thenReturn("hello");

// eq("world")替换成"world"也会报错
map.put(1, "world");
verify(map).put(eq(1), eq("world"));

匹配自定义的类

anyObject只是能匹配int、String、List等所有java自定义类型。 而对于MyClass这类自己自定义的类,通过org.mockito.MatchersargThat()方法, 创建自定义的匹配器。

例,参数的类型为于自定义的类MyModel

class MyModel {
	private String name;

	public MyModel(String name) { this.name = name; }

	public String getName() { return name; }
}

class MyModelService {

	public String func(MyModel m) { return m.getName(); }

}

需要定义一个ArgumentMatcher来实现检查是不是子类的逻辑:

class IsMyModel extends ArgumentMatcher<MyModel> {

	@Override
	public boolean matches(Object arg) { return arg instanceof MyModel; }

}

IsMyModel实例作为参数,调用argThat()方法就可以得到匹配器:

@Test
public void testMyModelArg() {
	MyModelService srv = mock(MyModelService.class);
	when(srv.func(argThat(new IsMyModel()))).thenReturn("hello");

	String result = srv.func(new MyModel("Not hello"));

	verify(srv).func(argThat(new IsMyModel()));
	Assert.assertEquals("hello", result);
}

ArgumentCaptor介绍

通过ArgumentCaptor对象的forClass(Class<T> clazz)方法来构建ArgumentCaptor对象。 然后便可在验证时对方法的参数进行捕获,最后验证捕获的参数值。如果方法有多个参数都要捕获验证,那就需要创建多个ArgumentCaptor对象处理。

ArgumentCaptor的Api:

  • argument.capture(): 捕获方法参数
  • argument.getValue(): 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值
  • argument.getAllValues(): 方法进行多次调用后,返回多个参数值

例:

@Test
public void argumentCaptorTest() {  
    List mock = mock(List.class);  
    List mock2 = mock(List.class);  
    mock.add("John");  
    mock2.add("Brian");  
    mock2.add("Jim");  
      
    ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);  
      
    verify(mock).add(argument.capture());  
    assertEquals("John", argument.getValue());  
      
    verify(mock2, times(2)).add(argument.capture());  
  
    assertEquals("Jim", argument.getValue());  
    assertArrayEquals(new Object[]{"Brian","Jim"},
				argument.getAllValues().toArray());  
}  

首先构建ArgumentCaptor需要传入捕获参数的对象,例子中是String。接着要在verify方法的参数中调用argument.capture()方法来捕获输入的参数,之后argument变量中就保存了参数值,可以用argument.getValue()获取。当某个对象进行了多次调用后,如mock2对象,这时调用argument.getValue()获取到的是最后一次调用的参数。如果要获取所有的参数值可以调用argument.getAllValues(),它将返回参数值的List。

在某种程度上参数捕获器和参数匹配器有很大的相关性。它们都用来确保传入mock对象参数的正确性。然而,当自定义的参数匹配器的重用性较差时,用参数捕获器会更合适,只需在最后对参数进行验证即可。

Hamcrest框架

Mockito参数匹配器的实现使用了Hamcrest框架, 一个书写匹配器对象时允许直接定义匹配规则的框架,网址:

http://code.google.com/p/hamcrest/

它已经提供了许多规则供我们使用, Mockito在此基础上也内建了很规则。 但有时我们还是需要更灵活的匹配,所以需要自定义参数匹配器。

ArgumentMatcher抽象类

自定义参数匹配器的时候需要继承ArgumentMatcher抽象类, 它实现了Hamcrest框架的Matcher接口,定义了describeTo方法, 所以我们只需要实现matches()方法在其中定义规则即可。

下面自定义的参数匹配器是匹配size大小为2的List:

class IsListOfTwoElements extends ArgumentMatcher<List> {  

  public boolean matches(Object list) { return ((List) list).size() == 2; }  
    
}  
  
@Test  
public void argumentMatchersTest() {
    List mock = mock(List.class);  
    when(mock.addAll(argThat(new IsListOfTwoElements()))).thenReturn(true);  
       
    mock.addAll(Arrays.asList("one", "two", "three"));  
    verify(mock).addAll(argThat(new IsListOfTwoElements()));  
}  

argThat(Matcher<T> matcher)方法用来应用自定义的规则, 可以传入任何实现Matcher接口的实现类。

上例中在stubbing和verify addAll方法时通过argThat(Matcher<T> matcher), 传入了自定义的参数匹配器IsListOfTwoElements用来匹配size大小为2的List。 因为例子中传入List的元素为三个,所以测试将失败。

较复杂的参数匹配将会降低测试代码的可读性。 有时实现参数对象的equals()方法是个不错的选择 (Mockito默认使用equals()方法进行参数匹配),它可以使测试代码更为整洁。 另外,有些场景使用参数捕获器(ArgumentCaptor)要比自定义参数匹配器更加合适。

返回调用结果

有些情况下不希望在`stubbing中写死返回值,而是希望真的调用方法得到实际的结果。 Answer接口可以调用方法中实现的逻辑。

例:模拟常见的request.getAttribute(key),由于这本来是个接口, 所以连内部实现都要自己写了。此次通过 Answer 接口获取参数内容。

final Map<String, Object> hash = new HashMap<String, Object>();  

Answer<String> aswser = new Answer<String>() {    
    public String answer(InvocationOnMock invocation) {    
        Object[] args = invocation.getArguments();    
        return hash.get(args[0].toString()).toString();    
    }   
};  
  
when(request.getAttribute("isRawOutput")).thenReturn(true);   
when(request.getAttribute("errMsg")).thenAnswer(aswser);   
when(request.getAttribute("msg")).thenAnswer(aswser);  

利用InvocationOnMock提供的方法可以获取 mock 方法的调用信息。下面是它提供的方法:

  • getArguments():调用后会以 Object 数组的方式返回 mock 方法调用的参数。
  • getMethod(): 返回java.lang.reflect.Method对象
  • getMock(): 返回 mock 对象
  • callRealMethod(): 真实方法调用,如果 mock 的是接口它将会抛出异常

void 方法可以获取参数,只是写法上有区别:

doAnswer(new Answer<Object>() {  
    public Object answer(InvocationOnMock invocation) {  
        Object[] args = invocation.getArguments();  
        // Object mock = invocation.getMock();    
        System.out.println(args[1]);  
        hash.put(args[0].toString(), args[1]);  
        return "called with arguments: " + args;  
    }  
}).when(request).setAttribute(anyString(), anyString());  

其实就是一个回调,如果不是接口,是实现类的话,估计不用自己写实现。

Spy

spy 的意思是你可以修改某个真实对象的某些方法的行为特征,而不改变他的基本行为特征,这种策略的使用跟 AOP 有点类似。下面举官方的例子来说明:

List list = new LinkedList();    
List spy = spy(list);    
    
//optionally, you can stub out some methods:    
when(spy.size()).thenReturn(100);    
     
//using the spy calls <b>real</b> methods    
spy.add("one");    
spy.add("two");    
     
//prints "one" - the first element of a list    
System.out.println(spy.get(0));    
     
//size() method was stubbed - 100 is printed    
System.out.println(spy.size());    
     
//optionally, you can verify    
verify(spy).add("one");    
verify(spy).add("two");  

可以看到 spy 保留了 list 的大部分功能,只是将它的 size() 方法改写了。不过 spy 在使用的时候有很多地方需要注意,一不小心就会导致问题,所以不到万不得已还是不要用 spy。

注解

如果没有 JUnit,可以使用 Mockito 的 @Before 的注解,进行一些前期的初始化工作,

public class ArticleManagerTest {  

    @Mock private ArticleCalculator calculator;  
    @Mock private ArticleDatabase database;  
    @Mock private UserProvider userProvider;  
  
    @Before public void setup() {  
        MockitoAnnotations.initMocks(this);  
    }  
}   

如果有 JUnit,则无需 @Before,但要修改 JUnit 默认容器,

@RunWith(MockitoJUnitRunner.class)  
public class ExampleTest {  
    @Mock private List list;  
  
    @Test public void shouldDoSomething() {  
        list.add(100);  
    }  
}  

Mock注解

使用@Mock注解来定义mock对象有如下的优点:

  1. 方便mock对象的创建
  2. 减少mock对象创建的重复代码
  3. 提高测试代码可读性
  4. 变量名字作为mock对象的标示,所以易于排错

@Mock注解也支持自定义name和answer属性。

下面是官方给出的@Mock使用的例子:

public class ArticleManagerTest extends SampleBaseTestCase {  
    @Mock   
    private ArticleCalculator calculator;  
    @Mock(name = "dbMock")   
    private ArticleDatabase database;  
    @Mock(answer = RETURNS_MOCKS)   
    private UserProvider userProvider;  
  
    private ArticleManager manager;  
  
    @Before   
    public void setup() {  
        manager = new ArticleManager(userProvider, database, calculator);  
    }  
}  

public class SampleBaseTestCase {  
    @Before   
    public void initMocks() {  
        MockitoAnnotations.initMocks(this);  
    }  
}  

Spy注解

Spy的使用方法请参阅前面的章节,在此不再赘述,下面是使用方法:

public class Test{  
    @Spy   
    Foo spyOnFoo = new Foo();  
  
    @Before  
    public void init(){  
       MockitoAnnotations.initMocks(this);  
    }  

}  

Captor注解

@Captor是参数捕获器的注解,有关用法见前章,通过注解的方式也可以更便捷的对它进行定义。使用例子如下:

public class Test {  
    @Captor  
    ArgumentCaptor<AsyncCallback<Foo>> captor;  
    @Before  
    public void init() {  
        MockitoAnnotations.initMocks(this);  
    }  
  
    @Test  
    public void shouldDoSomethingUseful() {  
        verify(mock.doStuff(captor.capture()));  
        assertEquals("foo", captor.getValue());  
    }  
}  

InjectMocks注解

通过这个注解,可实现自动注入mock对象。当前版本只支持setter的方式进行注入,Mockito首先尝试类型注入,如果有多个类型相同的mock对象,那么它会根据名称进行注入。当注入失败的时候Mockito不会抛出任何异常,所以你可能需要手动去验证它的安全性。

例:

@RunWith(MockitoJUnit44Runner.class)  
public class ArticleManagerTest {  
    @Mock  
    private ArticleCalculator calculator;  
    @Mock  
    private ArticleDatabase database;  
    @Spy  
    private UserProvider userProvider = new ConsumerUserProvider();  
    @InjectMocks  
    private ArticleManager manager = new ArticleManager();  
      
    @Test  
    public void shouldDoSomething() {  
        manager.initiateArticle();  
        verify(database).addListener(any(ArticleListener.class));  
    }  
}  

上例中,ArticleDatabase是ArticleManager的一个属性,由于ArticleManager是注解@InjectMocks标注的,所以会根据类型自动调用它的setter方法为它设置ArticleDatabase。

Mockito模拟Servlet

这里假设我们没有 Tomcat(虽然不太可能,假设吧!),那就使用 Mockito 模拟一个看看怎么样。本文结合 RESTful 接口来进行回归测试的目的。

模拟 ServletContextListener

Listener 是启动 App 的第一个模块,相当于执行整个 Web 项目的初始化工作,所以也必须先模拟 ServletContextListener 对象。通过初始化的工作是安排好项目的相关配置工作和先缓存一些底层的类(作为 static 成员保存在内存中)。

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;

import org.junit.Before;
import org.junit.Test;

public class TestApplication {
	private Application app;
	private ServletContext sc;

	@Before
	public void setUp() throws Exception {
		sc = mock(ServletContext.class);
		// 指定类似 Tomcat 的虚拟目录,若设置为 "" 表示 Root 根目录
		when(sc.getContextPath()).thenReturn("/zjtv");
		// 设置项目真实的目录,当前是 返回 一个特定的 目录,你可以不执行该步
		when(sc.getRealPath(anyString()))
				.thenReturn(
						"C:\\project\\zjtv\\WebContent"
								+ Constant.ServerSide_JS_folder);
		// 设置 /META-INF 目录,当前使用该目录来保存 配置
		when(sc.getRealPath("/META-INF")).thenReturn(
				"C:\\project\\zjtv\\WebContent\\META-INF");

		app = new Application();
	}

	@Test
	public void testContextInitialized() //
		throws IOException, ServletException 
	{
		ServletContextEvent sce = mock(ServletContextEvent.class);
		when(sce.getServletContext()).thenReturn(sc);
		app.contextInitialized(sce);
		assertNotNull(sce);
		assertTrue("App started OK!", Application.isConfig_Ready);
	}
}

上述代码中 Application app 是 javax.servlet.ServletContextListener 的实现。你可通过修改 setUp() 里面的相关配置,应适应你的测试。

模拟 Servlet

背景简介:由于这是 JSON RESTful 接口的原因,所以我使用同一个 Servlet 来处理,即 BaseServlet,为 HttpServlet 的子类,而且采用 Servlet 3.0 的注解方式定义 URL Mapping,而非配置 web.xml 的方式,代码组织更紧凑。——从而形成针对最终业务的 zjtvServlet 类,为 BaseServlet 的子类,如下,

package jadeutils.net.servlet;

import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;

@WebServlet(urlPatterns = { "/service/*", "/admin_service/*" }, initParams = {
		@WebInitParam(name = "news", value = "ajaxjs.data.service.News"),
		@WebInitParam(name = "img", value = "ajaxjs.data.service.subObject.Img"),
		@WebInitParam(name = "catalog", value = "zjtv.SectionService"),
		@WebInitParam(name = "live", value = "ajaxjs.data.ext.LiveService"),
		@WebInitParam(name = "vod", value = "ajaxjs.data.ext.VodService"),
		@WebInitParam(name = "compere", value = "zjtv.CompereService"),
		@WebInitParam(name = "misc", value = "zjtv.MiscService"),
		@WebInitParam(name = "user", value = "ajaxjs.data.user.UserService"), })
public class ZjtvServlet extends BaseServlet {
	private static final long serialVersionUID = 1L;
}

其中我们注意到,

urlPatterns = {"/service/*", "/admin_service/*"},  

就是定义接口 URL 起始路径,因为使用了*,所以可以允许我们/service/news//service/product/200形成各种各样的 REST 接口。

但是,我们不对 zjtvServlet 直接进行测试,而是其父类 BaseServlet 即可。个中原因是我们模拟像 WebServlet 这样的注解比较不方便。 虽然是注解,但最终还是通过某种形式的转化,形成 ServletConfig 对象被送入到 HttpServlet.init 实例方法中去。于是我们采用后一种方法。

我们试观察BaseServlet.init(ServletConfig config)方法,还有每次请求都会执行的doAction(),发现这两步所执行过程中需要用到的对象,及其方法是这样的,

public class BaseServlet {

	/**
	 * 初始化所有 JSON 接口 为了方便测试,可以每次请求加载一次 js 文件,
	 * 于是重载了一个子方法 private void init(String Rhino_Path)
	 */
	public void init(ServletConfig config) throws ServletException {
		init(Application.Rhino_Path);

		// 遍历注解的配置,需要什么类,收集起来,放到一个 hash 之中
		Enumeration<String> initParams = config.getInitParameterNames();
		while (initParams.hasMoreElements()) {
			String initParamName = initParams.nextElement()
			String initParamValue = config
					.getInitParameter(initParamName);

			System.out.println("initParamName:" + initParamName
					+ ", initParamValue:" + initParamValue);

			initParamsMap.put(initParamName, initParamValue);
		}
	}

	private void doAction(HttpServletRequest request,
			HttpServletResponse response) 
	{
		// 为避免重启服务器,调试模式下再加载 js
		if (Application.isDebug)
			init(Application.Rhino_Path);

		response.setContentType("application/json");

		Connection jdbcConn = DAO.getConn(getConnStr());

		try {
			Object obj = Application.jsRuntime.call(
					"bf_controller_init", request, jdbcConn);
			if (obj != null)
				response.getWriter().println(obj.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}

		output(request, response);
		try {
			jdbcConn.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

}

于是,我们遵循「依赖什么,模拟什么」的原则,让 Mockito 为我们生成模拟的对象,以假乱真。

首先,我们不能忘记这是一个 Web 项目,因此开头讲的那个 Listener 类也要首当其冲被初始化, 才能有 Servlet 正确执行。于是,在 JUnit 单元测试的起始工作中,执行,

@Before  
public void setUp() throws Exception {  
    TestApplication app = new TestApplication();  
    app.setUp();  
    app.testContextInitialized();  
}  

同时也把 setUp()、testContextInitialized() 手动执行一遍,因为之前的时候, 我们是让 JUnit 或者 Tomcat 自动执行的。运行这一步之后,我们就初始化完毕侦听器 Listener 了。

这里所涉及的对象和方法比较多,下面我们逐一分解。

模拟 ServletConfig 对象

接着,怎么通过「模拟注解」来初始化 Servlet 配置呢?这里涉及到一个 Enumeration 对象的模拟,——其实也挺好办,方法如下,

/** 
 * 初始化 Servlet 配置,这里是模拟 注解 
 * @return 
 */  
private ServletConfig initServletConfig(){  
    ServletConfig servletConfig = mock(ServletConfig.class);  
    // 模拟注解  
    Vector<String> v = new Vector<String>();  
    v.addElement("news");  
    when(servletConfig.getInitParameter("news")).thenReturn(
				"ajaxjs.data.service.News");  
    v.addElement("img");  
    when(servletConfig.getInitParameter("img")).thenReturn(
				"ajaxjs.data.service.subObject.Img");  
    v.addElement("catalog");  
    when(servletConfig.getInitParameter("catalog")).thenReturn(
				"zjtv.SectionService");  
    v.addElement("user");  
    when(servletConfig.getInitParameter("user")).thenReturn(
				"ajaxjs.data.user.UserService");  

    Enumeration<String> e = v.elements();   
    when(servletConfig.getInitParameterNames()).thenReturn(e);  
      
    return servletConfig;  
}  

你可以定义更多业务对象,就像注解那样,结果无异。

模拟 Request 对象

下面所有虚拟的 Request 方法都可以按照你的项目配置进行修改

/** 
 * 请求对象 
 * @return 
 */  
private HttpServletRequest initRequest(){  
    HttpServletRequest request = mock(HttpServletRequest.class);  
    when(request.getPathInfo()).thenReturn("/zjtv/service/news");  
    when(request.getRequestURI()).thenReturn("/zjtv/service/news");  
    when(request.getContextPath()).thenReturn("/zjtv");  
    when(request.getMethod()).thenReturn("GET");  
    // 设置参数  
    when(request.getParameter("a")).thenReturn("aaa");  
      
    final Map<String, Object> hash = new HashMap<String, Object>();  
    Answer<String> aswser = new Answer<String>() {    
        public String answer(InvocationOnMock invocation) {    
            Object[] args = invocation.getArguments();    
            return hash.get(args[0].toString()).toString();    
        }    
    };  
      
    when(request.getAttribute("isRawOutput")).thenReturn(true);    
    when(request.getAttribute("errMsg")).thenAnswer(aswser);    
    when(request.getAttribute("msg")).thenAnswer(aswser);    
      
    doAnswer(new Answer<Object>() {  
        public Object answer(InvocationOnMock invocation) {  
            Object[] args = invocation.getArguments();  
            // Object mock = invocation.getMock();    
            System.out.println(args[1]);  
            hash.put(args[0].toString(), args[1]);  
            return "called with arguments: " + args;  
        }  
    }).when(request).setAttribute(anyString(), anyString());  
      
    return request;  
}  

其中比较麻烦的 request.getAttribute() / setAttribute() 方法。鉴于 HttpServlet 是接口的缘故,我们必须实现一遍 getAttribute() / setAttribute() 的内部实现。此次我们只是简单地利用一个 map 来保存 reuqest.setAttribute() 的信息。然后使用 Mockito 的 Answer 接口获取真实的参数如何,从而让 request.getAttribute() 返回具体的值。

最初看到的做法是这样,

class StubServletOutputStream extends ServletOutputStream {  
    public ByteArrayOutputStream baos = new ByteArrayOutputStream();  
    public void write(int i) throws IOException {  
        baos.write(i);  
    }  
    public String getContent() {  
        return baos.toString();  
    }  
}  

上述是个内部类,实例化如下,

StubServletOutputStream servletOutputStream = new StubServletOutputStream();  
when(response.getOutputStream()).thenReturn(servletOutputStream);  
        ……
doPost(request, response);  
byte[] data = servletOutputStream.baos.toByteArray();  
System.out.println("servletOutputStream.getContent:" + servletOutputStream.baos.toString());  

我不太懂 Steam 就没深入了,再 Google 下其他思路,结果有人提到把响应结果保存到磁盘中,我觉得不是太实用,直接返回 String 到当前测试上下文,那样就好了。

HttpServletResponse response = mock(HttpServletResponse.class);  
StubServletOutputStream servletOutputStream = new StubServletOutputStream();  
when(response.getOutputStream()).thenReturn(servletOutputStream);  
StringWriter writer = new StringWriter();  
when(response.getWriter()).thenReturn(new PrintWriter(writer)); 

测试后,用 writer.toString() 返回服务端响应的结果。

模拟数据库

怎么模拟数据库连接?可以想象,模拟数据库的工作量比较大,干脆搭建一个真实的数据库得了。所以有人想到的办法是用 Mockito 绕过 DAO 层直接去测试 Service 层,对 POJO 充血。

不过我当前的方法,还是直接连数据库。因为是使用 Tomcat 连接池的,所以必须模拟 META-INF/context.xml 的配置,其实质是 Java Naming 服务。模拟方法如下,

/** 
 * 模拟数据库 链接 的配置 
 * @throws NamingException 
 */  
private void initDBConnection() throws NamingException{  
     // Create initial context  
    System.setProperty(Context.INITIAL_CONTEXT_FACTORY, 
				"org.apache.naming.java.javaURLContextFactory");  
    System.setProperty(Context.URL_PKG_PREFIXES, "org.apache.naming");    
    // 需要加入tomcat-juli.jar这个包,tomcat7此包位于tomcat根目录的bin下。  
    InitialContext ic = new InitialContext();  
    ic.createSubcontext("java:");  
    ic.createSubcontext("java:/comp");  
    ic.createSubcontext("java:/comp/env");  
    ic.createSubcontext("java:/comp/env/jdbc");  
    // Construct DataSource  
    try {  
        SQLiteJDBCLoader.initialize();  
    } catch (Exception e1) {  
        e1.printStackTrace();  
    }  

    SQLiteDataSource dataSource = new SQLiteDataSource();  
    dataSource.setUrl(
				"jdbc:sqlite:c:\\project\\zjtv\\WebContent\\META-INF\\zjtv.sqlite");  
      
    ic.bind("java:/comp/env/jdbc/sqlite", dataSource);  
}  

至此,我们就可以模拟一次 HTTP 请求,对接口进行测试了!