首頁 类库下载 java类库 Android單元測試與模擬測試詳解

Android單元測試與模擬測試詳解

Oct 31, 2016 pm 12:03 PM

測試與基本規範

為什麼需要測試?

為了穩定性,能夠明確的了解是否正確的完成開發。

更容易維護,能夠在修改程式碼後保證功能不會被破壞。

整合一些工具,規範開發規範,使得程式碼更加穩定( 如透過 phabricator differential 發送diff時提交需要執行的單元測試,在開發流程上就可以保證遠端程式碼的穩定性)。

2. 測什麼?

一般單元測試:

列出想要測試覆蓋的異常情況,進行驗證。

性能測試。

模擬測試: 根據需求,測試使用者真正在使用過程中,介面的回饋與顯示以及一些依賴系統架構的元件的應用測試。

3. 需要注意

考慮可讀性,對於方法名使用表達能力強的方法名,對於測試範式可以考慮使用一種規範, 如 RSpec-style。方法名稱可以採用一種格式,如: [測試的方法]_[測試的條件]_[符合預期的結果]。

不要使用邏輯流關鍵字(If/else、for、do/while、switch/case),在一個測試方法中,如果需要有這些,拆分到單獨的每個測試方法。

測試真正需要測試的內容,需要覆蓋的情況,一般情況只考慮驗證輸出(如某操作後,顯示什麼,值是什麼)。

考慮耗時,Android Studio預設會輸出耗時。

不需要考慮測試private的方法,將private方法當作黑盒內部組件,測試對其引用的public方法即可;不考慮測試瑣碎的程式碼,如getter或者setter。

每個單元測試方法,應沒有先後順序;盡可能的解耦對於不同的測試方法,不應該存在Test A與Test B存在時序性的情況。

4. 建立測試

選擇對應的類別

將遊標停留在類別名稱上

按下ALT + ENTER

在彈出的彈窗中選擇Create Test

模擬測試

control + shift + R (Android Studio 預設執行單元測試快速鍵)。

1. 本地單元測試

直接在開發機上面進行運行測試。

在沒有依賴或僅需要簡單的Android庫依賴的情況下,有限考慮使用該類單元測試。
(1)程式碼儲存 

如果是對應不同的flavor或是build type,直接在test後面加上對應後綴(如對應名為myFlavor的單元測試程式碼,應該放在src /testMyFlavor/java下面)。

src/test/java

(2)Google官方推薦引用

dependencies {
    // Required -- JUnit 4 framework,用于单元测试,google官方推荐
    testCompile 'junit:junit:4.12'
    // Optional -- Mockito framework,用于模拟架构,google官方推荐
    //  http://www.manongjc.com/article/1546.html
    testCompile 'org.mockito:mockito-core:1.10.19'
}
登入後複製

(3)JUnit

Annotation

或需要測試機

Annotation

Android單元測試與模擬測試詳解

或需要測試機。

主要用於測試: 單元(Android SDK層引用關係的相關的單元測試)、UI、應用元件整合測試(Service、Content Provider等)。

./gradlew connectedAndroidTest

(1)程式碼儲存:

src/androidTest/java

(2)Google

主要三點:

UI加載好後展示的資訊是否正確。

在使用者某個操作後UI資訊是否顯示正確。

展示正確的頁面供使用者操作。

(4)Espresso

谷歌官方提供用於UI交互測試

dependencies {
    androidTestCompile 'com.android.support:support-annotations:23.0.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    // Optional -- Hamcrest library
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
    // Optional -- UI testing with Espresso
    //  http://www.manongjc.com/article/1546.html
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    // Optional -- UI testing with UI Automator
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
}
登入後複製

啟動一個打開Activity的Intent

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

// 对于Id为R.id.my_view的View: 触发点击,检测是否显示
onView(withId(R.id.my_view)).perform(click())               
                            .check(matches(isDisplayed()));
