Android 단위 테스트 및 시뮬레이션 테스트에 대한 자세한 설명

高洛峰
풀어 주다: 2016-10-31 12:03:29
원래의
1538명이 탐색했습니다.

테스트 및 기본 사양

테스트가 필요한 이유는 무엇인가요?

안정성을 위해 개발이 제대로 완료되었는지 명확하게 파악하는 것이 가능합니다.

유지 관리가 더 쉽고, 코드 수정 후에도 함수가 파괴되지 않도록 할 수 있습니다.

개발 사양을 표준화하고 코드를 더욱 안정적으로 만들기 위한 일부 도구를 통합합니다(예: 개발 과정에서 원격 코드의 안정성을 보장할 수 있는 파브리케이터 차등을 통해 diff를 실행할 때 실행해야 하는 단위 테스트를 제출하는 등). .

2. 무엇을 테스트해야 할까요?

일반 단위 테스트:

테스트할 예외를 나열하고 확인합니다.

성능 테스트.

시뮬레이션 테스트: 필요에 따라 사용자의 실제 사용 과정에서 인터페이스의 피드백 및 표시를 테스트하고 시스템 아키텍처에 의존하는 일부 구성 요소의 애플리케이션 테스트를 테스트합니다.

3. 주의할 점

가독성을 고려하고, 메서드 이름에 표현적인 메서드 이름을 사용하고, RSpec 스타일과 같은 테스트 패러다임에 대한 사양을 사용하는 것을 고려하세요. 메소드 이름은 [테스트 메소드]_[테스트 조건]_[예상 결과]와 같은 형식일 수 있습니다.

논리적 흐름 키워드(If/else, for, do/while, switch/case)를 테스트 메서드에 사용하지 마세요. 이를 별도의 테스트 메서드로 분할하세요.

정말 테스트해야 할 내용과 다루어야 할 상황을 테스트합니다. 일반적으로 검증 출력(예: 특정 작업 후 표시되는 내용 및 값)만 고려합니다.

Android Studio는 시간 소모를 고려하여 기본적으로 시간 소모를 출력합니다.

비공개 메소드 테스트를 고려할 필요가 없습니다. 비공개 메소드를 블랙박스 내부 구성요소로 취급하고 이들이 참조하는 공개 메소드를 테스트하지 마세요. getter나 setter와 같은 사소한 코드는 테스트하지 마세요.

각 단위 테스트 방법은 순서가 없어야 하며, 테스트 방법이 서로 다르기 때문에 테스트 A와 테스트 B가 타이밍을 맞추는 상황이 있어서는 안 됩니다.

4. 테스트 만들기

해당 클래스 선택

클래스 이름에 커서를 놓고

Alt + Enter 누르기

팝업 창에서 Create Test

Android Studio의 단위 테스트 및 시뮬레이션 테스트

control + Shift + R(Android Studio의 기본 실행 단위 테스트 단축키)를 선택합니다. ).

1. 로컬 단위 테스트

개발 머신에서 직접 테스트를 실행합니다.
이 유형의 단위 테스트는 종속성이 없거나 간단한 Android 라이브러리 종속성만 필요한 경우에만 사용하는 것이 좋습니다.

./gradlew check

(1) 코드 저장

다른 플레이버나 빌드 유형에 해당하는 경우 테스트 직후 해당 접미사를 추가합니다(예: , 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

Android 단위 테스트 및 시뮬레이션 테스트에 대한 자세한 설명

2. 시뮬레이션 테스트

Android 기기 또는 가상 머신에서 실행해야 하는 테스트입니다.

주로 테스트에 사용됩니다: 단위(Android SDK 레이어 참조 관계와 관련된 단위 테스트), UI, 애플리케이션 구성요소 통합 테스트(서비스, 콘텐츠 제공자 등).

./gradlewconnectedAndroidTest

(1) 코드 저장:

src/androidTest/java

(2)Google 공식 추천 명언

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'
}
로그인 후 복사

(3) 공통 UI 테스트

는 Android 시스템 환경을 시뮬레이션해야 합니다.

