Android单元测试的整理

Xandy ·
更新时间:2024-11-15
· 679 次阅读

  本人近实践,个人比较喜欢采用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的样式、位置等等,一般直接人工测试。



android单元测试 测试 Android

需要 登录 后方可回复, 如果你还没有账号请 注册新账号