杭州电子商务研究院

带测试的 MVP - 第 2 部分(测试 MVP 架构)

2018-07-23 08:00飞浪

测试 MVP 架构

测试驱动开发 ( TDD ) 是开发行业的事实标准。有人说“如果没有测试,它就是有问题的 :- Bruce Eckel”

在上一篇文章MVP 架构中,我们介绍了 MVP 架构的基础知识,现在本文将介绍 MVP 架构的测试。

使用 Espresso 和 JUnit 测试 MVP

本节分为两部分,使用插桩测试测试 MVP UI,使用 junit 测试测试业务逻辑。本节重点测试项目特定逻辑,而不是测试框架是否正常工作。

测试 MoviePresenter

为了测试演示者的业务逻辑,我们需要模拟 UI 和存储库组件,因此主要重点将放在测试演示者及其与其他组件(如视图和存储库实例)的交互上。要应用模拟,我们将使用 mockito 框架。

设置

在module-name/src/test/java/包下创建的单元测试类在本地 JVM 上运行。这些测试本质上是快速的,并且没有 Android API 交互。

创建测试

要创建本地单元测试或仪表测试,您可以按照以下步骤为特定类或方法创建新的测试:

  1. 打开包含要测试的代码的 Java 文件。
  2. 单击要测试的类或方法,然后按 Ctrl+Shift+T。
  3. 在出现的菜单中,单击创建新测试。
  4. 在创建测试对话框中,编辑任何字段并选择要生成的任何方法,然后单击确定。
  5. 在“选择目标目录”对话框中,点击与要创建的测试类型相对应的源集:androidTest(用于仪表测试)或 test(用于本地单元测试)。然后点击“确定”。

测试演示者功能

  • 接收用户事件
  • 启动后台线程来检索响应
  • 使用ArgumentCaptor捕获来自后台线程的响应
  • 验证收到响应时模拟视图的行为

一步步解释

  1. 使用@Mock注释模拟诸如view和movieRepo之类的实例来测试presenter的完整功能。
  2. 带有@Before的方法用于设置测试环境和依赖项,在本例中,设置模拟框架并使用模拟实例初始化演示者。
  3. 创建一个argumentCaptor实例来捕获回调接口引用并为模拟视图实例提供响应。
    • 创建并传递一个虚拟列表作为响应。
  4. 验证视图实例的方法调用是否成功。

MoviePresenterTest.java

      public class MoviePresenterTest {

    private static final Random RANDOM = new Random();

    @Mock // 1
    private MoviesListContract.View view;

    @Mock // 1
    private MovieRepo movieRepo;

    private MoviePresenter presenter;

    @Captor // 3
    private ArgumentCaptor<MoviesListContract.OnResponseCallback> argumentCaptor;

    @Before // 2
    public void setUp(){
        // A convenient way to inject mocks by using the @Mock annotation in Mockito.
        //  For mock injections , initMocks method needs to be called.
        MockitoAnnotations.initMocks(this);

        // get the presenter reference and bind with view for testing
        presenter = new MoviePresenter(view,movieRepo);


    }

    @Test
    public void loadMoviewList() throws Exception {
        presenter.loadMoviewList();
        verify(movieRepo,times(1)).getMovieList(argumentCaptor.capture());
        argumentCaptor.getValue().onResponse(getList());
        verify(view,times(1)).hideProgress();
        ArgumentCaptor<List> entityArgumentCaptor = ArgumentCaptor.forClass(List.class);
        verify(view).showMovieList(entityArgumentCaptor.capture());

        assertTrue(entityArgumentCaptor.getValue().size() == 10);
    }

    @Test
    public void OnError() throws Exception {
        presenter.loadMoviewList();
        verify(movieRepo,times(1)).getMovieList(argumentCaptor.capture());
        argumentCaptor.getValue().onError("Error");
        ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
        verify(view,times(1)).showLoadingError(argumentCaptor.capture()); // 3
        verify(view).showLoadingError(argumentCaptor.getValue()); // 4
    }

    private List<Movie> getList() {
        ArrayList<Movie> movies = new ArrayList<>();
        try {
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "IT", Utility.convertStringToDate("2017-10-8"), 7.6, 127, Movie.Type.HORROR));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Jumanji 2", Utility.convertStringToDate("2018-12-20"), 8.3, 111, Movie.Type.ACTION));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "The Dark Knight", Utility.convertStringToDate("2008-07-08"), 9.0, 152, Movie.Type.ACTION));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Inception", Utility.convertStringToDate("2010-07-16"), 8.8, 148, Movie.Type.ACTION));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "The Green Mile", Utility.convertStringToDate("1999-12-10"), 8.5, 189, Movie.Type.DRAMA));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Transcendence", Utility.convertStringToDate("2014-04-18"), 6.3, 120, Movie.Type.FICTION));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Saving Private Ryan", Utility.convertStringToDate("1998-07-24"), 8.6, 169, Movie.Type.DRAMA));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Whiplash", Utility.convertStringToDate("2015-02-20"), 8.5, 117, Movie.Type.DRAMA));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "The Raid", Utility.convertStringToDate("2011-04-13"), 7.6, 111, Movie.Type.ACTION));
            movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Burnt", Utility.convertStringToDate("2015-10-30"), 6.6, 111, Movie.Type.DRAMA));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return movies;
    }
}
    