세 가지 주요 사항:

UI가 로드된 후 표시되는 정보가 올바른지 여부.

사용자 조작 후 UI 정보가 올바르게 표시되는지 여부.

사용자가 조작할 수 있는 올바른 페이지를 표시합니다.

(4)Espresso

Google은 공식적으로 UI 상호작용 테스트를 위해 이를 제공합니다

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());
로그인 후 복사

액티비티를 여는 인텐트 시작

@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")));
    }
}
로그인 후 복사

( 5 )비동기 상호작용

기기의 "설정->개발자 옵션"에서 애니메이션을 끄는 것이 좋습니다. 이러한 애니메이션은 비동기 작업을 감지할 때 Espresso에 혼란을 줄 수 있기 때문입니다: 창 애니메이션 배율(창 애니메이션 배율) , 전환 애니메이션 스케일, 애니메이터 지속 시간 스케일.

AsyncTask의 경우 테스트 중 click 이벤트가 발생한 후 AsyncTask 작업이 발생하면 테스트 중에 직접 onView(withId(R.id.update)).perform(click()) 을 감지한 다음 Detect 이때 탐지는 AsyncTask#onPostExecute 이후입니다.

// 通过实现IdlingResource,block住当非空闲的时候,当空闲时进行检测,非空闲的这段时间处理异步事情
public class IntentServiceIdlingResource implements IdlingResource {
    ResourceCallback resourceCallback;
    private Context context;

    public IntentServiceIdlingResource(Context context) { this.context = context; }

    @Override public String getName() { return IntentServiceIdlingResource.class.getName(); }

    @Override public void registerIdleTransitionCallback( ResourceCallback resourceCallback) { this.resourceCallback = resourceCallback; }

    @Override public boolean isIdleNow() {
      // 是否是空闲
      // 如果IntentService 没有在运行,就说明异步任务结束,IntentService特质就是启动以后处理完Intent中的事务,理解关闭自己
      // http://www.manongjc.com/article/1531.html
        boolean idle = !isIntentServiceRunning();
        if (idle && resourceCallback != null) {
          // 回调告知异步任务结束
            resourceCallback.onTransitionToIdle();
        }
        return idle;
    }

    private boolean isIntentServiceRunning() {
        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        // Get all running services
        List<ActivityManager.RunningServiceInfo> runningServices = manager.getRunningServices(Integer.MAX_VALUE);
        // check if our is running
        for (ActivityManager.RunningServiceInfo info : runningServices) {
            if (MyIntentService.class.getName().equals(info.service.getClassName())) {
                return true;
            }
        }
        return false;
    }
}

// 使用IntentServiceIdlingResource来测试,MyIntentService服务启动结束这个异步事务,之后的结果。
@RunWith(AndroidJUnit4.class)
public class IntegrationTest {

    @Rule
    public ActivityTestRule rule = new ActivityTestRule(MainActivity.class);
    IntentServiceIdlingResource idlingResource;

    @Before
    public void before() {
        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
        Context ctx = instrumentation.getTargetContext();
        idlingResource = new IntentServiceIdlingResource(ctx);
        // 注册这个异步监听
        Espresso.registerIdlingResources(idlingResource);

    }
    @After
    public void after() {
        // 取消注册这个异步监听
        Espresso.unregisterIdlingResources(idlingResource);

    }

    @Test
    public void runSequence() {
        // MainActivity中点击R.id.action_settings这个View的时候,会启动MyIntentService
        onView(withId(R.id.action_settings)).perform(click());
        // 这时候IntentServiceIdlingResource#isIdleNow会返回false,因为MyIntentService服务启动了
        // 这个情况下,这里会block住.............
        // 直到IntentServiceIdlingResource#isIdleNow返回true,并且回调了IntentServiceIdlingResource#onTransitionToIdle
        // 这个情况下,继续执行,这时我们就可以测试异步结束以后的情况了。
        onView(withText("Broadcast")).check(matches(notNullValue()));
    }
}
로그인 후 복사

(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 단위 테스트 및 시뮬레이션 테스트에 대한 자세한 설명

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!