mockito,当使用 when/thenReturn 设置模拟存根时出现异常

mockito, when setup mock stub using when/thenReturn it got exception

有 class 并且想要 mock/stub 一种方法

public class ToBeMocked {
    public List<HttpCookie> mergeList(@NonNull List<HttpCookie> cookies, HttpCookie oneCookie) {
        
        System.out.println("+++ mergeList(), cookies:"+cookies+", oneCookie:"+oneCookie);

        HashMap<String, HttpCookie> map = new HashMap();
        if (oneCookie != null) {
            map.put("A", oneCookie);
        }
        for (HttpCookie cookie : cookies) {  //<=== it crashed at this line
            map.put(cookie.getName(), cookie);
        }

        List<HttpCookie> list = new ArrayList<HttpCookie>();
        for (Map.Entry<String, HttpCookie> entry : map.entrySet()) {
           list.add(entry.getValue());
        }
        return list;
    }
}

测试;

@Test
public void test() {

        List<HttpCookie> aCookieList = new ArrayList<>();
        HttpCookie a1Cookie = new HttpCookie("A1", "a1");
        HttpCookie a2Cookie = new HttpCookie("A2", "a2");
        aCookieList.add(a1Cookie);
        aCookieList.add(a2Cookie); 
        
        HttpCookie bCookie = new HttpCookie("B", "b1");

        List<HttpCookie> fakeCookieList = new ArrayList<>();
        fakeCookieList.add(bCookie);
        fakeCookieList.addAll(aCookieList);

    ToBeMocked theSpy = spy(new ToBeMocked());

    System.out.println("+++ 111 test(), aCookieList:"+aCookieList+", bCookie:"+bCookie);
        
    //when(theSpy.mergeList(any(List.class), any(HttpCookie.class)))
    when(theSpy.mergeList(eq(aCookieList), any(HttpCookie.class))). //<== exception on this
           .thenReturn(fakeCookieList);

    System.out.println("+++ 222 test()");
    // test
    // it would call some other function which internally call the mergeList(aCookieList, bCookie), and expect to generate a list from the stubbed result to use, here just make it simple to be run able to show the problem
    List<HttpCookie> list = theSpy.mergeList(aCookieList, bCookie);
        
    // verify
    assertEquals(list.contains(bCookie), true);        
}

when(theSpy.mergeList(any(List.class), any(HttpCookie.class))).thenReturn(fakeCookieList); 上得到异常 NullPointerException

日志显示两行:

Called loadFromPath(/system/framework/framework-res.apk, true); mode=binary sdk=28
+++ 111 test(), aCookieList:[A1="a1", A2="a2"], bCookie:B="b1"
+++ mergeList(), cookies:null, oneCookie:null

java.lang.NullPointerException

显然 mergeList() 使用空参数执行,并在 for (HttpCookie cookie : cookies)

问题:

认为 when().thenReturn() 仅用于设置存根,这表示当使用任何参数(或特定参数)调用模拟的 mergeList() 时,它应该 return提供的列表。

when().thenReturn() 的参数不正确吗? 为什么它似乎在 when().thenReturn() 中执行 mergeList()?

来自 Mockito doc:

Sometimes it's impossible or impractical to use when(Object) for stubbing spies. Therefore when using spies please consider doReturn|Answer|Throw() family of methods for stubbing.

所以试试看:

  doReturn(fakeCookieList).when(theSpy)
            .mergeList(eq(aCookieList), any(HttpCookie.class));

但总的来说,你不太清楚你在用这样的测试测试什么。被测方法是 mergeList,同时你通过调用 doReturn 指定它的行为(return 值),最后只检查 returned 值。 (当然,如果真实代码完全表示的话)

@George Lvov 的解决方案有效。

Here 解释了 doReturn(...) 和 theReturn(...) 之间的区别,特别是在间谍中 doReturn(...) 不会进行真正的方法调用。 但是还没有找到记录行为差异的地方。

如果您使用间谍对象(用@Spy 注释)而不是模拟对象(用@Mock 注释),两种方法的行为会有所不同: when(...) thenReturn(...) 在返回指定值之前进行真正的方法调用。因此,如果被调用的方法抛出异常,您必须处理它/模拟它等。当然您仍然会得到结果(您在 thenReturn(...) 中定义的内容) doReturn(...) when(...) 根本不调用该方法。