伪装
- 伪装方法和伪装类
- 为测试设置伪装
- 可以被伪装的方法种类
- 内联的伪装类
- 伪装接口
- 伪装未被实现的类
- 调用次数的约束
- 伪装方法的初始化
- 使用调用上下文
- 执行真正的实现部分
- 在测试时间重用伪装
- 使用 before/after 方法
- 重用伪装类
- 在测试类和测试套件级别使用伪装
在 JMockit toolkit中, 伪装 API 提供了建立假的实现或者是伪装。 伪装区别于模拟的API在与,前者不仅仅只在测试调用的中被调用的时候返回我们所期待的值,我们还可以修改其中的依赖,使得它能满足测试的需要。通常,只是伪装部分的方法或者是构造函数在需要伪装的类中,使其他的方法和构造函数不修改。
伪装的实现在依赖于外部的实体或者是比如网络,文件系统的资源集成测试中特别有用。伪装的实现可以接触外部的实体在相同的集成环境中使用两种模式: 1) 真实的模式,所有的代码(被测试的代码和它的依赖)被正常的执行; 2) 模拟模式, 有问题的依赖将会被伪装的实例进行替换,因此他们可以在没有网络连接,没有文件系统,甚至没有任何他们所需要的外部依赖的情况下成功运行。使用伪装的对象对真实实现的替换对于使用这些依赖的代码来说是完全透明的,而且可以在不同的测试运行的时候进行开启或者是关闭。
对于这章的剩余部分,我们将为一个使用javax.security.auth.login.LoginContext 类型进行用户验证的应用编写测试。 在这个例子中,我们必须要所有的测试真实运行在任何 JAAS 代码,因为它可能依赖于外部的配置,而且很不容易在开发者的测试环境中部署起来。因此,一个依赖于LoginContext 的应用类将会被测试,然而 LoginContext 类 (所依赖的)将至少有一些方法和构造函数被伪装,为了测试验证逻辑。
伪装方法和伪装类
在 Mockups API的上下文中, mock方法可以使一个伪装类中的任何方法,一个伪装的类通过使用@Mock 注释。简单的来说,在这章我们将简单注释伪装的方法称为"mocks";在其他的上下文中"mock" 可能只的是一个伪装类的实例。 一个伪装的类可以使一个任何继承自 mockit.MockUp
public final class MockLoginContext extends MockUp<LoginContext>
{
@Mock
public void $init(String name, CallbackHandler callbackHandler)
{
assertEquals("test", name);
assertNotNull(callbackHandler);
}
@Mock
public void login() {}
@Mock
public Subject getSubject() { return null; }
}
当伪装类被使用到一个真实类的时候,之后获取对应被临时实现这些方法和构造函数的实现的将会被替换。换句话来说,真实的被伪装的类将在测试运行的时候被按照约定的方式进行调用修改。在运行时,被伪装的方法或者是构造函数将会被拦截和重定向到对应被伪装的函数中,然后执行并返回给原始的调用者(除非有 exception/error 被抛出), 不是当前方法而是一个不同的方法被真实的执行。通常在测试中的调用类的依赖类是伪装类。
伪装类通常在JUnit/TestNG测试类中被定义为嵌套的 (静态), 内部的 (非静态), 或甚至是匿名类。 尽管伪装类也可以是顶端的类。这在多一个测试用例类中复用伪装对象比较有用。正如我们将看到的,通常最方便的方法来实现一个伪装类是通过使他们成为匿名本地的类对于每一个测试方法。
当我们在生产代码中定义的真实类需要在测试中被伪装,一个新的伪装类将会被建立。其中必须至少顶一个伪装的方法,还可以包含任何数量的其他方法和构造方法; 它仍然可以定义任意多个字段。
每一个被 @Mock的方法必须有和在目标真实对象上的真实的方法或构造函数具有相同的签名。对于伪装的方法,签名包括 方法名和参数; 对于伪装的构造函数, 它仅仅是参数以及使用特殊的"$init"作为伪装的方法名。 如果在给定的伪装方法没有找到匹配的方法或构造函数在定义的实际类或者时其超类中 (不包括 java.lang.Object), 将会在尝试使用伪装类的时候抛出 IllegalArgumentException。注意这个异常可能被一个修改过的实际类导致(例如重命名实际的方法名), 因此需要明白它为什么会发生。
最后,注意不必伪装在实际类中所有的方法或者是构造方法。任何没有被伪装的在实际类中的方法将会入它原来一样,不会被伪装。当然是在假定没有其他的伪装类被使用在相同的实际类中,有是合法的(有时这比较有用)。 当有两个或者是多个伪装类使用在同一相同的实际类中,需要确保被伪装的类不能定义相同的伪装,如果有重复的定义,最后一个定义将会被使用。
为测试设置伪装
使用给定的伪装类对应于真实的类需要有一个结果。我们称这个为伪装类的设置。这些事情通常在独立测试方法或者是的在 @BeforeMethod (TestNG) 或 @Before (JUnit 4) 方法中执行。一旦一个伪装被设置,所有对于实际类中伪装方法和构造方法的执行将会被自动重定向到对应的伪装方法中。
为了设置上面的 MockLoginContext伪装类,我们简单的将其实例化:
@Test
public void settingUpAMockClass() throws Exception
{
new MockLoginContext());
// Inside an application class which creates a suitable CallbackHandler:
new LoginContext("test", callbackHandler).login();
...
}
因为伪装类是在一个测试方法中被设置,被伪装的 LoginContext类通过 MockLoginContext将只在特殊的测试中有效。
当构造方法被调用初始化 LoginContext的时候, 对应的 "$init" 伪装方法在 MockLoginContext中将会被执行, 假定有正确的调用参数。同样的的,当LoginContext#login 方法被调用的时候,对应的伪装方法将会被执行,在这个例子中将会不做任何东西,由于这个方法没有参数和返回值。这些调用发生在测试的第一部分就被建立的伪装实例上。
以上的部分示例简单的验证了LoginContext 类被使用正确的参数和特定包含一个上下文名称和回调的句柄所构造所初始化。 如果真实对象完全没有被初始化,测试仍将通过(除非包含一些其他的条件导致其失败)。login 方法的调用对于这个测试的输出没有影响, 除了这个调用只是执行了一个空的伪装方法而不是真实的那个。
现在,要是我们想模拟一个不同的验证失败错误?LoginContext#login() 方法声明其将抛出一个 LoginException "如果验证失败", 我们可以很简单的实现(使用 JUnit 4 如下所示):
public static class MockLoginContextThatFailsAuthentication extends MockUp<LoginContext>
{
@Mock
public void $init(String name) {}
@Mock
public void login() throws LoginException
{
throw new LoginException();
}
}
@Test(expected = LoginException.class)
public void settingUpAnotherMockClass() throws Exception
{
new MockLoginContextThatFailsAuthentication();
// Inside an application class:
new LoginContext("test").login();
}
这个测试只有在 LoginContext#login() 方法抛出一个异常才会通过,,只有当对应的伪装方法被执行。
可以被伪装的方法种类
目前为止,我们只使用公开的伪装方法伪装了实例的公共方法。实际上任何在实际类上任何种类的方法都可以被伪装: 私有方法,保护或是包保护级别的, 静态方法, 最终方法, 和本地方法。(同样包括同步和 strictfp 方法, 但是这些修改只会影响方法的实现,而不是它们的接口。)甚至一个实际类中的静态方法都可以被通过一个伪装实例方法进行修改,反之亦然(一个实例真方法包含一个静态的伪装); 这些都可以被使用到 final 修饰符中。
被伪装的方法需要包含一个实现,虽然不必要是字节码上(在本地方法的例子中)。因此,一个抽象的方法不能够被直接的伪装,对于Java 接口也一样。 (那就是说如下的 Mockups API 将会自动的生产一个代理对象,实现这个接口。)
内联的伪装类
通常对于各实际类的一个特定组的伪装方法只会在一个测试中有用。在这种情况下,我们将会建立一个匿名类在各自的测试方法中,正如下面的例子所展示的。
@Test
public void settingUpMocksUsingAnAnonymousMockClass() throws Exception
{
new MockUp<LoginContext>() {
@Mock void $init(String name) { assertEquals("test", name); }
@Mock void login() {}
});
new LoginContext("test").login();
}
注意这里的伪装方法不需要申明为 public。
伪装接口
大多数情况下伪装类总是面对直接使用在一个实际的对象上。但是假如我们需要伪装一个实现确定接口的对象传入代码中进行测试时怎么办?如下示例显示了如何实现接口javax.security.auth.callback.CallbackHandler。
@Test
public void mockingAnInterface() throws Exception
{
CallbackHandler callbackHandler = new MockUp<CallbackHandler>() {
@Mock
void handle(Callback[] callbacks)
{
assertEquals(1, callbacks.length);
assertTrue(callbacks[0] instanceof NameCallback);
}
}.getMockInstance();
callbackHandler.handle(new Callback[] {new NameCallback("Enter name:")});
}
MockUp#getMockInstance() 方法返回了一个实现目标接口的代理对象。
伪装未被实现的类
为了展示这个功能,我们使用如下的测试代码。
public interface Service { int doSomething(); }
final class ServiceImpl implements Service { public int doSomething() { return 1; } }
public final class TestedUnit
{
private final Service service1 = new ServiceImpl();
private final Service service2 = new Service() { public int doSomething() { return 2; } };
public int businessOperation()
{
return service1.doSomething() + service2.doSomething();
}
}
我们想要测试的方法 businessOperation(), 实现了一个特定的接口Service的类。 这些实现是通过一个匿名的内部类实现的,而这些内容通过客户端代码是无法获取的(除了使用反射的方式)。
给定一个基类(可以使一个接口,一个抽象类,或者是任何种类的基类), 我们可以编写一个测试只需要知道这个基类,它的所有实现和扩展都将被伪装。为了做到这个,我们需要建立一个伪装,一个伪装类型只知道其基类,而被通过类型变量的方式进行使用。不仅仅已经被JVM载入的实现类可以被伪装,那些在测试执行的时候将要被JVM载入的其他类也可以被伪装。这些能力将被如下所示。
@Test
public <T extends Service> void mockingImplementationClassesFromAGivenBaseType()
{
new MockUp<T>() {
@Mock int doSomething() { return 7; }
};
int result = new TestedUnit().businessOperation();
assertEquals(14, result);
}
在上面的测试中,所有对 Service#doSomething() 方法实现将被重定向到被伪装的实现中,而不是原来实际上实现接口的类。
调用次数的约束
所以的测试用例实例到目前为止只是使用了JUnit/TestNG 来判断样子调用的参数。 有时,我们可能想验证给定的依赖的方法或构造方法在整个测试用被调用。我们可能想验证实际上一个给定的伪装在测试中被调用了多少次,或定义如果大于或者是少于给定的调用次数将会测试失败。为了实现这个,我们可以定义在一个给定伪装中调用次数的约束,如下例子所示。
@Test
public void specifyingInvocationCountConstraints() throws Exception
{
new MockUp<LoginContext>() {
@Mock(minInvocations = 1)
void $init(String name) { assertEquals("test", name); }
@Mock(invocations = 1)
void login() {}
@Mock(maxInvocations = 1)
void logout() {}
});
new LoginContext("test").login();
}
在这个测试中我们使用了 @Mock 注释中三个相关调用次数的属性。第一个伪装定义了LoginContext(String) 构造方法必须至少在测试中被调用一次。第二个定义了 login() 方法必须被调用只有一次, 但是第三个声明了 logout() 可以被调用不仅仅一次。
同样的可以在一个伪装中定义调用的最小和最大次数,为了约束调用次数在给定的范围中。
伪装方法的初始化
当一个生产代码中的类在一个或者是多个静态初始化块中做了一些工作,我们可能需要进行伪装为了避免在执行测试的时候进行这些初始化。我们可以通过顶一个特殊的伪装方法来做这些,通过如下的代码。
@Test
public void mockingStaticInitializers()
{
new MockUp<ClassWithStaticInitializers>() {
@Mock
void $clinit()
{
// Do nothing here (usually).
}
};
ClassWithStaticInitializers.doSomething();
}
特别需要注意的是如果一个类的静态初始化代码被伪装了,这个类中的所有静态代码块和对静态属性的赋值都将不被执行 (除了那些在编译时就被解析的,而不会产生任何可执行字节码)。由于JVM只会尝试初始化一次类,恢复一个被伪装的静态初始化代码是无效的。因此,如果你伪装了一个没有被JVM进行初始化的类的静态初始化,原始类中的初始化代码将在测试中不会被执行。这将导致任何的静态属性将运行时被表达式所覆盖,而不是他们缺省的初始化的值。
使用调用上下文
一个伪装的方法可以选择性的声明附加的参数在 mockit.Invocation 类型中, 使用第一个参数提供出来。 针对每一个真实的被执行的相应伪装的方法或者是构造方法,一个调用对象将被自动传入伪装方法的执行中。
调用上下文对象提供了一些 "getters" 方法,可以在伪装的方法中使用。 其中一个是 getInvokedInstance() 方法, 将会返回在调用发生时的伪装实例(null 如果这个方法是静态的)。 其他的 getters 提供了一些调用(包括当前) 包括伪装的方法或构造方法,调用次数的约束(如果有的话) 在 @Mock 注释中被定义, 等等。 如下我们有一个测试的例子。
@Test
public void accessingTheMockedInstanceInMockMethods() throws Exception
{
final Subject testSubject = new Subject();
new MockUp<LoginContext>() {
@Mock
void $init(Invocation invocation, String name, Subject subject)
{
assertNotNull(name);
assertSame(testSubject, subject);
// Gets the invoked instance.
LoginContext loginContext = invocation.getInvokedInstance();
// Verifies that this is the first invocation.
assertEquals(1, invocation.getInvocationCount());
// Forces setting of private Subject field, since no setter is available.
Deencapsulation.setField(loginContext, subject);
}
@Mock(minInvocations = 1)
void login(Invocation invocation)
{
// Gets the invoked instance.
LoginContext loginContext = invocation.getInvokedInstance();
// getSubject() returns null until the subject is authenticated.
assertNull(loginContext.getSubject());
// Private field set to true when login succeeds.
Deencapsulation.setField(loginContext, "loginSucceeded", true);
}
@Mock
void logout(Invocation invocation)
{
// Gets the invoked instance.
LoginContext loginContext = invocation.getInvokedInstance();
assertSame(testSubject, loginContext.getSubject());
}
};
LoginContext theMockedInstance = new LoginContext("test", testSubject);
theMockedInstance.login();
theMockedInstance.logout();
}
执行真正的实现部分
一旦一个 @Mock 方法被执行了,任何其他的对伪装的方法的执行都将被重定向到对应的伪装方法中,导致它的实现是可重入的。 然后如果我们想执行被伪装方法的真实实现是,我们可以通过调用proceed() 方法在伪装方法接收到的第一个调用上下文参数对象上。
如下的测试示例演示了 LoginContext 对象通过正常的方式被建立 (在创建的时候不需要任何的伪装), 使用一个未被明确定义的配置。 (想看完整测试版本,请看 mockit.MockAnnotationsTest 类。)
@Test
public void proceedIntoRealImplementationsOfMockedMethods() throws Exception
{
// Create objects to be exercised by the code under test:
LoginContext loginContext = new LoginContext("test", null, null, configuration);
// Set up mocks:
ProceedingMockLoginContext mockInstance = new ProceedingMockLoginContext();
// Exercise the code under test:
assertNull(loginContext.getSubject());
loginContext.login();
assertNotNull(loginContext.getSubject());
assertTrue(mockInstance.loggedIn);
mockInstance.ignoreLogout = true;
loginContext.logout(); // first entry: do nothing
assertTrue(mockInstance.loggedIn);
mockInstance.ignoreLogout = false;
loginContext.logout(); // second entry: execute real implementation
assertFalse(mockInstance.loggedIn);
}
static final class ProceedingMockLoginContext extends MockUp<LoginContext>
{
boolean ignoreLogout;
boolean loggedIn;
@Mock
void login(Invocation inv) throws LoginException
{
try {
inv.proceed(); // executes the real code of the mocked method
loggedIn = true;
}
finally {
// This is here just to show that arbitrary actions can be taken inside
// the mock, before and/or after the real method gets executed.
LoginContext lc = inv.getInvokedInstance();
System.out.println("Login attempted for " + lc.getSubject());
}
}
@Mock
void logout(Invocation inv) throws LoginException
{
// We can choose to proceed into the real implementation or not.
if (!ignoreLogout) {
inv.proceed();
loggedIn = false;
}
}
}
在上面的示例中,所有的在被测试的 LoginContext 类中方法将被执行, 即使一些方法被伪装(login 和 logout)。 这个例子有点做作,实际上对真实实现的执行很少在测试中被使用,至少不会直接使用。
你可能会注意到使用 Invocation#proceed(...) 在一个伪装的方法中的效果和使用 advice (AOP 的概念) 对应于真实的方法。这是一个强大的能力特别是对于一些事情(考虑一下一个拦截器或装饰器)。
针对所有的可以使用的 mockit.Invocation 类中的方法描述, 可以查看它的 API 文档。
在测试时间重用伪装
大多数的测试可能只会使用专用的伪装类,针对每一个特殊的测试而构建。可能有事我们需要在多个测试线程或者是单个或者是真个测试套件中复用伪装对象。 我们将会看到不同的方式来构建伪装从而保证他们共享于测试组,和定义一些可重用的伪装类。
使用 before/after 方法
在一个给定的测试对象上,我们可以定义在每个测试方法运行之前后内容(即使测试抛出了一个错误或者是异常)。 使用 JUnit, 我们可以使用 @Before 和 @After 注释来完成一个或者多个随意的测试实例的实例方法。在TestNG,我们仍然可以同样的使用 @BeforeMethod 和 @AfterMethod 注释。
任何伪装类在一个测试方法中可以在"before" 方法中声明使用。 这个伪装类将会在这个测试类的所有测试方法中被共享。任何在before中的伪装同样在 "after" 方法中也有用的。
例如,我们想要伪装 LoginContext 类,使用在一系列的测试中,我们可以在测试类中使用如下的方法:
public class MyTestClass
{
@Before
public void setUpSharedMocks()
{
new MockUp<LoginContext>() {
// shared mocks here...
};
}
// test methods that will share the mocks set up above...
}
以上的例子是使用 JUnit, 但是在 TestNG中也是可以类似的相同使用的。
从基类中继承而来的类,可能选择性的定义了 "before" 和/或者 "after" 方法包含了对 Mockups API 的调用。
重用伪装类
命名的伪装类可以被设计成为了一个具体类(可选择为final ),从而可以在特定的测试用使用。当通过测试代码直接初始化的时候,这些伪装实例可以通过构造方法参数,属性,或者是非伪装函数的方法注入。此外,他们仍然可以被定义为基类,(可能是 abstract) 被具体的伪装类进行扩展在特定的测试类或方法中。
关于这章的测试例子来自于 JMockit 自己的测试套件。它们所使用的类,部分显示在这里:
public final class TextFile
{
// fields and constructors that accept a TextReader or DefaultTextReader object...
public List<String[]> parse()
{
skipHeader();
List<String[]> result = new ArrayList<String[]>();
while(true) {
String strLine = nextLine();
if (strLine == null) {
closeReader();
break;
}
String[] parsedLine = strLine.split(",");
result.add(parsedLine);
}
return result;
}
// private helper methods that call "skip(n)", "readLine()", and "close()"...
public interface TextReader
{
long skip(long n) throws IOException;
String readLine() throws IOException;
void close() throws IOException;
}
static final class DefaultTextReader implements TextReader
{
DefaultTextReader(String fileName) throws FileNotFoundException { ...mocked... }
public long skip(long n) throws IOException { ...mocked... }
public String readLine() throws IOException { ...mocked... }
public void close() throws IOException { ...mocked... }
}
}
以上类的一些测试方法如下所示。
public final class TextFileUsingMockUpsTest
{
// A reusable mock-up class to be applied in specific tests.
static final class MockTextReaderConstructor extends MockUp<DefaultTextReader>
{
@Mock(invocations = 1)
void $init(String fileName) { assertThat(fileName, equalTo("file")); }
}
@Test
public void parseTextFileUsingDefaultTextReader() throws Exception
{
new MockTextReaderConstructor();
new MockTextReaderForParse<DefaultTextReader>() {};
List<String[]> result = new TextFile("file", 200).parse();
// assert result from parsing
}
...
以上的测试使用了两个可重用的伪装类。第一个包装了单个构造TextFile.DefaultTextReader 嵌套类的伪装。 任何在 TextFile 类中的测试都将被调用这个构造方法。它通过简单的初始化在测试方法中。
第二个伪装类被使用在对DefaultTextReader 类的相同测试上。 接下来我们将看到, 它定义了对整个不同集合成员的通过 TextFile#parse() 方法的调用中伪装。
...
// A reusable base mock class to be extended in specific tests.
static class MockTextReaderForParse<T extends TextReader> extends MockUp<T>
{
static final String[] LINES = { "line1", "another,line", null};
int invocation;
@Mock(invocations = 1)
long skip(long n)
{
assertEquals(200, n);
return n;
}
@Mock(invocations = 3)
String readLine() throws IOException { return LINES[invocation++]; }
@Mock(invocations = 1)
void close() {}
}
...
以上伪装类,像 mockit.MockUp
...
@Test
public void parseTextFileUsingProvidedTextReader() throws Exception
{
TextReader textReader = new MockTextReaderForParse<TextReader>() {}.getMockInstance();
List<String[]> result = new TextFile(textReader, 200).parse();
// assert result from parsing
}
...
在这个例子中接口的实现是一个伪装的代理对象,通过调用MockUp
最后,我们将使用一个更有趣的例子,具体的伪装子类真正的覆盖了基伪装类的实现:
...
@Test
public void doesNotCloseTextReaderInCaseOfIOFailure() throws Exception
{
new MockTextReaderConstructor();
new MockTextReaderForParse<DefaultTextReader>() {
@Override @Mock
String readLine() throws IOException { throw new IOException(); }
@Override @Mock(invocations = 0)
void close() {}
};
TextFile textFile = new TextFile("file", 200);
try {
textFile.parse();
fail();
}
catch (RuntimeException e) {
assertTrue(e.getCause() instanceof IOException);
}
}
测试中强制一个 IOException 被抛出在首次调用readLine()的时候。 (这个异常将被在解析方法中包装成一个 RuntimeException异常。) 它还通过定义调用次数约束,保证 close() 方法没有被调用。 这里不仅仅展示了继承的伪装可以被覆盖,而且通过@Mock 注释所定义的元数据也可以被覆盖。
在测试类和测试套件级别使用伪装
正如我们所看到的,伪装类通常被使用在独立的测试专用。有时可能我们需要伪装类作为整个测试类内(指的是其中所有的测试方法)或者是整个测试套件中使用(指的是其中所有测试类)。它仍可能定义模拟类在一个整体的测试中通过外部的配置,通过定义一个JVM级别的系统属性或者是添加一个外部的 jmockit.properties 文件在运行时的 classpath下。
伪装程序化的应用在更广阔的范围
为了伪装一个类在整个测试类的范围内(所有测试), 我们简单的使用内置的 @BeforeClass 方法 (在 JUnit 或 TestNG)。 为了使用伪装在一个测试套件中,我们需要使用 TestNG中的 @BeforeSuite 方法, 或在 JUnit 套件类中。接下来的例子将会显示一个JUnit 4 测试套件的配置应用的伪装。
@RunWith(Suite.class)
@Suite.SuiteClasses({MyFirstTest.class, MySecondTest.class})
public final class TestSuite
{
@BeforeClass
public static void setupMocks()
{
new LoggingMocks();
new MockUp<SomeClass>() {
@Mock someMethod() {}
};
}
}
在这个例子中,我们使用 LoggingMocks 伪装类和一个内联的伪装类; 这些伪装将会作用到整个测试套件最后一个测试执行结束。
通过一个系统属性的外部应用
jmockit-mocks 系统属性支持以逗号分隔的全名称定义的伪装类列表。 如果是定义在 JVM启动的时候,所有的这些类(继承于MockUp
注意一个系统的属性可以通过标准的"-D" 命令行格式传递给JVM。 Ant/Maven/等 构建脚步都包含它们自己的方式进行系统属性的定义,可以检测它们的详细文档。
通过 jmockit.properties 文件的外部应用
伪装类同样的可以被定义为系统属性通过一个 独立的jmockit.properties 文件, 必须放在 classpath 的根下。如果有多个这个文件被包含在 classpath下 (无乱是在jars中或者是在纯路径下), 所有的类名列表将被添加到一起。这允许奖励可重用的伪装类打包在一个jar文件中,其中它包含了自己的 properties 文件; 当其被添加到 测试套件的 classpath 下,这些伪装类将会被自动在启动时候被使用。
为了方便,在 properties文件中项可以不需要写 "jmockit-" 的前缀。