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

发布于 2024-12-06 05:31:00 字数 1366 浏览 1 评论 0原文

我想编写一个 JUnit 测试来验证下面的代码是否使用 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 是一个接口。)

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

@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 的调用引发了一个异常,并显示以下消息:

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 的调用?

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

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.

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

梦年海沫深 2024-12-13 05:31:00

这个消息非常明显:你不能模拟不可见的和最终的类。简短的回答:创建一个命名类您的匿名类,然后测试这个类

答案很长,让我们来探究一下原因!

匿名类是final的

你实例化一个FilterFactory的匿名类,当编译器看到一个匿名类时,它会创建一个final包可见类。因此,匿名类不能通过标准方式(即通过 Mockito)进行模拟。

模拟匿名类:可能,但如果不是 HACKY,则很脆弱

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

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

模拟匿名类可能但很脆弱(我是认真的)
因此,假设匿名类是第十一个要声明的类,它将显示为

InputHelper$11.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 不使用编译类来检查代码报告。

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

因此,通过 Powermock 模拟匿名类是可能的,但很脆弱,永远不要在实际项目中这样做!

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

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

另外,我认为 JLS 没有明确指定编译器应如何命名匿名类。

您无需将间谍重新分配给静态字段

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 返回间谍,它不会更改 InputHelper.BZIP2_FACTORY 的值。因此,您需要通过反射实际设置该字段。您可以使用Powermock提供的Whitebox实用程序。

结论

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

我宁愿

编写以下代码:

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

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

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

@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 的优点,我什至会说他们正在设计遗留代码。

希望这会有所帮助!

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 !

An anonymous class is final

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.

Mocking anonymous class : possible but BRITTLE if not HACKY

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>

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

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!

So mocking anonymous classes through Powermock is possible but brittle, don't ever do that in a real project!

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>

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

You don't reassign the spy to the static field

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 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.

Conclusion

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

Alternative

I would rather write the following code:

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

And now the filter itself :

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

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.

Hope that helps !

不交电费瞎发啥光 2024-12-13 05:31:00

旧帖子,但您不需要创建命名类 - 请使用通配符,如本文中所述 通过whennew()进行的powermock模拟构造函数不适用于匿名类

@PrepareForTest(fullyQualifiedNames =“com.yourpackage.containing.anonclass.*”)

Old post, but you don't need to create a named class - use wildcards instead as mentioned in this post powermock mocking constructor via whennew() does not work with anonymous class

@PrepareForTest(fullyQualifiedNames = "com.yourpackage.containing.anonclass.*")

屋檐 2024-12-13 05:31:00

您需要使用 PowerMockito 运行程序运行测试,并且需要告诉框架哪个类应该具有自定义行为。在您的测试类上添加以下类注释:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ BufferedInputStream.class })

You need to run the test using the PowerMockito runner, and you need to tell the framework which class(es) should have custom behaviour. Add the following class annotations on your test class:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ BufferedInputStream.class })
爱*していゐ 2024-12-13 05:31:00

我刚刚遇到了同样的问题。因此,根据构造函数模拟文档,您需要准备该类,这将创建邪恶的阶级。在你的例子中,邪恶的类是 BufferedInputStream 和 CBZip2InputStream,它们的创建者是一个匿名类,不能在PrepareForTest注释中定义。所以我必须像你一样(嗯,刚刚看到你的评论),我将匿名类移动到命名类

I just came around the same problem. So according to the documentation of constructor mocking you need to prepare the class, which will create the evil class(es). In your case the evil classes are BufferedInputStream and CBZip2InputStream, and the creator of them is an anonymous class, which cannot be defined in PrepareForTest annotation. So I had to do the same as you did (hmm, just saw your comment), I moved the anonymous class to named class.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文