如何在Android中进行本地单元测试

Android上的测试种类

  1. Local Unit Test

在本机的Java VM上运行

  • 优点:运行速度快,jenkins每次编译时可以运行,适合做TDD
  • 缺点:不能测试android代码

2.Intrumented Test

运行的时候生成测试apk和被测应用的apk,安装到手机上运行

-优点:所有代码都能测试
-缺点:运行速度慢,不能在jenkins编译时运行

1.png

Local Unit Test##

代码位置

在src下创建test文件夹,如果测试代码是针对某个flavor和build type的,则创建testFlavorBuildType的文件夹

2.png

在gradle中设置


testCompile 'junit:junit:4.12'

生成测试代码

打开要测试类的代码,选中类的名字,ctrl+shift+T

3.png

选择要测试的方法,以及setUp和tearDown方法

4.png

生成的测试类

5.png

Junit 4注解

标注 说明
@Before 标注setup方法,每个单元测试用例方法调用之前都会调用
@After 标注teardown方法,每个单元测试用例方法调用之后都会调用
@Test 标注的每个方法都是一个测试用例
@BeforeClass 标注的静态方法,在当前测试类所有用例方法执行之前执行
@AfterClass 标注的静态方法,在当前测试类所有用例方法执行之后执行
@Test(timeout=) 为测试用例指定超时时间
6.png

断言

Junit提供了一系列断言来判断是pass还是fail

方法 说明
assertTrue(condition) condition为真pass,否则fail
assertFalse(condition) condition为假pass,否则fail
fail() 直接fail
assertEquals(expected, actual) expected equal actual pass,否则fail
assertSame(expected, actual) expected == actual pass,否则fail

更多方法查看Assert.java的静态方法

运行测试

IDE:点击边栏的箭头可以运行整个测试类或单个方法

7.png

Command line:gradle test,运行整个unit test

测试代码覆盖率

android gradle 插件自带jacoco进行代码测试覆盖率的统计

根build.gralde配置

buildscript {
...
    dependencies {
        ....
        classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1'
    }
}

模块build.gradle配置

apply plugin: 'jacoco-android'

buildTypes {
    debug {
        testCoverageEnabled = true
    }
}

//需要排除在统计之外的类
jacocoAndroidUnitTestReport {
    excludes += ['**/ApiConnectionImpl**']
}

cmd:

  • gradle jacocoTestReport 生成全部flavor和buildtype的测试覆盖率报告
  • gradle jacocoTestFlavorBuildTypeUnitTestReport 指定flavor和buildtype的测试覆盖率报告

生成的报告放在

module\build\reports\jacoco\jacocoTestFlavorBuildTypeUnitTestReport\html

8.png

Local unit test help libary

Hamcrest

testCompile 'org.hamcrest:hamcrest-library:1.3'

为断言提供更好的可读性(更接近与自然语言)

可读性

Junit:

assertEquals(expected, actual);

Hamcrest:

assertThat(actual, is(equalTo(expected)));

Junit:

assertFalse(expected.equals(actual));

Hamcrest:


assertThat(actual, is(not(equalTo(expected))));

失败信息更加详细

Junit

//AssertionError里没有expected和actual的信息
assertTrue(expected.contains(actual));
java.lang.AssertionError at ...

Hamcrest

assertThat(actual, containsString(expected));
java.lang.AssertionError:
Expected: a string containing "abc"
got: "def"

Assert 条件更加灵活

可以将多个assert条件通过anyOf() (或), allOf()(与)组合在一起

assertThat("test", anyOf(is("test2"), containsString("te")));

More Info

http://www.vogella.com/tutorials/Hamcrest/article.html

Mockito

testCompile "org.mockito:mockito-core:2.2.0"

Mock所需要测试的类,可以指定方法的返回值

两种方式创建mock

1.注解,这种方式必须添加MockitoRule

public class MockitoTest  {

        @Mock
        MyDatabase databaseMock; 

        @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); 

        @Test
        public void testQuery()  {
                ClassToTest t  = new ClassToTest(databaseMock); 
                boolean check = t.query("* from t"); 
                assertTrue(check); 
                verify(databaseMock).query("* from t"); 
        }
}

2.代码创建mock

@Test
public void test1()  {
        //  create mock
        MyClass test = Mockito.mock(MyClass.class);

        // define return value for method getUniqueId()
        when(test.getUniqueId()).thenReturn(43);

        // use mock in test....
        assertEquals(test.getUniqueId(), 43);
}

验证方法的调用