// 对于文本打头是"ABC"的View: 检测是否没有Enable
onView(withText(startsWith("ABC"))).check(matches(not(isEnabled()));
// 按返回键
pressBack();
// 对于Id为R.id.button的View: 检测内容是否是"Start new activity"
// http://www.manongjc.com/article/1537.html
onView(withId(R.id.button)).check(matches(withText(("Start new activity"))));
// 对于Id为R.id.viewId的View: 检测内容是否不包含"YYZZ"
onView(withId(R.id.viewId)).check(matches(withText(not(containsString("YYZZ")))));
// 对于Id为R.id.inputField的View: 输入"NewText",然后关闭软键盘
onView(withId(R.id.inputField)).perform(typeText("NewText"), closeSoftKeyboard());
// 对于Id为R.id.inputField的View: 清除内容
onView(withId(R.id.inputField)).perform(clearText());
登入後複製

(5)異步交互

建議關閉設備中”設置-> ,因為這些動畫可能會是的Espresso在偵測非同步任務的時候產生混淆: 視窗動畫縮放(Window animation scale)、過渡動畫縮放(Transition animation scale)、動畫程式時長縮放(Animator duration scale)。

針對AsyncTask,在測試的時候,如觸發點擊事件以後拋了一個AsyncTask任務,在測試的時候直接onView(withId(R.id.update)).perform(click()),然後直接進行檢測,此時的偵測就是在AsyncTask#onPostExecute之後。

@RunWith(AndroidJUnit4.class)
public class SecondActivityTest {
    @Rule
    public ActivityTestRule<SecondActivity> rule =
            new ActivityTestRule(SecondActivity.class, true,
                                  // 这个参数为false,不让SecondActivity自动启动
                                  // 如果为true,将会在所有@Before之前启动,在最后一个@After之后关闭
                                  false);
    @Test
    public void demonstrateIntentPrep() {
        Intent intent = new Intent();
        intent.putExtra("EXTRA", "Test");
        // 启动SecondActivity并传入intent
        rule.launchActivity(intent);
        // 对于Id为R.id.display的View: 检测内容是否是"Text"
        // http://www.manongjc.com/article/1532.html
        onView(withId(R.id.display)).check(matches(withText("Test")));
    }
}
登入後複製

(6)自訂匹配器

// 定义
public static Matcher<View> withItemHint(String itemHintText) {
  checkArgument(!(itemHintText.equals(null)));
  return withItemHint(is(itemHintText));
}

public static Matcher<View> withItemHint(final Matcher<String> matcherText) {
  checkNotNull(matcherText);
  return new BoundedMatcher<View, EditText>(EditText.class) {

    @Override
    public void describeTo(Description description) {
      description.appendText("with item hint: " + matcherText);
    }

    @Override
    protected boolean matchesSafely(EditText editTextField) {
      // 取出hint,然后比对下是否相同
      // http://www.manongjc.com/article/1524.html
      return matcherText.matches(editTextField.getHint().toString());
    }
  };
}

// 使用
onView(withItemHint("test")).check(matches(isDisplayed()));
登入後複製

拓展工具

1. AssertJ Android

square/assertj-android
极大的提高可读性。

import static org.assertj.core.api.Assertions.*;

// 断言: view是GONE的
assertThat(view).isGone();

MyClass test = new MyClass("Frodo");
MyClass test1 = new MyClass("Sauron");
MyClass test2 = new MyClass("Jacks");

List<MyClass> testList = new ArrayList<>();
testList.add(test);
testList.add(test1);

// 断言: test.getName()等于"Frodo"
assertThat(test.getName()).isEqualTo("Frodo");
// 断言: test不等于test1并且在testList中
// http://www.manongjc.com/article/1519.html
assertThat(test).isNotEqualTo(test1)
                 .isIn(testList);
// 断言: test.getName()的字符串,是由"Fro"打头,以"do"结尾,忽略大小写会等于"frodo"
assertThat(test.getName()).startsWith("Fro")
                            .endsWith("do")
                            .isEqualToIgnoringCase("frodo");
// 断言: testList有2个数据,包含test,test1,不包含test2
assertThat(list).hasSize(2)
                .contains(test, test1)
                .doesNotContain(test2);

// 断言: 提取testList队列中所有数据中的成员变量名为name的变量,并且包含name为"Frodo"与"Sauron"
//      并且不包含name为"Jacks"
assertThat(testList).extracting("name")
                    .contains("Frodo", "Sauron")
                    .doesNotContain("Jacks");
登入後複製

2. Hamcrest

JavaHamcrest
通过已有的通配方法,快速的对代码条件进行测试
org.hamcrest:hamcrest-junit:(version)

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.equalTo;

// 断言: a等于b
assertThat(a, equalTo(b));
assertThat(a, is(equalTo(b)));
assertThat(a, is(b));
// 断言: a不等于b
assertThat(actual, is(not(equalTo(b))));

List<Integer> list = Arrays.asList(5, 2, 4);
// 断言: list有3个数据
assertThat(list, hasSize(3));
// 断言: list中有5,2,4,并且顺序也一致
assertThat(list, contains(5, 2, 4));
// 断言: list中包含5,2,4
assertThat(list, containsInAnyOrder(2, 4, 5));
// 断言: list中的每一个数据都大于1
// http://www.manongjc.com/article/1507.html
assertThat(list, everyItem(greaterThan(1)));
// 断言: fellowship中包含有成员变量"race",并且其值不是ORC
assertThat(fellowship, everyItem(hasProperty("race", is(not((ORC))))));
// 断言: object1中与object2相同的成员变量都是相同的值
assertThat(object1, samePropertyValuesAs(object2));

Integer[] ints = new Integer[] { 7, 5, 12, 16 };
// 断言: 数组中包含7,5,12,16
assertThat(ints, arrayContaining(7, 5, 12, 16));
登入後複製

(1)几个主要的匹配器:

Android單元測試與模擬測試詳解

(2)自定义匹配器

// 自定义
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;

public class RegexMatcher extends TypeSafeMatcher<String> {
    private final String regex;

    public RegexMatcher(final String regex) { this.regex = regex; }
    @Override
    public void describeTo(final Description description) { description.appendText("matches regular expression=`" + regex + "`"); }

    @Override
    public boolean matchesSafely(final String string) { return string.matches(regex); }

    // 上层调用的入口
    public static RegexMatcher matchesRegex(final String regex) {
        return new RegexMatcher(regex);
    }
}

// 使用
String s = "aaabbbaaa";
assertThat(s, RegexMatcher.matchesRegex("a*b*a"));
登入後複製

3. Mockito

Mockito
Mock对象,控制其返回值,监控其方法的调用。
org.mockito:mockito-all:(version)

// import如相关类
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

// 创建一个Mock的对象
 MyClass test = mock(MyClass.class);

// 当调用test.getUniqueId()的时候返回43
when(test.getUniqueId()).thenReturn(43);
// 当调用test.compareTo()传入任意的Int值都返回43
when(test.compareTo(anyInt())).thenReturn(43);
// 当调用test.compareTo()传入的是Target.class类型对象时返回43
when(test.compareTo(isA(Target.class))).thenReturn(43);
// 当调用test.close()的时候,抛IOException异常
doThrow(new IOException()).when(test).close();
// 当调用test.execute()的时候,什么都不做
doNothing().when(test).execute();

// 验证是否调用了两次test.getUniqueId()
// http://www.manongjc.com/article/1503.html
verify(test, times(2)).getUniqueId();
// 验证是否没有调用过test.getUniqueId()
verify(test, never()).getUniqueId();
// 验证是否至少调用过两次test.getUniqueId()
verify(test, atLeast(2)).getUniqueId();
// 验证是否最多调用过三次test.getUniqueId()
verify(test, atMost(3)).getUniqueId();
// 验证是否这样调用过:test.query("test string")
verify(test).query("test string");

// 通过Mockito.spy() 封装List对象并返回将其mock的spy对象
List list = new LinkedList();
List spy = spy(list);

// 指定spy.get(0)返回"foo"
doReturn("foo").when(spy).get(0);

assertEquals("foo", spy.get(0));
登入後複製

对访问方法时,传入参数进行快照

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import static org.junit.Assert.assertEquals;

@Captor
private ArgumentCaptor<Integer> captor;

@Test
public void testCapture(){
  MyClass test = mock(MyClass.class);

  test.compareTo(3, 4);
  verify(test).compareTo(captor.capture(), eq(4));

  assertEquals(3, (int)captor.getValue());

  // 需要特别注意,如果是可变数组(vargars)参数,如方法 test.doSomething(String... params)
  // 此时是使用ArgumentCaptor<String>,而非ArgumentCaptor<String[]>
  ArgumentCaptor<String> varArgs = ArgumentCaptor.forClass(String.class);
  test.doSomething("param-1", "param-2");
  verify(test).doSomething(varArgs.capture());

  // 这里直接使用getAllValues()而非getValue(),来获取可变数组参数的所有传入参数
  assertThat(varArgs.getAllValues()).contains("param-1", "param-2");
}
登入後複製

(1)对于静态的方法的Mock:

可以使用 PowerMock:

org.powermock:powermock-api-mockito:(version) & org.powermock:powermock-module-junit4:(version)(For PowerMockRunner.class)

@RunWith(PowerMockRunner.class)
@PrepareForTest({StaticClass1.class, StaticClass2.class})
public class MyTest {

  @Test
  public void testSomething() {
    // mock完静态类以后,默认所有的方法都不做任何事情
    mockStatic(StaticClass1.class);
    when(StaticClass1.getStaticMethod()).andReturn("anything");

    // 验证是否StaticClass1.getStaticMethod()这个方法被调用了一次
    verifyStatic(time(1));
    StaticClass1.getStaticMethod();

    when(StaticClass1.getStaticMethod()).andReturn("what ever");

    // 验证是否StaticClass2.getStaticMethod()这个方法被至少调用了一次
    verifyStatic(atLeastOnce());
    StaticClass2.getStaticMethod();

    // 通过任何参数创建File的实力,都直接返回fileInstance对象
    whenNew(File.class).withAnyArguments().thenReturn(fileInstance);
  }
}
登入後複製

或者是封装为非静态,然后用Mockito:

class FooWraper{  void someMethod() {
    Foo.someStaticMethod();
  }
}
登入後複製

4. Robolectric

Robolectric
让模拟测试直接在开发机上完成,而不需要在Android系统上。所有需要使用到系统架构库的,如(Handler、HandlerThread)都需要使用Robolectric,或者进行模拟测试。

主要是解决模拟测试中耗时的缺陷,模拟测试需要安装以及跑在Android系统上,也就是需要在Android虚拟机或者设备上面,所以十分的耗时。基本上每次来来回回都需要几分钟时间。针对这类问题,业界其实已经有了一个现成的解决方案: Pivotal实验室推出的Robolectric。通过使用Robolectrict模拟Android系统核心库的Shadow Classes的方式,我们可以像写本地测试一样写这类测试,并且直接运行在工作环境的JVM上,十分方便。

5. Robotium

RobotiumTech/robotium
(Integration Tests)模拟用户操作,事件流测试。

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyActivityTest{

@Test
  public void doSomethingTests(){
    // 获取Application对象
    Application application = RuntimeEnvironment.application;

    // 启动WelcomeActivity
    WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
    // 触发activity中Id为R.id.login的View的click事件
    // http://www.manongjc.com/article/1502.html
    activity.findViewById(R.id.login).performClick();

    Intent expectedIntent = new Intent(activity, LoginActivity.class);
    // 在activity之后,启动的Activity是否是LoginActivity
    assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent);
  }
}
登入後複製

