如何为带有日志记录器的方法编写模拟测试?

huangapple go评论69阅读模式
英文:

How to write mock test for method with logger?

问题

I have the following interface:

public interface SomeProcessor<T> {
    void process(Stream<T> someStream);

    default <R> R someMap(Function<T, R> function, T message, Logger log) {
        try {
            return function.apply(message);
        } catch (Exception exception) {
            log.error("Mapping error for message => {}", message, exception);
            return null;
        }
    }
}

And want to cover log part of the method by mock test but unfortunately it always fails. Now I have the following test:

@ExtendWith(MockitoExtension.class)
class SomeProcessorTest<T> implements SomeProcessor<T> {

    @Mock
    private Logger mockLogger;

    @Override
    public void process(Stream<T> someStream) {
    }

    @Override
    public <R> R someMap(Function<T, R> function, T message, Logger log) {
        try {
            return function.apply(message);
        } catch (Exception exception) {
            log.error("Mapping error for message => {}", message, exception);
            return null;
        }
    }

    @Test
    void testWithException() {

        SomeProcessor<String> processor = new SomeProcessor <String>() {
            @Override
            public void process(Stream<String> someStream) {
            }

            @Override
            public <R> R someMap(Function<String, R> function, String message, Logger log) {
                throw new RuntimeException("test exception");
            }
        };

        Function<String, Integer> function = Integer::parseInt;
        String message = "123";
        assertThrows(RuntimeException.class, () -> processor.someMap(function, message, mockLogger));

        ArgumentCaptor<String> someCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<Throwable> exceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
        verify(mockLogger).error(eq("Mapping error for message => {}"), eq(message), any(RuntimeException.class));
        assertEquals("Mapping error for message => {}", someCaptor.getValue());
        assertTrue(exceptionCaptor.getValue() instanceof RuntimeException);
    }
}

But I always see errors related to Wanted but not invoked: mockLogger.error...
and I cannot cover the following lines in my interface

catch (Exception exception) {
            log.error("Mapping error for message => {}", message, exception);
            return null;

What should I change in my mock test to cover these lines and not catch an error? Give me a piece of advice, please.

英文:

I have the following interface:

public interface SomeProcessor&lt;T&gt; {
void process(Stream&lt;T&gt; someStream);
default &lt;R&gt; R someMap(Function&lt;T, R&gt; function,
T message,
Logger log) {
try {
return function.apply(message);
} catch (Exception exception) {
log.error(&quot;Mapping error for message =&gt; {}&quot;, message, exception);
return null;
}
}
}

And want to cover log part of the method by mock test but unfortunately it always fails.
Now I have the following test:

@ExtendWith(MockitoExtension.class)
class SomeProcessorTest&lt;T&gt; implements SomeProcessor&lt;T&gt; {
@Mock
private Logger mockLogger;
@Override
public void process(Stream&lt;T&gt; someStream) {
}
@Override
public &lt;R&gt; R someMap(Function&lt;T, R&gt; function, T message, Logger log) {
try {
return function.apply(message);
} catch (Exception exception) {
log.error(&quot;Mapping error for message =&gt; {}&quot;, message, exception);
return null;
}
}
@Test
void testWithException() {
SomeProcessor&lt;String&gt; processor = new SomeProcessor &lt;String&gt;() {
@Override
public void process(Stream&lt;String&gt; someStream) {
}
@Override
public &lt;R&gt; R someMap(Function&lt;String, R&gt; function, String message, Logger log) {
throw new RuntimeException(&quot;test exception&quot;);
}
};
Function&lt;String, Integer&gt; function = Integer::parseInt;
String message = &quot;123&quot;;
assertThrows(RuntimeException.class, () -&gt; processor.someMap(function, message, mockLogger));
ArgumentCaptor&lt;String&gt; someCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor&lt;Throwable&gt; exceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(mockLogger).error(eq(&quot;Mapping error for message =&gt; {}&quot;), eq(message), any(RuntimeException.class));
assertEquals(&quot;Mapping error for message =&gt; {}&quot;, someCaptor.getValue());
assertTrue(exceptionCaptor.getValue() instanceof RuntimeException);
}}

But I always see errors related to Wanted but not invoked: mockLogger.error...
and I cannot cover the following lines in my interface

catch (Exception exception) {
log.error(&quot;Mapping error for message =&gt; {}&quot;, message, exception);
return null;

What should I change im my mock test to cover these lines and not catching an error?
Give me a piece of advice please.

答案1

得分: 1

你的代码中似乎有许多混乱的地方,一开始不太清楚其中的原理。mockLogger.error 没有被调用的问题源于你在测试方法内部的匿名类中覆盖了接口的 someMap 方法的默认实现:

@Override
public &lt;R&gt; R someMap(Function&lt;String, R&gt; function, String message, Logger log) {
    throw new RuntimeException(&quot;test exception&quot;);
}

因此,你试图测试的代码根本没有被调用。相反,你应该从匿名类中调用 super.someMap(...) 来调用实际的接口方法默认实现。

然而,当你这样做时,断言会失败,因为原始方法会捕获异常并返回 null,不会抛出异常。

移除抛出异常的断言仍然不会使测试通过,因为你正在测试的函数(Integer::parseInt)应该对你提供的 message&quot;123&quot;)正常工作,因此不会抛出异常,catch 块也不会执行。为了解决这个问题,你应该传递一个会导致解析失败的消息值,而不是一个数字(例如 &quot;abc&quot;)。

最后,我不明白为什么你的类要实现 SomeProcessor&lt;T&gt;,但这似乎是多余的,可以移除。

英文:

There seems to be a lot of mixed up things in your code and the rationale behind it is not clear at first glance. The problem with mockLogger.error not being called has its source in the fact that you're overriding the default implementation of the someMap method from the interface within the anonymous class inside your test method:

@Override
public &lt;R&gt; R someMap(Function&lt;String, R&gt; function, String message, Logger log) {
    throw new RuntimeException(&quot;test exception&quot;);
}

Because of that the code you're trying to test is not called at all. Instead, you should call super.someMap(...) from your anonymous class to invoke the actual interface method default implementation.

When you do that though, the assertion of RuntimeException being thrown will fail as the original method consumes the exception and returns null - no exception is thrown.

Removing the assertion of throwing an exception still would not make the test pass, because the function you're testing (Integer::parseInt) should work fine for the message you're providing (&quot;123&quot;), so the catch block will not be executed as no exception is thrown. To solve that you should pass a message value that would cause parsing failure - not a number (e.g. &quot;abc&quot;).

Finally - I don't understand why your class implements SomeProcessor&lt;T&gt;, but that seems to be redundant and may be removed.

huangapple
  • 本文由 发表于 2023年3月31日 23:29:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/75900266.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定