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

2022-09-04 19:51:43

我要测试的类:

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

答案 1

这归结为间谍的实施。根据文档,Spy是作为真实实例的副本创建的:

Mockito不会将调用委托给传递的真实实例,而是实际创建它的副本。因此,如果您保留真实实例并与之交互,请不要期望被监视者知道这些交互及其对真实实例状态的影响。推论是,当在间谍上调用未存根的方法但不在真实实例上调用时,您将不会在真实实例上看到任何影响。

它似乎是一个浅薄的副本。因此,就我的调试显示,副本和原始对象之间共享,但其对其封闭对象的引用是原始对象,而不是间谍。因此,真正的被召唤而不是被嘲笑的。CacheLoaderretrieveValue

我不确定解决这个问题的最佳方法是什么。对于这个特定的例子,一种方法是反转依赖关系(即将其传递到而不是在内部定义它),并嘲笑它而不是。CacheLoaderSubjectSubjectSubject


答案 2

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

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

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

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

    当然,根据您的需要,可以是一个完全独立的类,也可以接受整个类作为参数。valueRetrieverCacheLoader


推荐