我使用了 doReturn,为什么 Mockito 仍然会在匿名 class 中调用真正的实现?

I used doReturn, why would Mockito still call real implementation inside anonymous class?

Class我要测试:

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

public class Subject {

    private CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
        @Override
        public String load(String key)
                throws Exception {
            return retrieveValue(key);
        }
    };

    private LoadingCache<String, String> cache = CacheBuilder.newBuilder()
            .build(cacheLoader);

    public String getValue(String key) {
        return cache.getUnchecked(key);
    }

    String retrieveValue(String key) {
        System.out.println("I should not be called!");
        return "bad";
    }
}

这是我的测试用例

import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doReturn;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class SubjectTest {

    String good = "good";

    @Spy
    @InjectMocks
    private Subject subject;

    @Test
    public void test() {
        doReturn(good).when(subject).retrieveValue(anyString());
        assertEquals(good, subject.getValue("a"));
    }
}

我得到了

org.junit.ComparisonFailure: 
Expected :good
Actual   :bad

这归结为间谍的实施。根据 docs,Spy 被创建为真实实例的 副本

Mockito does not delegate calls to the passed real instance, instead it actually creates a copy of it. So if you keep the real instance and interact with it, don't expect the spied to be aware of those interaction and their effect on real instance state. The corollary is that when an unstubbed method is called on the spy but not on the real instance, you won't see any effects on the real instance.

好像是的副本。结果,据我的调试显示,CacheLoader 在副本和原始对象之间共享,但它对其封闭对象的引用是原始对象,而不是间谍。因此真正的 retrieveValue 被调用而不是模拟的

我不确定解决这个问题的最佳方法是什么。这个特定示例的一种方法是反转 CacheLoader 依赖项(即将其传递到 Subject 而不是 Subject 内部定义它),并模拟它而不是 Subject

Mark Peters 在诊断和解释根本原因方面做得很好。我可以想到几个解决方法:

  • 将缓存(重新)初始化移动到单独的方法中。

    通过从间谍内部调用 new CacheLoader,匿名内部 class 被创建并引用间谍作为父实例。根据您的实际被测系统,您还可以从构造函数路径中创建缓存中获益,尤其是在涉及繁重的初始化或加载的情况下。

    public class Subject {
    
      public Subject() {
        initializeCache();
      }
    
      private LoadingCache<String, String> cache;
    
      @VisibleForTesting
      void initializeCache() {
        cache = CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
          @Override
          public String load(String key) throws Exception {
            return retrieveValue(key);
          }
        });
      }
    
      /* ... */
    }
    
    @Test
    public void test() {
      subject.initializeCache();
      doReturn(good).when(subject).retrieveValue(anyString());
      assertEquals(good, subject.getValue("a"));
    }
    
  • 进行手动覆盖。

    你的问题的根本原因是间谍实例与原始实例不同。通过覆盖测试中的单个实例,您可以在不处理不匹配的情况下更改行为。

    @Test
    public void test() {
      Subject subject = new Subject() {
        @Override public String getValue() { return "good"; }
      }
    }
    
  • 重构。

    尽管您可以使用完整的 DI,但您可以只向值函数添加一个测试接缝:

    public class Subject {
    
      private CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
          return valueRetriever.apply(key);
        }
      };
    
      private LoadingCache<String, String> cache =
          CacheBuilder.newBuilder().build(cacheLoader);
    
      Function<String, String> valueRetriever = new Function<String, String>() {
        @Override
        public String apply(String t) {
          System.out.println("I should not be called!");
          return "bad";
        }
      };
    
      public String getValue(String key) {
        return cache.getUnchecked(key);
      }
    }
    
    @Test
    public void test() {
      subject = new Subject();
      subject.valueRetriever = (x -> good);
      assertEquals(good, subject.getValue("a"));
    }
    

    当然,根据您的需要,valueRetriever 可以是一个完全独立的 class,或者您可以接受整个 CacheLoader 作为参数。

我有同样的问题,对于 Mockito 1.9.5 可能的解决方案可能是将方法可见性更改为 "protected"。老实说,我不知道它是如何工作的,但仍然没有错误。