junit4 使用 mockito 打开 getResources().openRawResource 运行 nullpointer

junit4 opening a getResources().openRawResource using mockito runs nullpointer

android studio 2.1. preview 4

我正在创建一个 junit4 unit test 来测试打开原始目录中包含的文件。

但是,每次代码运行时,我都可以从 openRawResource.

获取一个空指针

这是我要测试的功能。这在实际设备上 运行 时有效。但是在单元测试中没有。

public String getNewsFeed(Context mContext) {
    InputStream inputStream = mContext.getResources().openRawResource(R.raw.news_list); // Null pointer

    Writer writer = new StringWriter();
    char[] buffer = new char[1024];

    try {
        InputStreamReader inputReader = new InputStreamReader(inputStream, "UTF-8");
        BufferedReader bufferReader = new BufferedReader(inputReader);
        int n;
        while ((n = bufferReader.read(buffer)) != -1) {
            writer.write(buffer, 0, n);
        }

        inputStream.close();
    }
    catch (IOException ioException) {
        return "";
    }

    return writer.toString();
}

这是我的测试用例

@RunWith(MockitoJUnitRunner.class)
public class NewsListPresenterTest {

    @Mock
    private Context mContext;
    @Mock
    private NewsListPresenter mNewsListPresenter;

    @org.junit.Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mNewsListPresenter = new NewsListPresenter(mContext);
    }

    @org.junit.Test
    public void testLoadNewsFeed() throws Exception {
        assertNotNull(mNewsListPresenter.getNewsFeed(mContext));
    }
}

非常感谢您的任何建议,

您必须告诉 mContext 模拟在调用 getResources() 时要做什么。

when(mContext.getResources()).thenReturn(some_mock_of_Resources);

如果您不指定任何内容,模拟将 return 为空。

对于您的示例,这意味着您可能还需要一个 Resources 模拟,并告诉它何时 do/return 在其上调用 openRawResource() 时。

您已经创建了一个模拟 Context 因此您必须对被测方法使用的方法进行存根。 在 getNewsFeed 中,您正在使用 Context::getResources,因此在调用 getNewsFeed 之前的测试中,您应该:

Resources resoures = ...
when(mContext.getResources()).thenReturn(resources);

Mockito documentation of stubbing 说得很清楚了。


在你的测试中,你也遇到了一些问题。我认为它们没有任何后果,但它们往往表明您正在发现 Mockito。

您写道:

@Mock
private NewsListPresenter mNewsListPresenter;

@Mock 注释该字段,告诉 mockito 创建 NewsListPresenter 的模拟,其中 class 正在测试。 (这不是什么大问题,因为您在 setUp 中创建了真实实例)。你可以看看 @InjectMocks (even if i am not a big fan of it).

另一点是你同时使用 @RunWith(MockitoJUnitRunner.class)MockitoAnnotations.initMocks(this),你可以避免最后一个,因为它是由跑步者调用的。此外,跑步者是更好的选择,因为它 validate framework usage.


我最后的观察是关于您的设计。可能它是一个 android 约束,但是你在演示者构造函数和 getNewsFeed 方法中都注入了上下文,这看起来很奇怪并且可能导致不一致的情况。我会选择构造函数注入(更面向对象的设计)。

另一种简化测试的方法是在实用程序中提取方法的主要部分 class 并测试不同的分支(空流、空流、有效流、IOException...)需要模拟上下文和资源:

class IOUtils {
    static String readFromStream(InputStream inputStream) {
        ....
    }
}

请注意,guava 提供了一个 similar utility

您混淆了 Android 中的两种类型的单元测试。这个很多人不是很清楚,我在这里解释一下。

为什么它可以在设备上运行:因为这是一个仪器测试。 什么是仪器化测试? 运行在真实device/emulator上的测试,测试代码在'src/androidTest'文件夹中。

为什么它不能用作本地 junit 测试:因为本地 junit 测试不是插桩测试。本地 Junit 测试 运行 在您计算机的 JVM 上,而不是在设备上。本地 Junit 测试不应 contain/use Android 代码,因为真正的 Android 代码在 device/emulator 上,而不是在您计算机的 JVM 上。

我想你想把它 运行 作为一个 junit 测试 运行 它更快,这就是为什么我想你把你的测试移到 'src/test' 文件夹和 context.getResources() 抛出 NullPointerException。

我想你有两个选择:

  1. 使用Robolectric到运行这个测试作为junit测试
  2. 重构您的方法,使其不依赖于 Android 的 类

对于选项 2,这就是我会做的。将方法的参数更改为 InputStream:

public String getNewsFeed(InputStream inputStream) {... use inputStream... }

现在您的方法看不到任何 Android 代码,您可以将其作为普通的 junit 方法进行测试。然后,您可以将伪造的 inputStream 传递给您的方法,如下所示:

@Test
public void testLoadNewsFeed() throws Exception {
    String fileContents = "line one";
    InputStream inputStream = new ByteArrayInputStream(fileContents.getBytes());
    assertNotNull(mNewsListPresenter.getNewsFeed(inputStream));
}

如果您仍想传递与您在应用中使用的相同的 inputStream(我不推荐),您仍然可以在测试中使用以下代码:

@Test
    public void testLoadNewsFeed() throws Exception {
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("raw/news_list");
        assertNotNull(mNewsListPresenter.getNewsFeed(inputStream));
    }

并且您必须将此行添加到您的 build.gradle 文件中:

sourceSets.test.resources.srcDirs += ["src/main"]

Android 单元测试可能会非常混乱。您可以在我写的 this 博客 post 中阅读这些概念