测试 MovieListActivity

为了对 UI 执行自动化测试,我们将使用 Espresso 框架执行插桩测试。Espresso 不了解任何异步操作或后台线程。为了让 Espresso 了解您应用的长时间运行的操作,我们将使用CountingIdlingResource。

MoviePresent必须增加空闲资源,以使 Espresso 了解空闲时间,如下所示

      @Override
    public void loadMoviewList() {
        view.showProgress();
        mclient.getMovieList(callback);

        // required for espresso UI testing
        EspressoTestingIdlingResource.increment();
    }
    

并在收到响应时减少它。

使用实用程序类EspressoTestingIdlingResource以静态方式访问空闲资源

      public class EspressoTestingIdlingResource {
    private static final String RESOURCE = "GLOBAL";

    private static CountingIdlingResource mCountingIdlingResource =
            new CountingIdlingResource(RESOURCE);

    public static void increment() {
        mCountingIdlingResource.increment();
    }

    public static void decrement() {
        mCountingIdlingResource.decrement();
    }

    public static IdlingResource getIdlingResource() {
        return mCountingIdlingResource;
    }

}
    

检查存储库中演示者代码的完整实现

一步步解释

  1. 在@Before注解的方法中注册空闲资源,让Espresso感知异步延迟。
  2. 编写测试来验证正在使用的默认空消息的可见性。
  3. 执行用户操作以启动数据检索任务。
  4. 检查屏幕上列表的可见性。
  5. 单击特定列表项。
  6. 验证包含电影详细信息的 toast 消息。

电影列表测试.java

      @RunWith(AndroidJUnit4.class)
public class MovieListTest {


    @Rule
    public ActivityTestRule<MoviesListActivity> activityTestRule =
            new ActivityTestRule<>(MoviesListActivity.class);

    /**
     * Register IdlingResource resource to tell Espresso when your app is in an
     * idle state. This helps Espresso to synchronize test actions.
     */

    @Before // 1
    public void registerIdlingResource() {
        IdlingRegistry.getInstance().register(EspressoTestingIdlingResource.getIdlingResource());
    }

    /**
     * Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
     */
    @After
    public void unregisterIdlingResource() {
        IdlingRegistry.getInstance().unregister(EspressoTestingIdlingResource.getIdlingResource());
    }

    @Test // 2
    public void checkStaticView() {
        // verify default empty text message
        onView(withId(R.id.swipe_msg_tv)).check(matches(isDisplayed()));
        //                                |--------------------------|
        //|----------------------------| find a view | using swipe_msg_tv id
        //check visibility of view on screen <-------|
    }

    @Test
    public void checkRecyclerViewVisibility() {
        // 3. perform swipe
        onView(withId(R.id.swipe_msg_tv)).check(matches(isDisplayed()));
        onView(withId(R.id.swipe_container)).perform(swipeDown());

        // verify swipe is displayed
        onView(withId(R.id.swipe_msg_tv)).check(matches(not(isDisplayed())));

        // 4 verify recycler view is displayed
        onView(withId(R.id.movies_recycler_list)).check(matches(isDisplayed()));
        // 5 perform click on item at 0th position
        onView(withId(R.id.movies_recycler_list))
                .perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));

        // 6 verify the toast text
        MoviesListActivity activity = activityTestRule.getActivity();
        onView(withText("Title : 'IT' Rating : '7.6'")).
                inRoot(withDecorView(not(is(activity.getWindow().getDecorView())))).
                check(matches(isDisplayed()));
    }
}
    

奖金

Robolectric:Robolectric 是一个测试框架,可让您使用任何模拟器测试 Android 框架组件(活动、服务等)。它还能让您控制要测试的组件的生命周期。

<font style="vertical-a

曝光37524浏览1910