Использование mockito для проверки функции, использующей контекст

Android Studio 2.1.2

Я пытаюсь проверить getJsonFromResource, который вызывает loadNewsFeed. Я хочу иметь возможность протестировать 2 случая 1, где loadNewsFeed вернет пустую строку, а другую, где она вернет некоторую строку json.

Итак, я пытаюсь высмеять функцию loadNewsFeed, чтобы вернуть пустую строку. Однако, когда вызывается конкретный getJsonFromResource, он будет вызывать реальный loadNewsFeed и вызывать исключение нулевого указателя. Это то, что я пробовал в своих тестовых комментариях, объясняя, что я делаю:

@Test
public void shouldFailIfJSONStringIsEmpty() throws Exception {
    /* Mock Context class */
    Context context = mock(Context.class);
    /* initialize the concrete parseNewsFeed passing in the fake context */
    ParseNewsFeed parseNewsFeed = new ParseNewsFeed(context);
    /* Create a mock of the parseNewsFeed so a fake call to loadNewsFeed will return an empty string */
    ParseNewsFeed mockParseNewsFeed = mock(ParseNewsFeed.class);
    /* Mock the events that will be verified */
    ParseNewsFeedContract.Events<Status> mockEvents = mock(ParseNewsFeedContract.Events.class);

    /* Return an empty string when loadNewsFeed is called */
    when(mockParseNewsFeed.loadNewsFeed()).thenReturn("");

    /* Called the concrete getJsonFromResource */
    parseNewsFeed.getJsonFromResource(mockEvents);

    /* verify that onNewsFailure was called once and onNewsSuccess was never called */
    verify(mockEvents, times(1)).onNewsFailure(anyString());
    verify(mockEvents, never()).onNewsSuccess(any(Status.class));
}

Это класс, который я пытаюсь проверить.

public class ParseNewsFeed implements ParseNewsFeedContract {
    private Context mContext;

    public ParseNewsFeed(Context context) {
        if(context != null) {
            Timber.d("mContext != null");
            mContext = context;
        }
    }

    /**
     * Get the json from the local resource file and add to the cache to save loading each time
     * @return the json in string representation
     */
    @Override
    public void getJsonFromResource(Events<Status> events) {
        /* Get the json in string format */
        final String jsonString = loadNewsFeed();

        /* Check that is contains something */
        if(!jsonString.isEmpty()) {
            try {
                final Status status = new Gson().fromJson(jsonString, Status.class);
                if(status != null) {
                    Timber.d("url: %s", status.getResults().get(0).getMultimedia().get(0).getUrl());
                    events.onNewsSuccess(status);
                }
                else {
                    Timber.e("status == null");
                    events.onNewsFailure("Failed to get results from json");
                }
            }
            catch (JsonSyntaxException e) {
                Timber.e("Invalid JSON: %s", e.getMessage());
                events.onNewsFailure(e.getMessage());
            }
        }
    }

    /**
     * Opens and reads from the news_list and writes to a buffer
     * @return return the json representation as a string or a empty string for failure
     */
    public String loadNewsFeed() {
        InputStream inputStream = mContext.getResources().openRawResource(R.raw.news_list);
        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();
    }
}

Ответ 1

Похоже, вы хотите иметь объект ParseNewsFeed, где метод loadNewsFeed был заштрихован, но другие методы работают правильно. Самый простой способ получить это, вероятно, будет для создания шпиона, что-то вроде

ParseNewsFeed spyParseNewsFeed = Mockito.spy(new ParseNewsFeed(context));
Mockito.doReturn("").when(spyParseNewsFeed).loadNewsFeed();

Ответ 2

Прежде всего, причина, по которой ваш исходный код не работает, заключается в том, что между вашими двумя объектами parseNewsFeed и mockParseNewsFeed не существует никакой связи, поэтому у stubbing, который вы делаете для mockParseNewsFeed, нет эффект при вызове parseNewsFeed.getJsonFromResource(mockEvents). Использование spy, как предложил Дэвид Уоллес, будет работать, но если бы я был вами, я бы переписал код немного по-другому, чтобы сделать его еще проще для тестирования.

Одно из наблюдений заключается в том, что код в методе loadNewsFeed(), похоже, не имеет сильной связи с классом parseNewsFeed, поэтому я извлечу этот код в объект (например, NewsFeedLoader), а затем этот объект как зависимость класса parseNewsFeed. Затем вы можете легко обмануть этот Loader (return "" или любую строку, которая вы хотите, передав Context и, возможно, R.raw.news_list id). С помощью этого класса Loader вы даже можете unit test отдельно от parseNewsFeed и сможете улучшить Loader, тем не менее, вы хотите (например, лучший способ прочитать необработанный ресурс), не затрагивая parseNewsFeed класс.

Ответ 3

Используйте методы when() и then() вашего издевающегося контекста. Фактически это описано в примере официального учебника здесь.

@Mock
Context mMockContext;

@Test
public void readStringFromContext_LocalizedString() {
    // Given a mocked Context injected into the object under test...
    when(mMockContext.getString(R.string.hello_word))
            .thenReturn(FAKE_STRING);
    ClassUnderTest myObjectUnderTest = new ClassUnderTest(mMockContext);

    // ...when the string is returned from the object under test...
    String result = myObjectUnderTest.getHelloWorldString();

    // ...then the result should be the expected one.
    assertThat(result, is(FAKE_STRING));