本文通过分析一个完整的项目,来学习如果对一个完整的工程比较好的进行编写测试程序
项目地址 ,该项目采用的是 mvp 架构,(关于 mvp 的介绍可以看这里 ),mvp 对于测试的好处就是讲 view 逻辑和业务代码分离,我们可以很方便的对业务代码进行 local unit test 的测试。
项目结构
可以看到项目的整体结构比较清晰
- 项目代码
- Android 测试(Instrumentation test)
- Android 测试 相关 mock
- local unit test
- local unit test 相关 mock
local unit test mock
项目的 model 层采用的是 Repository 模式,在 mock 文件夹中,mock 测试中需要使用的数据源,并提供了注入的接口,这里采用的是手动注入,在后续依赖比较复杂的情况下可以使用 dagger 注入,减少大量冗余代码。
下面看下 mock 的数据源的实现
public class FakeTasksRemoteDataSource implements TasksDataSource { private static FakeTasksRemoteDataSource INSTANCE; private static final Map<String, Task> TASKS_SERVICE_DATA = newLink edHashMap<>(); // Prevent direct instantiation. private FakeTasksRemoteDataSource() {} public static FakeTasksRemoteDataSource getInstance() { if (INSTANCE == null) { INSTANCE = new FakeTasksRemoteDataSource(); } return INSTANCE; } @Override public void getTasks(@NonNull LoadTasksCallback callback) { callback.onTasksLoaded(Lists.newArrayList(TASKS_SERVICE_DATA.values())); } @Override public void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback) { Task task = TASKS_SERVICE_DATA.get(taskId); callback.onTaskLoaded(task); } @Override public void saveTask(@NonNull Task task) { TASKS_SERVICE_DATA.put(task.getId(), task); } @Override public void completeTask(@NonNull Task task) { Task completedTask = new Task(task.getTitle(), task.getDescript ion(), task.getId(), true); TASKS_SERVICE_DATA.put(task.getId(), completedTask); } @Override public void completeTask(@NonNull String taskId) { // Not required for the remote data source. } @Override public void activateTask(@NonNull Task task) { Task activeTask = new Task(task.getTitle(), task.getDescript ion(), task.getId()); TASKS_SERVICE_DATA.put(task.getId(), activeTask); } @Override public void activateTask(@NonNull String taskId) { // Not required for the remote data source. } @Override public void clearCompletedTasks() { Iterator<Map.Entry<String, Task>> it = TASKS_SERVICE_DATA.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Task> entry = it.next(); if (entry.getValue().isCompleted()) { it.remove(); } } } public void refreshTasks() { // Not required because the {@link TasksRepository} handles the logic of refreshing the // tasks from all the available data sources. } @Override public void deleteTask(@NonNull String taskId) { TASKS_SERVICE_DATA.remove(taskId); } @Override public void deleteAllTasks() { TASKS_SERVICE_DATA.clear(); } @VisibleForTesting public void addTasks(Task... tasks) { for (Task task : tasks) { TASKS_SERVICE_DATA.put(task.getId(), task); } } }
mock 的数据线 实现了 TasksDataSource
接口,将 mock 的数据都存储在了类的 Map<String, Task>
中
local unit test
model 层测试
使用了 mockito
来 mock 数据,
再看测试代码之前 先回顾下 TasksRepository
里面逻辑,方法比较多,挑几个来分析一下
/** * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it * uses the network data source. This is done to simplify the sample. * <p> * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if both data sources fail to * get the data. */ @Override public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) { checkNotNull(taskId); checkNotNull(callback); Task cachedTask = getTaskWithId(taskId); // Respond immediately with cache if available //如果缓存中有数据的话从缓存中获取数据 if (cachedTask != null) { callback.onTaskLoaded(cachedTask); return; } // Load from server/persisted if needed. //缓存没有数据则从服务端获取 // Is the task in the local data source? If not, query the network. mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() { @Override public void onTaskLoaded(Task task) { callback.onTaskLoaded(task); } @Override public void onDataNotAvailable() { //本地没有数据再从服务端获取 mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() { @Override public void onTaskLoaded(Task task) { callback.onTaskLoaded(task); } @Override public void onDataNotAvailable() { callback.onDataNotAvailable(); } }); } }); } @Override public void saveTask(@NonNull Task task) { checkNotNull(task); //将数据 储存的本地 和服务端 mTasksRemoteDataSource.saveTask(task); mTasksLocalDataSource.saveTask(task); // Do in memory cache update to keep the app UI up to date //将数据存储在缓存中 if (mCachedTasks == null) { mCachedTasks = newLink edHashMap<>(); } mCachedTasks.put(task.getId(), task); }
由此可以看出,数据的存储分为三层,缓存,本地存储,服务端存储。优先级递减
接着看 modle 的测试代码。
/** * Unit tests for the implementation of the in-memory repository with cache. */ public class TasksRepositoryTest { private final static String TASK_TITLE = "title"; private final static String TASK_TITLE2 = "title2"; private final static String TASK_TITLE3 = "title3"; private static List<Task> TASKS = Lists.newArrayList(new Task("Title1", "Descript ion1"), new Task("Title2", "Descript ion2")); private TasksRepository mTasksRepository; @Mock private TasksDataSource mTasksRemoteDataSource; @Mock private TasksDataSource mTasksLocalDataSource; @Mock private Context mContext; @Mock private TasksDataSource.GetTaskCallback mGetTaskCallback; @Mock private TasksDataSource.LoadTasksCallback mLoadTasksCallback; /** * {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to * perform further actions or assertions on them. */ @Captor private ArgumentCaptor<TasksDataSource.LoadTasksCallback> mTasksCallbackCaptor; /** * {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to * perform further actions or assertions on them. */ @Captor private ArgumentCaptor<TasksDataSource.GetTaskCallback> mTaskCallbackCaptor; @Before public void setupTasksRepository() { // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To // inject the mocks in the test the initMocks method needs to be called. MockitoAnnotations.initMocks(this); //首先在 @Before 中创建出Repository,可以看到在TasksRepository.getInstance 中的两个参数也都是 mock 出的 // Get a reference to the class under test mTasksRepository = TasksRepository.getInstance( mTasksRemoteDataSource, mTasksLocalDataSource); } @After public void destroyRepositoryInstance() { //销毁创建出的实例 TasksRepository.destroyInstance(); } @Test public void getTasks_repositoryCachesAfterFirstApiCall() { // Given a setup Captor to capture callbacks // When two calls are issued to the tasks repository // 使用 mLoadTasksCallback 来记录 callback twoTasksLoadCallsToRepository(mLoadTasksCallback); // Then tasks were only requested once from Service API //第一次调用 gettask 时本地是没有缓存的,所以 verify调用了mTasksRemoteDataSource 的 getTasks,并且其中的参数是 TasksDataSource.LoadTasksCallback.class类型,在第二次调用中,本地已经数据已经有了,所以mTasksRemoteDataSource 的方法只调用了一次 verify(mTasksRemoteDataSource).getTasks(any(TasksDataSource.LoadTasksCallback.class)); } @Test public void getTasks_requestsAllTasksFromLocalDataSource() { // When tasks are requested from the tasks repository mTasksRepository.getTasks(mLoadTasksCallback); // Then tasks are loaded from the local data source verify(mTasksLocalDataSource).getTasks(any(TasksDataSource.LoadTasksCallback.class)); } @Test public void saveTask_savesTaskToServiceAPI() { // Given a stub task with title and descript ion Task newTask = new Task(TASK_TITLE, "Some Task Descript ion"); // When a task is saved to the tasks repository mTasksRepository.saveTask(newTask); // Then the service API and persistent repository are called and the cache is updated //save 时 ,本地和服务端都调用了saveTask,并且本地的缓存 list 会增加 verify(mTasksRemoteDataSource).saveTask(newTask); verify(mTasksLocalDataSource).saveTask(newTask); assertThat(mTasksRepository.mCachedTasks.size(), is(1)); } @Test public void getTask_requestsSingleTaskFromLocalDataSource() { // When a task is requested from the tasks repository mTasksRepository.getTask(TASK_TITLE, mGetTaskCallback); // Then the task is loaded from the database verify(mTasksLocalDataSource).getTask(eq(TASK_TITLE), any( TasksDataSource.GetTaskCallback.class)); } @Test public void getTasksWithDirtyCache_tasksAreRetrievedFromRemote() { // When calling getTasks in the repository with dirty cache mTasksRepository.refreshTasks(); mTasksRepository.getTasks(mLoadTasksCallback); // And the remote data source has data available setTasksAvailable(mTasksRemoteDataSource, TASKS); // Verify the tasks from the remote data source are returned, not the local verify(mTasksLocalDataSource, never()).getTasks(mLoadTasksCallback); verify(mLoadTasksCallback).onTasksLoaded(TASKS); } @Test public void getTasksWithLocalDataSourceUnavailable_tasksAreRetrievedFromRemote() { // When calling getTasks in the repository mTasksRepository.getTasks(mLoadTasksCallback); // And the local data source has no data available setTasksNotAvailable(mTasksLocalDataSource); // And the remote data source has data available setTasksAvailable(mTasksRemoteDataSource, TASKS); // Verify the tasks from the local data source are returned verify(mLoadTasksCallback).onTasksLoaded(TASKS); } @Test public void getTasksWithBothDataSourcesUnavailable_firesOnDataUnavailable() { // When calling getTasks in the repository mTasksRepository.getTasks(mLoadTasksCallback); // And the local data source has no data available setTasksNotAvailable(mTasksLocalDataSource); // And the remote data source has no data available setTasksNotAvailable(mTasksRemoteDataSource); // Verify no data is returned verify(mLoadTasksCallback).onDataNotAvailable(); } /** * Convenience method that issues two calls to the tasks repository */ private void twoTasksLoadCallsToRepository(TasksDataSource.LoadTasksCallback callback) { // When tasks are requested from repository mTasksRepository.getTasks(callback); // First call to API // Use the Mockito Captor to capture the callback // 由于是第一次调用 mTasksRepository.getTasks ,所以会先调用 mTasksLocalDataSource,并且用mTasksCallbackCaptor.capture 记录 callback verify(mTasksLocalDataSource).getTasks(mTasksCallbackCaptor.capture()); // Local data source doesn't have data yet //由于第一次调用 get,所以本地数据是空的,会调用onDataNotAvailable 的回调 mTasksCallbackCaptor.getValue().onDataNotAvailable(); // Verify the remote data source is queried //本地为空,onDataNotAvailable回调中会调用,mTasksRemoteDataSource.getTasks verify(mTasksRemoteDataSource).getTasks(mTasksCallbackCaptor.capture()); // Trigger callback so tasks are cached //加载完 task 的回调 mTasksCallbackCaptor.getValue().onTasksLoaded(TASKS); //第二次调用 mTasksRepository.getTasks(callback); // Second call to API } private void setTasksNotAvailable(TasksDataSource dataSource) { verify(dataSource).getTasks(mTasksCallbackCaptor.capture()); mTasksCallbackCaptor.getValue().onDataNotAvailable(); } private void setTasksAvailable(TasksDataSource dataSource, List<Task> tasks) { verify(dataSource).getTasks(mTasksCallbackCaptor.capture()); mTasksCallbackCaptor.getValue().onTasksLoaded(tasks); } private void setTaskNotAvailable(TasksDataSource dataSource, String taskId) { verify(dataSource).getTask(eq(taskId), mTaskCallbackCaptor.capture()); mTaskCallbackCaptor.getValue().onDataNotAvailable(); } private void setTaskAvailable(TasksDataSource dataSource, Task task) { verify(dataSource).getTask(eq(task.getId()), mTaskCallbackCaptor.capture()); mTaskCallbackCaptor.getValue().onTaskLoaded(task); } }
P层测试
选择 AddEditTaskPresenter
的测试来分析,这事增加 task 的业务
/** * Unit tests for the implementation of {@link AddEditTaskPresenter}. */ public class AddEditTaskPresenterTest { @Mock private TasksRepository mTasksRepository; @Mock private AddEditTaskContract.View mAddEditTaskView; /** * {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to * perform further actions or assertions on them. */ @Captor private ArgumentCaptor<TasksDataSource.GetTaskCallback> mGetTaskCallbackCaptor; private AddEditTaskPresenter mAddEditTaskPresenter; @Before public void setupMocksAndView() { // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To // inject the mocks in the test the initMocks method needs to be called. MockitoAnnotations.initMocks(this); // The presenter wont't update the view unless it's active. when(mAddEditTaskView.isActive()).thenReturn(true); } @Test public void saveNewTaskToRepository_showsSuccessMessageUi() { // Get a reference to the class under test //手动构造出AddEditTaskPresente r mAddEditTaskPresenter = new AddEditTaskPresenter("1", mTasksRepository, mAddEditTaskView); // When the presenter is asked to save a task mAddEditTaskPresenter.saveTask("New Task Title", "Some Task Descript ion"); // Then a task is saved in the repository and the view updated //确认调用了 mTasksRepository 的 saveTask 方法 verify(mTasksRepository).saveTask(any(Task.class)); // saved to the model //确认 UI 对应的结构得到调用,具体效果不在这里的单元测试验证,在 UI 验证 verify(mAddEditTaskView).showTasksList(); // shown in the UI } @Test public void saveTask_emptyTaskShowsErrorUi() { // Get a reference to the class under test mAddEditTaskPresenter = new AddEditTaskPresenter(null, mTasksRepository, mAddEditTaskView); // When the presenter is asked to save an empty task mAddEditTaskPresenter.saveTask("", ""); // Then an empty not error is shown in the UI verify(mAddEditTaskView).showEmptyTaskError(); } @Test public void saveExistingTaskToRepository_showsSuccessMessageUi() { // Get a reference to the class under test mAddEditTaskPresenter = new AddEditTaskPresenter("1", mTasksRepository, mAddEditTaskView); // When the presenter is asked to save an existing task mAddEditTaskPresenter.saveTask("New Task Title", "Some Task Descript ion"); // Then a task is saved in the repository and the view updated verify(mTasksRepository).saveTask(any(Task.class)); // saved to the model verify(mAddEditTaskView).showTasksList(); // shown in the UI } @Test public void populateTask_callsRepoAndUpdatesView() { Task testTask = new Task("TITLE", "DESCRIPT ION"); // Get a reference to the class under test mAddEditTaskPresenter = new AddEditTaskPresenter(testTask.getId(), mTasksRepository, mAddEditTaskView); // When the presenter is asked to populate an existing task mAddEditTaskPresenter.populateTask(); // Then the task repository is queried and the view updated verify(mTasksRepository).getTask(eq(testTask.getId()), mGetTaskCallbackCaptor.capture()); // Simulate callback mGetTaskCallbackCaptor.getValue().onTaskLoaded(testTask); verify(mAddEditTaskView).setTitle(testTask.getTitle()); verify(mAddEditTaskView).setDescript ion(testTask.getDescript ion()); } }
android mock test
/** * Tests for the statistics screen. */ @RunWith(AndroidJUnit4.class) @LargeTest public class StatisticsScreenTest { /** * {@link ActivityTestRule} is a JUnit {@link Rule @Rule} to launch your activity under test. * * <p> * Rules are interceptors which are executed for each test method and areimport ant building * blocks of Junit tests. */ @Rule public ActivityTestRule<StatisticsActivity> mStatisticsActivityTestRule = new ActivityTestRule<>(StatisticsActivity.class, true, false); /** * Setup your test fixture with a fake task id. The {@link TaskDetailActivity} is started with * a particular task id, which is then loaded from the service API. * * <p> * Note that this test runs hermetically and is fully isolated using a fake implementation of * the service API. This is a great way to make your tests more reliable and faster at the same * time, since they are isolated from any outside dependencies. */ @Before public void intentWithStubbedTaskId() { // Given some tasks TasksRepository.destroyInstance(); //做了两个 mock 数据,一个已完成 一个未完成 FakeTasksRemoteDataSource.getInstance().addTasks(new Task("Title1", "", false)); FakeTasksRemoteDataSource.getInstance().addTasks(new Task("Title2", "", true)); // Lazily start the Activity from the ActivityTestRule Intent startIntent = new Intent(); mStatisticsActivityTestRule.launchActivity(startIntent); } @Test public void Tasks_ShowsNonEmptyMessage() throws Exception { // Check that the active and completed tasks text is displayed //检测这两种 view 是不是都显示了 String expectedActiveTaskText = InstrumentationRegistry.getTargetContext() .getString(R.string.statistics_active_tasks); onView(withText(containsString(expectedActiveTaskText))).check(matches(isDisplayed())); String expectedCompletedTaskText = InstrumentationRegistry.getTargetContext() .getString(R.string.statistics_completed_tasks); onView(withText(containsString(expectedCompletedTaskText))).check(matches(isDisplayed())); } }
Android test
模拟人工点击,并检测响应的页面显示,选择增加 task 的例子来分析
/** * Tests for the tasks screen, the main screen which contains a list of all tasks. */ @RunWith(AndroidJUnit4.class) @LargeTest public class TasksScreenTest { private final static String TITLE1 = "TITLE1"; private final static String DESCRIPT ION = "DESCR"; private final static String TITLE2 = "TITLE2"; /** * {@link ActivityTestRule} is a JUnit {@link Rule @Rule} to launch your activity under test. * <p> * Rules are interceptors which are executed for each test method and areimport ant building * blocks of Junit tests. */ @Rule public ActivityTestRule<TasksActivity> mTasksActivityTestRule = new ActivityTestRule<TasksActivity>(TasksActivity.class) { /** * To avoid a long list of tasks and the need to scroll through the list to find a * task, we call {@link TasksDataSource#deleteAllTasks()} before each test. */ @Override protected void beforeActivityLaunched() { super.beforeActivityLaunched(); // Doing this in @Before generates a race condition. //先删除所有 task Injection.provideTasksRepository(InstrumentationRegistry.getTargetContext()) .deleteAllTasks(); } }; } //检测增加一个 task 到 list @Test public void addTaskToTasksList() throws Exception { createTask(TITLE1, DESCRIPT ION); // Verify task is displayed on screen onView(withItemText(TITLE1)).check(matches(isDisplayed())); } //增加数据的操作都是点击并模拟输入的,不是 mock 的数据 private void createTask(String title, String descript ion) { // Click on the add task button onView(withId(R.id.fab_add_task)).perform(click()); // Add task title and descript ion onView(withId(R.id.add_task_title)).perform(typeText(title), closeSoftKeyboard()); // Type new task title onView(withId(R.id.add_task_descript ion)).perform(typeText(descript ion), closeSoftKeyboard()); // Type new task descript ion and close the keyboard // Save the task onView(withId(R.id.fab_edit_task_done)).perform(click()); }