我有一个遗留类,其中包含一个new()
调用来实例化LoginContext
对象:
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
}
}
我想使用 Mockito 来模拟LoginContext
来测试这个类,因为它要求在实例化之前设置 JAAS 安全的东西,但我不确定如何在不更改 login()
方法来外部化LoginContext
的情况下做到这一点。
是否可以使用Mockito来模拟LoginContext
类?
对于未来,我会推荐 Eran Harel 的答案(重构将new
移动到可以嘲笑的工厂)。但是,如果您不想更改原始源代码,请使用非常方便和独特的功能:间谍。从文档中:
您可以创建真实物体的间谍。当您使用间谍时,将调用真正的方法(除非方法被存根)。
真正的间谍应该谨慎使用,例如在处理遗留代码时。
在您的情况下,您应该写:
TestedClass tc = spy(new TestedClass());
LoginContext lcMock = mock(LoginContext.class);
when(tc.login(anyString(), anyString())).thenReturn(lcMock);
我完全支持Eran Harel的解决方案,如果不可能,Tomasz Nurkiewicz的间谍建议非常好。但是,值得注意的是,在某些情况下两者都不适用。例如,如果login
方法有点"更强大":
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
return lc;
}
}
。这是旧代码,无法重构以将新LoginContext
的初始化提取到其自己的方法中并应用上述解决方案之一。
为了完整起见,值得一提的是第三种技术 - 在调用 new
运算符时使用 PowerMock 注入模拟对象。不过,PowerMock并不是灵丹妙药。它的工作原理是对其模拟的类应用字节码操作,如果测试的类采用字节码操作或反射,这可能是狡猾的做法,至少从我的个人经验来看,已知会给测试带来性能影响。再说一次,如果没有其他选择,唯一的选择必须是好的选择:
@RunWith(PowerMockRunner.class)
@PrepareForTest(TestedClass.class)
public class TestedClassTest {
@Test
public void testLogin() {
LoginContext lcMock = mock(LoginContext.class);
whenNew(LoginContext.class).withArguments(anyString(), anyString()).thenReturn(lcMock);
TestedClass tc = new TestedClass();
tc.login ("something", "something else");
// test the login's logic
}
}
编辑:
Mockito的现代版本提供了类似的功能,而不需要额外的PowerMock库和mockito-inline
依赖项(而不是mockito-core
依赖项):
public class TestedClassTest {
@Test
public void testLogin() {
try (MockedConstruction<LoginContext> mockedConstruction =
Mockito.mockConstruction(LoginContext.class)) {
TestedClass tc = new TestedClass();
tc.login("something", "something else");
// test the login's logic
}
}
}
您可以使用工厂来创建登录上下文。然后,您可以模拟工厂并返回您想要的测试。
public class TestedClass {
private final LoginContextFactory loginContextFactory;
public TestedClass(final LoginContextFactory loginContextFactory) {
this.loginContextFactory = loginContextFactory;
}
public LoginContext login(String user, String password) {
LoginContext lc = loginContextFactory.createLoginContext();
}
}
public interface LoginContextFactory {
public LoginContext createLoginContext();
}
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
}
}
-- 测试类:
@RunWith(PowerMockRunner.class)
@PrepareForTest(TestedClass.class)
public class TestedClassTest {
@Test
public void testLogin() {
LoginContext lcMock = mock(LoginContext.class);
whenNew(LoginContext.class).withArguments(anyString(), anyString()).thenReturn(lcMock);
//comment: this is giving mock object ( lcMock )
TestedClass tc = new TestedClass();
tc.login ("something", "something else"); /// testing this method.
// test the login's logic
}
}
当从testLogin()调用实际方法tc.login ("something", "something else");
时{- 此 LoginContext lc 设置为 null 并在调用 lc.doThis();
时抛出 NPE
我不知道,但是当你创建一个要测试的 TestedClass 实例时,做这样的事情怎么样:
TestedClass toTest = new TestedClass() {
public LoginContext login(String user, String password) {
//return mocked LoginContext
}
};
另一种选择是使用 Mockito 创建 TestedClass 的实例,并让模拟实例返回 LoginContext。
在可以修改所测试的类的情况下,当需要避免字节码操作、保持快速或最小化第三方依赖性时,以下是我对使用工厂提取new
操作的看法。
public class TestedClass {
interface PojoFactory { Pojo getNewPojo(); }
private final PojoFactory factory;
/** For use in production - nothing needs to change. */
public TestedClass() {
this.factory = new PojoFactory() {
@Override
public Pojo getNewPojo() {
return new Pojo();
}
};
}
/** For use in testing - provide a pojo factory. */
public TestedClass(PojoFactory factory) {
this.factory = factory;
}
public void doSomething() {
Pojo pojo = this.factory.getNewPojo();
anythingCouldHappen(pojo);
}
}
完成此操作后,您可以轻松测试、断言和验证对 Pojo 对象的调用:
public void testSomething() {
Pojo testPojo = new Pojo();
TestedClass target = new TestedClass(new TestedClass.PojoFactory() {
@Override
public Pojo getNewPojo() {
return testPojo;
}
});
target.doSomething();
assertThat(testPojo.isLifeStillBeautiful(), is(true));
}
如果TestClass
有多个构造函数,则必须使用额外的参数复制这些构造函数,则可能会出现此方法的唯一缺点。
出于坚实的原因,您可能希望将PojoFactory接口放到Pojo类上,以及生产工厂。
public class Pojo {
interface PojoFactory { Pojo getNewPojo(); }
public static final PojoFactory productionFactory =
new PojoFactory() {
@Override
public Pojo getNewPojo() {
return new Pojo();
}
};
我碰巧处于一种特殊情况下,我的用例类似于Mureinik的用例,但我最终使用了Tomasz Nurkiewicz的解决方案。
方法如下:
class TestedClass extends AARRGGHH {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
return lc;
}
}
现在,PowerMockRunner
无法初始化TestedClass
,因为它扩展了AARRGGHH
,这反过来又进行了更多的上下文初始化......你看这条路把我引向何方:我需要在几层上模拟。显然是巨大的气味。
我发现了一个很好的技巧,对TestedClass
的重构最少:我创建了一个小方法
LoginContext initLoginContext(String login, CallbackHandler callbackHandler) {
new lc = new LoginContext(login, callbackHandler);
}
此方法的范围必然是package
。
然后,测试存根将如下所示:
LoginContext lcMock = mock(LoginContext.class)
TestedClass testClass = spy(new TestedClass(withAllNeededArgs))
doReturn(lcMock)
.when(testClass)
.initLoginContext("login", callbackHandler)
伎俩就完成了...