在匿名类中测试方法时,如何使用 Powermockito 模拟新对象的构造?

我想编写一个 JUnit 测试来验证下面的代码是否使用了 BufferedInputStream:

I woud like to write a JUnit test to verify that the code below uses a BufferedInputStream:

public static final FilterFactory BZIP2_FACTORY = new FilterFactory() {
    public InputStream makeFilter(InputStream in) {        
        // a lot of other code removed for clarity 
        BufferedInputStream buffer = new BufferedInputStream(in);
        return new CBZip2InputStream(buffer);
    }
};

(FilterFactory 是一个接口.)

(FilterFactory is an interface.)

到目前为止,我的测试如下所示:

My test thus far looks like this:

@Test
public void testBZIP2_FactoryUsesBufferedInputStream() throws Throwable {
    InputStream in = mock(InputStream.class);
    BufferedInputStream buffer = mock(BufferedInputStream.class);
    CBZip2InputStream expected = mock(CBZip2InputStream.class);

    PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
    whenNew(BufferedInputStream.class).withArguments(in).thenReturn(buffer);
    whenNew(CBZip2InputStream.class).withArguments(buffer).thenReturn(expected);
    InputStream observed = InputHelper.BZIP2_FACTORY.makeFilter(in);

    assertEquals(expected, observed);
}

对 PowerMockito.spy 的调用引发异常并显示以下消息:

The call to PowerMockito.spy raises an exception with this message:

org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class edu.gvsu.cis.kurmasz.io.InputHelper$1
Mockito can only mock visible & non-final classes.

我应该使用什么来代替 PowerMocktio.spy 来设置对 whenNew 的调用?

What should I be using instead of PowerMocktio.spy to set up the calls to whenNew?

推荐答案

信息很明显:你不能模拟非可见类和 final 类.简短回答:为您的匿名类创建一个命名类,然后测试这个类!

The message is pretty obvious: You can't mock non-visible and final classes. Short answer : Create a named class of your anonymous one, and test this class instead!

长答案,让我们来挖掘一下原因!

Long answer, let's dig why !

你实例化一个FilterFactory的匿名类,当编译器看到一个匿名类时,它会创建一个final和package visible类.所以匿名类不能通过标准方法模拟,即通过 Mockito.

You instantiate an anonymous class of FilterFactory, when the compiler sees an anonymous class, it creates a final and package visible class. So the anonymous class is not mockable through standard mean i.e. through Mockito.

好的,现在假设您希望能够通过 Powermock 模拟这个匿名类.当前编译器使用以下方案编译匿名类:

OK, now suppose you want to be able to mock this anonymous class through Powermock. Current compilers compile anonymous class with following scheme :

Declaring class + $ + <order of declaration starting with 1>

模拟匿名类可能但很脆弱(我是认真的)所以假设匿名类是第 11 个被声明的类,它会显示为

Mocking anonymous class possible but brittle (And I mean it) So supposing the anonymous class is the eleventh to be declared, it will appear as

InputHelper$11.class

所以你可以准备测试匿名类:

So you could potentially prepare for test the anonymous class:

@RunWith(PowerMockRunner.class)
@PrepareForTest({InputHelper$11.class})
public class InputHelperTest {
    @Test
    public void anonymous_class_mocking works() throws Throwable {
        PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
    }
}

此代码将编译,但最终会在您的 IDE 中报告为错误.IDE 可能不知道 InputHelper$11.class.不使用编译类的IntelliJ检查代码报告如此.

This code will compile, BUT will eventually be reported as an error with your IDE. The IDE probably doesn't know about InputHelper$11.class. IntelliJ who doesn't use compiled class to check the code report so.

匿名类命名实际上取决于声明的顺序这一事实也是一个问题,当有人之前添加另一个匿名类时,编号可能会改变.匿名类是为了保持匿名,如果编译器决定有一天使用字母甚至随机标识符怎么办!

Also the fact that the anonymous class naming actually depends on the order of the declaration is a problem, when someone adds another anonymous class before, the numbering could change. Anonymous classes are made to stay anonymous, what if the compiler guys decide one day to use letters or even random identifiers!

所以通过 Powermock 模拟匿名类是可能的,但很脆弱,千万不要在实际项目中这样做!

编辑说明: Eclipse 编译器有不同的编号方案,它总是使用 3 位数字:

EDITED NOTE : The Eclipse compiler has a different numbering scheme, it always uses a 3 digit number :

Declaring class + $ + <pad with 0> + <order of declaration starting with 1>

另外,我认为 JLS 并没有明确规定编译器应该如何命名匿名类.

Also I don't think the JLS clearly specify how the compilers should name anonymous classes.

PowerMockito.spy(InputHelper.BZIP2_FACTORY);  // This line fails
whenNew(BufferedInputStream.class).withArguments(in).thenReturn(buffer);
whenNew(CBZip2InputStream.class).withArguments(buffer).thenReturn(expected);
InputStream observed = InputHelper.BZIP2_FACTORY.makeFilter(in);

PowerMockito.spy 返回 spy,它不会改变 InputHelper.BZIP2_FACTORY 的值.所以你需要通过反射来实际设置这个字段.您可以使用 Powermock 提供的 Whitebox 实用程序.

PowerMockito.spy returns the spy, it doesn't change the value of InputHelper.BZIP2_FACTORY. So you would need to actually set via reflection this field. You can use the Whiteboxutility that Powermock provide.

只用模拟来测试匿名过滤器使用 BufferedInputStream 太麻烦了.

Too much trouble to just test with mocks that the anonymous filter uses a BufferedInputStream.

我宁愿写如下代码:

一个将使用命名类的输入助手,我不使用接口名称向用户说明此过滤器的意图是什么!

An input helper that will use the named class, I don't use the interface name to make clear to the user what is the intent of this filter!

public class InputHelper {
    public static final BufferedBZIP2FilterFactory BZIP2_FACTORY = new BufferedBZIP2FilterFactory();
}

现在是过滤器本身:

public class BufferedBZIP2FilterFactory {
    public InputStream makeFilter(InputStream in) {
        BufferedInputStream buffer = new BufferedInputStream(in);
        return new CBZip2InputStream(buffer);
    }
}

现在你可以编写这样的测试了:

Now you can write a test like this :

@RunWith(PowerMockRunner.class)
public class BufferedBZIP2FilterFactoryTest {

    @Test
    @PrepareForTest({BufferedBZIP2FilterFactory.class})
    public void wraps_InputStream_in_BufferedInputStream() throws Exception {
        whenNew(CBZip2InputStream.class).withArguments(isA(BufferedInputStream.class))
                .thenReturn(Mockito.mock(CBZip2InputStream.class));

        new BufferedBZIP2FilterFactory().makeFilter(anInputStream());

        verifyNew(CBZip2InputStream.class).withArguments(isA(BufferedInputStream.class));
    }

    private ByteArrayInputStream anInputStream() {
        return new ByteArrayInputStream(new byte[10]);
    }
}

但如果你强制 CBZip2InputStream 只接受 BufferedInputStream,最终可以避免这个测试场景的 powermock 东西.通常使用 Powermock 意味着设计有问题.在我看来,Powermock 非常适合遗留软件,但在设计新代码时会使开发人员失明;由于他们错过了 OOP 的优点,我什至会说他们正在设计遗留代码.

But could eventually avoid powermock stuff for this test scenario if you force the CBZip2InputStream to only accept BufferedInputStream. Usually using Powermock means something is wrong with the design. In my opinion Powermock is great for legacy softwares, but can blind developers when designing new code; as they are missing the point of OOP's good part, I would even say they are designing legacy code.

希望有帮助!

相关文章