@Test
public void testVerify()  {
        // create and configure mock
        MyClass test = Mockito.mock(MyClass.class);
        when(test.getUniqueId()).thenReturn(43);

        // call method testing on the mock with parameter 12
        test.testing(12);
        test.getUniqueId();
        test.getUniqueId();

        // now check if method testing was called with the parameter 12
        verify(test).testing(Matchers.eq(12));

        // was the method called twice?
        verify(test, times(2)).getUniqueId();

        // other alternatives for verifiying the number of method calls for a method
        verify(mock, never()).someMethod("never called");
        verify(mock, atLeastOnce()).someMethod("called at least once");
        verify(mock, atLeast(2)).someMethod("called at least twice");
        verify(mock, times(5)).someMethod("called five times");
        verify(mock, atMost(3)).someMethod("called at most 3 times");
}

Spy vs Mock

对真正对象的包装,所有调用会调用真正对象的方法,但是会记录方法调用的信息

调用方法:@Spy or Mockito.spy()

@Test
public void whenSpyingOnList_thenCorrect() {
    List<String> list = new ArrayList<String>();
    List<String> spyList = Mockito.spy(list);
 
    spyList.add("one");
    spyList.add("two");
 
    Mockito.verify(spyList).add("one");
    Mockito.verify(spyList).add("two");
 
    assertEquals(2, spyList.size());
}

Spy也可以覆盖真正对象的方法:

@Test
public void whenStubASpy_thenStubbed() {
    List<String> list = new ArrayList<String>();
    List<String> spyList = Mockito.spy(list);
 
    assertEquals(0, spyList.size());
 
    Mockito.doReturn(100).when(spyList).size();
    assertEquals(100, spyList.size());
}

The mock simply creates a bare-bones shell instance of the Class, entirely instrumented to track interactions
with it.

On the other hand, the spy will wrap an existing instance. It will still behave in the same way as the normal
instance – the only difference is that it will also be instrumented to track all the interactions with it.

依赖注入

为了方便测试,一个类对外部类的依赖需要通过某种方式传入,而不是在类的内部创建依赖

public class MyClass {
    Foo foo;
    Boo boo;

    public MyClass() {
        foo = new Foo(); //依赖在内部创建,无法mock
        boo = new Boo();
    }
}
public class MyClass {
    Foo foo;
    Boo boo;

    public MyClass(Foo foo, Boo boo) {
        this.foo = foo; //依赖注入,测试MyClass时可以传入Mock的foo和boo
        this.boo = boo;
    }
}

More Info

http://www.baeldung.com/mockito-spy

http://www.vogella.com/tutorials/Mockito/article.html

Robolectric

testCompile "org.robolectric:robolectric:3.1.1"

Robolectric is designed to allow you to test Android applications on the JVM based on the JUnit 4 framework. Robolectric is a framework that allows you to write unit tests and run them on a desktop JVM while still using Android API. Robolectric mocks part of the Android framework contained in the android.jar file. Robolectric provides also implementations for the methods while the standard Android unit testing support throws exceptions for all Android methods.

This enables you to run your Android tests in your continuous integration environment without any additional setup. Robolectric supports resource handling, e.g., inflation of views. You can also use the findViewById() to search in a view

如何使用

在test之前使用RunWith注解

@RunWith(RobolectricGradleTestRunner.class)

在@Config中配置测试参数

@Config(sdk = Build.VERSION_CODES.JELLY_BEAN(default 16), 
    application = CustomApplication.class, 
    manifest = "some/build/path/AndroidManifest.xml")

在测试运行时,robolectric根据你要测试的api level从maven仓库中拉取对应android api实现jar包,如果你的机器上网络不太好,可以将这些jar先下载到本地,然后启用offline模式

android {
  testOptions {
    unitTests.all {
      systemProperty 'robolectric.offline', 'true'
      systemProperty 'robolectric.dependency.dir', "somewhere you place your jar"
    }
  }
}
9.png

示例代码:

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

        private MainActivity activity;

        @Test
        public void shouldHaveHappySmiles() throws Exception {
                String hello = new MainActivity().getResources().getString(
                                R.string.hello_world);
                assertThat(hello, equalTo("Hello world!"));
        }

        @Before
        public void setup()  {
                activity = Robolectric.buildActivity(MainActivity.class)
                                .create().get();
        }
        @Test
        public void checkActivityNotNull() throws Exception {
                assertNotNull(activity);
        }

        @Test
        public void buttonClickShouldStartNewActivity() throws Exception
        {
            Button button = (Button) activity.findViewById( R.id.button2 );
            button.performClick();
            Intent intent = Shadows.shadowOf(activity).peekNextStartedActivity();
            assertEquals(SecondActivity.class.getCanonicalName(), intent.getComponent().getClassName());
        }

        @Test
        public void testButtonClick() throws Exception {
                MainActivity activity = Robolectric.buildActivity(MainActivity.class)
                                .create().get();
                Button view = (Button) activity.findViewById(R.id.button1);
                assertNotNull(view);
                view.performClick();
                assertThat(ShadowToast.getTextOfLatestToast(), equalTo("Lala") );
        }

}

More Info

http://robolectric.org/

http://www.vogella.com/tutorials/Robolectric/article.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容