在Mockito中使用new()调用的测试类

102

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

7个回答

90

对于未来,我建议参考 Eran Harel的回答 (重构将new移动到可以进行模拟的工厂中)。但如果您不想更改原始源代码,则使用非常方便且独特的功能:spy(间谍)。从 文档 中得知:

你可以创建真实对象的spy(间谍)。当你使用这个spy时,实际的方法会被调用(除非方法被存根)。

真正的spy应该小心谨慎地偶尔使用,例如处理旧代码的情况。

在您的情况下,应编写以下代码:

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

3
是的,在这里使用可以被模拟的工厂才是真正的答案。使用间谍是有争议的,而且在我看来应该避免使用。 - dhaag23
1
链接404:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html#13 - VedantK
2
重构将new操作移动到可模拟的工厂中。我不理解这个。你只是把同样的问题移到了一个新地方。 - Christopher Schneider
2
@ChristopherSchneider 是的,但在新的位置(通常是您的测试类)中,您可以编写工厂以返回一个对象,您可以在测试中实例化该对象,以便您可以在测试验证中使用该对象。 - Adam
1
工厂类只包含创建新实例的代码,没有涉及任何业务逻辑。因此,测试它是没有意义的。我们不需要测试 new() 是否确实返回一个新实例。 :) - shazwashere
显示剩余6条评论

69

我完全支持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
    }
}

编辑:
现代版本的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
        }
    }
}

11
通常情况下,大多数需要此功能的方法都是旧的/遗留代码,而且重构不可能或困难,因此你的陈述值得一赞! - Pankaj
7
这种方法的一个大问题是,你必须在@PrepareForTest中包含TestedClass,这会导致整个类在Sonar和Eclemma的测试覆盖率都为0。这可能是由于某个测试覆盖分析工具中的错误导致的。 - ACV
7
如果没有其他选择,唯一的选择必须是最好的选择。向夏洛克·福尔摩斯先生致敬。;) - Shane
我正在遵循这个方法,但它没有起作用。这个解决方法有什么改变吗? - Mehul Parmar
2
PowerMock目前不支持JUnit5,请参考:[https://github.com/powermock/powermock/issues/929] - Splines
显示剩余2条评论

36

你可以使用工厂来创建登录上下文。然后你可以对工厂进行模拟,并在测试中返回任何你想要的结果。

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
19
每个班级都需要一个工厂,这是必须的。对于POJOs而言,我认为创建这种工厂有点过度设计。是否有其他的测试框架可供选择? - alltej
1
新代码中,“new LoginContext(“login”,callbackHandler);”将在哪里? - Denly
1
为什么我们不将用户和密码传递给loginContextFactory.createLoginContext()方法?或者说我们不应该这样做吗?LoginContextFactoryImpl是什么样子的?对回答进行扩展会更好。 - testphreak
1
@Adam,如果每个不同的类都有一个新实例,你不需要一个新的工厂吗?例如,如果Class X调用new A()和new B()。那么Class X不是需要注入两个工厂吗? - Kosi
显示剩余3条评论

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()中 { - 当调用 lc.doThis(); 时,此LoginContext lc设置为null并抛出NPE错误。


4
据我所知没有,但当您创建一个要测试的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();
            }
        };

2

我碰巧处于一个特殊情况,我的用例类似于 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,而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)

技巧就在这里...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接