通过模拟用户的操作的行为事件流进行测试,这类测试无法避免需要在虚拟机或者设备上面运行的。是一些用户操作流程与视觉显示强相关的很好的选择。

 

6. Test Butler

linkedin/test-butler
避免设备/模拟器系统或者环境的错误,导致测试的失败。

通常我们在进行UI测试的时候,会遇到由于模拟器或者设备的错误,如系统的crash、ANR、或是未预期的Wifi、CPU罢工,或者是锁屏,这些外再环境因素导致测试不过。Test-Butler引入就是避免这些环境因素导致UI测试不过。

该库被谷歌官方推荐过,并且收到谷歌工程师的Review。

 

拓展思路

1. Android Robots

Instrumentation Testing Robots – Jake Wharton

假如我们需要测试: 发送 $42 到 “foo@bar.com”,然后验证是否成功。

(1)通常的做法

Android單元測試與模擬測試詳解

Android單元測試與模擬測試詳解

(2)Robot思想

在写真正的UI测试的时候,只需要关注要测试什么,而不需要关注需要怎么测试,换句话说就是让测试逻辑与View或Presenter解耦,而与数据产生关系。

首先通过封装一个Robot去处理How的部分:

Android單元測試與模擬測試詳解

然后在写测试的时候,只关注需要测试什么:

Android單元測試與模擬測試詳解

终的思想原理

Android單元測試與模擬測試詳解

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)