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

ssg*_*gao 10 java mockito guava

我要测试的课程:

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";
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的测试用例

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"));
    }
}
Run Code Online (Sandbox Code Playgroud)

我有

org.junit.ComparisonFailure: 
Expected :good
Actual   :bad
Run Code Online (Sandbox Code Playgroud)

Jef*_*ica 9

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"));
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 进行手动覆盖.

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

    @Test
    public void test() {
      Subject subject = new Subject() {
        @Override public String getValue() { return "good"; }
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 重构.

    虽然您可以使用完整的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"));
    }
    
    Run Code Online (Sandbox Code Playgroud)

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


Mar*_*ers 7

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

Mockito 不会将调用委托给传递的实例,而是实际创建它的副本.因此,如果您保留真实实例并与之交互,请不要指望间谍知道这些交互及其对实际实例状态的影响.其必然结果是,当unstubbed方法被调用的间谍,但不是真正的实例,你不会看到真正的实例的任何影响.

这似乎是一个浅薄的副本.因此,就我的调试显示而言,CacheLoader副本和原始对象之间共享,但它对其封闭对象的引用是原始对象,而不是间谍.因此,真实retrieveValue被称为而不是被模拟的真实.

我不确定最好的解决方法是什么.这个具体示例的一种方法是反转CacheLoader依赖(即将其传递给内部Subject而不是在Subject内部定义),并模拟而不是Subject.