使用Mockito通过new()调用测试类


72

我有一个旧类,其中包含一个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类?

Answers:


68

对于将来,我会推荐伊兰·哈雷尔Eran Harel)的答案(重构new到可以嘲笑的工厂)。但是,如果您不想更改原始源代码,请使用非常方便且独特的功能:spies。从文档中

您可以创建真实对象的间谍。当您使用间谍时,将调用真实的方法(除非对某个方法进行了打桩)。

真正的间谍应该小心谨慎地使用,例如在处理遗留代码时。

在您的情况下,您应该写:

TestedClass tc = spy(new TestedClass());
LoginContext lcMock = mock(LoginContext.class);
when(tc.login(anyString(), anyString())).thenReturn(lcMock);

这正是我想要的,谢谢托马斯。doco底部有关doReturn的多余部分特别有用。
bwobbones 2011年

版本低于1.8的任何替代方法
pseudoCoder 2015年

是的,使用可嘲笑的工厂是这里的真正答案。使用间谍是有争议的,应避免使用IMO。
dhaag23 '16

1
重构将新工厂搬到工厂可以嘲笑我不明白这一点。您刚刚将相同的问题移到了新地方。
Christopher Schneider

1
@ChristopherSchneider是,但是在新位置(通常是测试类),您可以对工厂进行编码以返回一个对象,而该对象将在测试中实例化,因此可以在测试验证中使用该对象。
亚当

45

我全力支持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为其自身的方法并应用上述解决方案之一。

为了完整起见,值得一提的是第三种技术-在调用运算符时使用PowerMock注入模拟对象new。但是,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
    }
}

7
+1表示情况,通常情况就是这样,因为大多数需要此方法且无法进行重构/难以重构的方法都是旧的/旧版代码!
Pankaj

6
这种方法的最大问题是您必须在中加入TestedClass@PrepareForTest这样会自动使整个声纳和nar的全班覆盖率均为0。这可能是由于覆盖率分析工具之一的错误
ACV

3
if there are no other options, the only option must be the good option+1为福尔摩斯先生。;)
Shane

如果可能的话,我会投两次票:一次是为了有效回答,另一次是为了对您的回答进行有礼貌的介绍。顺便说一句,我也投票赞成Tomasz Nurkiewicz的回答
avi.elkharrat

29

您可以使用工厂来创建登录上下文。然后,您可以模拟工厂并返回您想要进行测试的任何内容。

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();
}

1
在这种情况下,我将需要创建LoginContextFactory的实现,该实现仅返回LoginContext的新对象?那是对的吗?
user1692342

8
这将要求我需要的每个班级都需要有一个工厂。对于POJO,我发现创建这样的工厂是过大的选择。测试框架还有其他选择吗?
alltej

@alltej您只需要在实例化新对象的类中使用工厂-当然不是每个pojo吗?
亚当(Adam)

哪里是“新的LoginContext(“ login”,callbackHandler);“ 在新代码中?
丹利(Denly)

@Denly通常会在您的设置代码中,例如在DI框架中,或者在您初始化应用程序的主目录中
Eran Harel

5
    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
        }
    }

tc.login ("something", "something else");从testLogin()调用实际方法时,{-此LoginContext lc设置为null并在调用时抛出NPElc.doThis();


3

我不知道,但是在创建要测试的TestedClass实例时如何做这样的事情:

TestedClass toTest = new TestedClass() {
    public LoginContext login(String user, String password) {
        //return mocked LoginContext
    }
};

另一个选择是使用Mockito创建TestedClass的实例,并让模拟的实例返回LoginContext。


2

在可以修改被测类并且需要避免字节码操纵,保持快速运行或最小化第三方依赖性的情况下,这是我使用工厂提取 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具有多个构造函数,而这些构造函数必须使用多余的参数进行复制,则这种方法的唯一缺点可能是出现。

由于SOLID原因,您可能希望将PojoFactory接口改为Pojo类,以及生产工厂。

public class Pojo {

    interface PojoFactory { Pojo getNewPojo(); }

    public static final PojoFactory productionFactory = 
        new PojoFactory() {
            @Override 
            public Pojo getNewPojo() {
                return new Pojo();
            }
        };

1

我碰巧处于一种特殊的情况下,我的用例类似于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无法初始化,这又进行了更多的上下文初始化...您将看到此路径将我引向何处:我将需要在多个层上进行模拟。明显有巨大的气味。TestedClassAARRGGHH

我发现了一个很好的hack,它的重构最少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)

技巧就完成了...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.