本人近实践,个人比较喜欢采用JUit+Mock+Espresso,所以也展示了这三个。本来想分篇的,后还是压缩了一下一篇吧。 文中代码大部分是以前摘录的,比较零散也忘记出处了,也有自己写的一些,总体来说都是比较好的示例。 JUnit 导包 //如果只在Java环境下测试,只需以下且默认都有这个配置 testCompile 'junit:junit:4.12' //如果需要调用Android的组件则需要多加 androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support:support-annotations:'+supportLibVersion //且defaultConfig节点需要加上 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 使用 这是Java界用的广泛,很多框架都是基于这个框架的,用起来也比较简单 public int add(int one, int another) { return one + another; } 本来写个测试需要这样: public int test() { Calculator calculator = new Calculator(); int sum = calculator.add(1, 2); if(sum == 3) { System.out.println("add() works!") } else { System.out.println("add() does not works!") } } 现在有了这个框架,只需要这样 //会在每个测试方法前执行 @Before public void setup() { mCalculator = new Calculator(); } @Test public void testAdd() throws Exception { int sum = calculator.add(1, 2); Assert.assertEquals(3, sum); } //如果要验证抛出异常 @Test(expected = IllegalArgumentException.class) public void test() { mCalculator.divide(4, 0); } 验证方法都在Assert类中,看方法名能理解,不列举了 然后说一下常用的注解: setUp/@Before:在每个单元测试方法执行之前调用 tearDown/@After:在每个单元测试方法执行后调用 setUpBeforeClass/@BeforeClass:在每个单元测试类运行前调用 tearDownAfterClass/@AfterClass:在每个单元测试类运行完成后调用 Junit3中每个测试方法必须以test打头,Junit4中增加了注解,对方法名没有要求,@Test可以 如果想在测试类中暂时忽略某个方法,标注@Ignore 高阶 Parameterized 主要用于多参数的一次性测试,需要5步 用 @RunWith(Parameterized.class) 来注释 test 类 创建一个由 @Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合 创建一个公共的构造函数,它接受和一行测试数据相等同的东西 为每一列测试数据创建一个实例变量 用实例变量作为测试数据的来源来创建你的测试用例 示例: @RunWith(Parameterized.class) public class CalculatorAddParameterizedTest { @Parameters public static Iterable<Object[]> data() { return Arrays.asList(new Object[][]{ {0, 0, 0}, {0, -1, -1}, {2, 2, 4}, {8, 8, 16}, {16, 16, 32}, {32, 0, 32}, {64, 64, 128}}); } private final double mOperandOne; private final double mOperandTwo; private final double mExpectedResult; private Calculator mCalculator; public CalculatorAddParameterizedTest(double operandOne, double operandTwo, double expectedResult) { mOperandOne = operandOne; mOperandTwo = operandTwo; mExpectedResult = expectedResult; } @Before public void setUp() { mCalculator = new Calculator(); } @Test public void testAdd_TwoNumbers() { double resultAdd = mCalculator.add(mOperandOne, mOperandTwo); assertThat(resultAdd, is(equalTo(mExpectedResult))); } } Rule 类似于@Before、@After,是用来在每个测试方法的执行前后可以标准化的执行一些代码,一般我们直接用框架中现有的Rule可以了 具体可以看这篇: Junit Rule的使用 Mockito 依赖 androidTestCompile "org.mockito:mockito-core:$mockitoVersion" Mock作用 专注于单元测试,可以把想要测试类中没有实现的模块虚拟Mock出来,先给需要测试的模块用着 Mock出来的类是空壳,是一个继承与原类,方法都是hook的新类,每个方法都需要Stub,否则返回的都是默认值 Spy出来的类可以使用原来类的方法,但是也可以指定方法有hook处理 使用 创建Mock 使用Rule @Mock MyDatabase databaseMock; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); 标注RunWith @RunWith(MockitoJUnitRunner.class) public class Test{ @Mock MyDatabase databaseMock; } 手动mock MyClass test = mock(MyClass.class); 指定Stub 创建之后由于都是空的,所以要指定行为 若方法中的某一个参数使用了matcher,则所有的参数都必须使用matcher 第一种:Mockito.when(obj.methodCall()).thenReturn(result) 不能用于重复的Stub、返回void函数、Spy出来的类 @Test public void test1() { // define return value for method getUniqueId() when(test.getUniqueId()).thenReturn(43); // use mock in test.... assertEquals(test.getUniqueId(), 43); } @Test public void testMoreThanOneReturnValue() { Iterator<String> i= mock(Iterator.class); when(i.next()).thenReturn("Mockito").thenReturn("rocks"); String result= i.next()+" "+i.next(); //assert assertEquals("Mockito rocks", result); } @Test public void testReturnValueInDependentOnMethodParameter() { Comparable<Integer> c= mock(Comparable.class); when(c.compareTo(anyInt())).thenReturn(-1); //assert assertEquals(-1, c.compareTo(9)); } //return都可以用answer来代替 @Test public final void answerTest() { // with thenAnswer(): when(list.add(anyString())).thenAnswer(returnsFirstArg()); // with then() alias: when(list.add(anyString())).then(returnsFirstArg()); } 但是如果用这个来指定Spy则无效 @Test public void testLinkedListSpyWrong() { // Lets mock a LinkedList List<String> list = new LinkedList<>(); List<String> spy = spy(list); //无效且会抛出异常,因为调用了一次方法且此时list空 when(spy.get(0)).thenReturn("foo"); assertEquals("foo", spy.get(0)); } 第二种:Mockito.doReturn(result).when(obj).methodCall() 可以重复Stub,可以使用doAnswer来Stub方法 @Test public void testLinkedListSpyCorrect() { // Lets mock a LinkedList List<String> list = new LinkedList<>(); List<String> spy = spy(list); // You have to use doReturn() for stubbing doReturn("foo").when(spy).get(0); assertEquals("foo", spy.get(0)); } // with doAnswer(): doAnswer(returnsFirstArg()).when(list).add(anyString()); 该when(….).thenReturn(….)方法链可以用于抛出异常 Properties properties = mock(Properties.class); when(properties.get(”Anddroid”)).thenThrow(new IllegalArgumentException(...)); try { properties.get(”Anddroid”); fail(”Anddroid is misspelled”); } catch (IllegalArgumentException ex) { // good! } 并且可以指定Spy @Test public void testLinkedListSpyCorrect() { // Lets mock a LinkedList List<String> list = new LinkedList<>(); List<String> spy = spy(list); // You have to use doReturn() for stubbing doReturn("foo").when(spy).get(0); assertEquals("foo", spy.get(0)); } doAnswer //需要测试的代码 public void getTasks(@NonNull final LoadTasksCallback callback) {...} interface LoadTasksCallback { void onTasksLoaded(List<Task> tasks); void onDataNotAvailable(); } //stub doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] arg=invocation.getArguments();//获取参数 TasksDataSource.LoadTasksCallback callback = (TasksDataSource.LoadTasksCallback) arg[0];//0代表第一个参数 callback.onTasksLoaded(TASKS); return null; } }).when(mTasksRepository).getTasks(any(TasksDataSource.LoadTasksCallback.class)); 验证测试 主要验证是否方法调用和次数 //方法调用,且参数一定 Mockito.verify(mockUserManager, Mockito.times(2)).performLogin("xiaochuang", "xiaochuang password"); //如果是一次,可以简写 Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password"); //也可以限定次数 Mockito.verify(test, atLeastOnce()).someMethod("called at least once"); 高阶 ArgumentCaptor 可以捕获方法的参数,然后进行验证,也可以用来在有回调的方法上,避免doAnswer的复杂写法 需要导包hamcrest-library @Captor private ArgumentCaptor<List<String>> captor; @Test public final void shouldContainCertainListItem() { List<String> asList = Arrays.asList("someElement_test", "someElement"); final List<String> mockedList = mock(List.class); mockedList.addAll(asList); verify(mockedList).addAll(captor.capture()); final List<String> capturedArgument = captor.getValue(); assertThat(capturedArgument, hasItem("someElement")); } InOrder 可以指定验证的次序 // A. Single mock whose methods must be invoked in a particular order List singleMock = mock(List.class); //using a single mock singleMock.add("was added first"); singleMock.add("was added second"); //create an inOrder verifier for a single mock InOrder inOrder = inOrder(singleMock); //following will make sure that add is first called with "was added first, then with "was added second" inOrder.verify(singleMock).add("was added first"); inOrder.verify(singleMock).add("was added second"); // B. Multiple mocks that must be used in a particular order List firstMock = mock(List.class); List secondMock = mock(List.class); //using mocks firstMock.add("was called first"); secondMock.add("was called second"); //create inOrder object passing any mocks that need to be verified in order InOrder inOrder = inOrder(firstMock, secondMock); //following will make sure that firstMock was called before secondMock inOrder.verify(firstMock).add("was called first"); inOrder.verify(secondMock).add("was called second"); // Oh, and A + B can be mixed together at will @InjectMocks 主动构造有构造函数的Mock,且其参数也需要用注解来生成 public ArticleManager(User user, ArticleDatabase database) { super(); this.user = user; this.database = database; } @RunWith(MockitoJUnitRunner.class) public class ArticleManagerTest { @Mock ArticleDatabase database; @Mock User user; @InjectMocks private ArticleManager manager; } Espresso来UI测试依赖 // Android JUnit Runner androidTestCompile 'com.android.support.test:runner:0.5' // JUnit4 Rules androidTestCompile 'com.android.support.test:rules:0.5' //一些依赖关系可能出现的冲突。在这种情况下可以在 espresso-contrib 中 exclude他们 androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' } //并且需要在 defaultConfig 节点添加 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 并且每个测试类都需要加标注 @RunWith(AndroidJUnit4.class) public class Test {...} ActivityTestRule 在开始自动化测试之前都会定义一个ActivityTestRule 它用于在测试的时候launch待测试的activity 获得View 使用ViewMatcher.class里面的方法可以找到你想要的View,如你想找有Hello文字的View,你可以这样使用 onView(withText("Hello")); 相似的你也可以使用View的资源Id来找到该view onView(withId(R.id.hello)); 当有多个约束条件时,可以使用Matchers.class的allof()方法来组合,例子如下: onView(allOf(withText("Hello") ,withId(R.id.hello))); 对View执行一些操作 对View操作的代码大概是这样: onView(...).perform(); 在onView中找到这个View后,调用perform()方法进行操作,如点击该View: onView(withId(R.id.hello)).perform(click()); 也可以执行多个操作在一个perform中如 onView(withId(R.id.hello)).perform(click(),clearText()); 检查View(测试与验证) 使用check()方法来检查View是否符合我们的期望 onView(...).check(); 如检查一个View里面是否有文字Hello: onView(withId(R.id.hello)).check(matches(withText("Hello"))); 总之全部操作都在这个图里了
其他 1、判断这个View存不存在,返回一个boolen //Espresso不推荐在测试使用条件逻辑,找不到而不想直接报错只能try catch try { onView(withText("my button")).check(matches(isDisplayed())); //view is displayed logic } catch (NoMatchingViewException e) { //view not displayed logic } 2、模拟退出Activity的返回操作 Espresso.pressBack(); 3、有2个一样文字View,怎么只使用第一次找到的这个View public static <T> Matcher<T> firstFindView(final Matcher<T> matcher) { return new BaseMatcher<T>() { boolean isFirst = true; @Override public boolean matches(final Object item) { if (isFirst && matcher.matches(item)) { isFirst = false; return true; } return false; } @Override public void describeTo(final Description description) { description.appendText("should return first matching item"); } }; } //使用 onView(allOf(isDisplayed(),firstFindView(withText("Hello")))); 高阶 Intented与Intending 导包 androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2' 每次使用必须先Intents.init(),用完后必须调用Intents.release释放,或者使用的RuleActivity是IntentsTestRule,比如: //继承自ActivityTestRule,会在每个测试前和结束自动初始化和释放 @Rule public IntentsTestRule<ImageViewerActivity> mIntentsRule = new IntentsTestRule<>(ImageViewerActivity.class); 使用 Intending与Mockito.when相似,respondWith 相当于 thenReturn ActivityResult result = new ActivityResult(Activity.RESULT_OK, resultData); intending(hasComponent(hasShortClassName(".ContactsActivity"))).respondWith(result); Intenteded与Mockito.verify相似,验证某个Intent是否发出 intended(allOf( hasAction(Intent.ACTION_CALL), hasData(INTENT_DATA_PHONE_NUMBER), toPackage(PACKAGE_ANDROID_DIALER))); Espresso中提供了许多方法用于检测Intent的各个部分,下面是每个字段的对应关系 Intent.setData <–> hasData Intent.setAction <–> hasAction Intent.setFlag <–> hasFlag Intent.setComponent <–> hasComponent 一个标准的使用可以是这样 public void testLoginPass() { ActivityResult activityResult = new ActivityResult(Activity.RESULT_OK, new Intent()); Intents.init(); intending(expectedIntent).respondWith(activityResult); onView(withId(R.id.button_login)).perform(click()); intended(expectedIntent); Intents.release(); onView(withId(R.id.button_login)).check(matches(withText(R.string.pass_login))); } 其他使用 想要每个Activity启动的时候都收到某个Intent @RunWith(AndroidJUnit4.class) public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class) { @Override protected Intent getActivityIntent() { Context targetContext = InstrumentationRegistry.getInstrumentation() .getTargetContext(); Intent result = new Intent(targetContext, MainActivity.class); result.putExtra("Name", "Value"); return result; } }; } 可以去屏蔽掉其他包发的Intent的影响 @Before public void stubAllExternalIntents() { intending(not(isInternal())).respondWith(new ActivityResult(Activity.RESULT_OK, null)); } Idling Resource 一般用在异步里面,可以再测试的时候让其不会因为延迟而导致测试失败 导包 compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2' 为了便于测试,一般都会融合在实际回调中来控制当前是否处于空闲IDLE状态,可以在Activity中加入以下方法,然后再测试中获取 //要想在测试用例中使用源码中的数据可以使用VisibleForTesting这个注释符 @VisibleForTesting public IdlingResource getIdlingResource() { return mIdlingResource; } 使用 首先要实现一个IdlingResource一般app都用一个可以了,且重写三个函数: getName():必须返回代表idling resource的非空字符串,一般直接通过class.getName() isIdleNow():表示当前是否idle状态 registerIdleTransitionCallback(..): 用于注入回调 然后再有异步可能有延迟的地方使用IdlingResource,一般实现的时候使用Atom类来做并发的处理 后在每次测试的时候都需要在Espresso注册这个IdlingResource @Before public void registerIdlingResource() { mIdlingResource = mActivityRule.getActivity().getIdlingResource(); Espresso.registerIdlingResources(mIdlingResource); } @After public void unregisterIdlingResource() { if (mIdlingResource != null) { Espresso.unregisterIdlingResources(mIdlingResource); } } RecyclerView 当要测试RecyclerView的时候需要添加如下依赖: // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2' 使用也比较简单,基本和一般view一样只是多了些方法 onView(ViewMatchers.withId(R.id.recyclerView)) .perform(RecyclerViewActions.actionOnItemAtPosition(ITEM_BELOW_THE_FOLD, click())); onView(ViewMatchers.withId(R.id.recyclerView)) .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle())); onView(withId(R.id.recycleviews)).perform(RecyclerViewActions.actionOnHolderItem(new CustomViewHolderMatcher(hasDescendant(withText("Name"))), click())); 后说一下项目哪些需要测 一般的逻辑与功能性代码使用JUnit+Mock 所有的Model、Presenter/ViewModel、Api、Utils等类的public方法 Data类除了getter、setter、toString、hashCode等一般自动生成的方法之外的逻辑部分 UI测试 自定义View的功能:比如set data以后,text有没有显示出来等等,简单的交互,比如click事件,负责的交互一般不测,比如touch、滑动事件等等。 Activity的主要功能:比如view是不是存在、显示数据、错误信息、简单的点击事件等,组件之间intent交互。 比较复杂的用户交互比如onTouch,以及view的样式、位置等等,一般直接人工测试。