Android单元测试(二)-实战

一、项目单元测试环境配置

gradle配置:

dependencies {
    // junit4
    testImplementation 'junit:junit:4.12'
    // mokito
    testImplementation "org.mockito:mockito-core:2.8.9"
    testImplementation "org.mockito:mockito-android:2.8.9”
  
    // powermokito
    testImplementation "org.powermock:powermock-module-junit4:1.7.1"
    testImplementation "org.powermock:powermock-api-mockito2:1.7.1"
    testImplementation 'org.powermock:powermock-core:1.7.1'
    testImplementation "org.powermock:powermock-module-junit4-rule:1.7.1"
    testImplementation "org.powermock:powermock-classloading-xstream:1.7.1"
   
    //robolectric
    testImplementation "org.robolectric:robolectric:3.3.1"
}

抽取单测基类:

1)纯java单测基类

@RunWith(RobolectricTestRunner.class)
@Config(manifest = "AndroidManifest.xml", sdk = 21, application = ApplicationStub.class)
public abstract class BaseJavaTest {

    private int androidSdkVersion;

    protected static void log(String msg) {
        System.out.println(msg);
    }

    @Before
    public void init() {
        // 输出日志
        MockitoAnnotations.initMocks(this);
        androidSdkVersion = VERSION.SDK_INT;
    }

    /**
     * hook执行前,测试log
     */
    @Before
    public void startLog() {
        log("======= start =======");
    }

    /**
     * hook执行后,测试log
     */
    @After
    public void endLog() throws NoSuchFieldException, IllegalAccessException {
        log("======= end =======");
        mockVersionSdkIntReturn(androidSdkVersion);
    }

    //基类可以封装一些通用的单测方法

    /**
     * mock sdk 版本
     */
    protected void mockVersionSdkIntReturn(int apiVersion) throws NoSuchFieldException, IllegalAccessException {
        Field sdkInt = VERSION.class.getField("SDK_INT");
        sdkInt.setAccessible(true);
        sdkInt.set(null, apiVersion);
    }
  ...
}

2)PowerMock基类

@RunWith(RobolectricTestRunner.class)
@Config(manifest = "AndroidManifest.xml", sdk = 21, application = ApplicationStub.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.powermock.*"})
public abstract class BasePowerMockTest {

    private int androidSdkVersion;

         //解决powermock和robolectric兼容性
    @Rule
    public PowerMockRule rule = new PowerMockRule();

    protected static void log(String msg) {
        System.out.println(msg);
    }

    @Before
    public void init() {
        // 输出日志
        MockitoAnnotations.initMocks(this);
        androidSdkVersion = VERSION.SDK_INT;
    }

    /**
     * hook执行前,测试log
     */
    @Before
    public void startLog() {
        log("======= start =======");
    }

    /**
     * hook执行后,测试log
     */
    @After
    public void endLog() {
        log("======= end =======");
        try {
            mockVersionSdkIntReturn(androidSdkVersion);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected void mockVersionSdkIntReturn(int apiVersion) throws NoSuchFieldException, IllegalAccessException {
        Field sdkInt = VERSION.class.getField("SDK_INT");
        sdkInt.setAccessible(true);
        sdkInt.set(null, apiVersion);
    }
  ...
}

二、可测代码设计原则

  • 充分利用mvp模式;
  • 一个函数只做一件事情,不要把几件事揉在一起;
  • 通过将查询和行为分离,可以方便对查询做测试;
  • 将纯逻辑部分拆分为独立函数,方便测试;
  • 函数的依赖尽量通过参数来传递;
  • 尽量不要把复杂结构当做参数传递给函数。

好的代码,能极大的避免mock,降低单测书写难度,因此单测某种程度上也能反过来倒逼程序员写出更优秀的代码。

三、Android单测实战场景

mock场景的单测主要包括三大步:构建对象 + 打桩 + 验证行为

3.1 构建对象
  • new 常规初始化对象
  • mock 构建空实现对象
  • spy 构建具体实现对象

注:
类没有依赖且对象好构建,那可以选择new来初始化对象,否则使用mock/spy。前者类对外部依赖较多,只关新少数函数的具体实现;后者类对外依赖较少,关心大部分函数的具体实现。

3.2 打桩

mock对象之后的后续函数操作,doCallRealMethod()、doReturn()、thenReturn()、doNothing()等是比较常用的打桩方法。

1)public/protected/default方法:

mock类执行真实方法

AppManagerModule appManagerModule = Mockito.mock(AppManagerModule.class);
Mockito.doCallRealMethod().when(appManagerModule).isIgnoreApp("22");
boolean res = appManagerModule.isIgnoreApp("22");
Assert.assertFalse(res);

公有方法返回修改值

ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

Context context = Mockito.mock(Context.class);
ActivityManager activityManager = Mockito.mock(ActivityManager.class);
Mockito.doReturn(activityManager).when(context).getSystemService(Mockito.eq(Context.ACTIVITY_SERVICE));
2)private/static/final方法

mock类执行真实方法

AppManagerModule mockAppManagerModule = PowerMockito.mock(AppManagerModule.class);
doCallRealMethod().when(mockAppManagerModule).getDownloadingApkCount();
mockAppManagerModule.getDownloadingApkCount();

静态方法返回修改值

List<DownloadInfo> downloadInfoList = DownloadProxy.getInstance().getDownloadInfoList(DownloadType.APK, true);

DownloadProxy downloadProxy = PowerMockito.mock(DownloadProxy.class);
//类的@PrepareForTest需要添加DownloadProxy.class
PowerMockito.mockStatic(DownloadProxy.class);
//DownloadProxy.getInstance()默认返回mock的downloadProxy实例
PowerMockito.when(DownloadProxy.getInstance()).thenReturn(downloadProxy);
//构建List<DownloadInfo>
ArrayList<DownloadInfo> downloadInfos = new ArrayList<>();
DownloadInfo downloadInfo = new DownloadInfo();
downloadInfos.add(downloadInfo);
//返回设置为构建的list
PowerMockito.when(downloadProxy.getDownloadInfoList(Mockito.any(SimpleDownloadInfo.DownloadType.class),Mockito.anyBoolean())).thenReturn(downloadInfos);

私有方法返回修改值

public class MockPrivateObjectClass {
    public String stepName;
    public MockPrivateObjectClass() {
    }

    private void setStepName() {
        System.out.print("enter setStepName");
        stepName = "has set name";
    }

    public void testStepName() {
        setStepName();
        System.out.print(stepName);
    }
}

  @Test
    public void replacePrivateMethodTest() {
        MockPrivateObjectClass objectClass = new MockPrivateObjectClass();
        PowerMockito.replace(PowerMockito.method(MockPrivateObjectClass.class, "setStepName")).with(new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                Whitebox.setInternalState(o, "stepName", "modify step name");
                return null;
            }
        });
        objectClass.testStepName();
    }

invoke对象私有方法

ObjectWhiteBoxClass objectWhiteBoxClass = new ObjectWhiteBoxClass();
Whitebox.invokeMethod(objectWhiteBoxClass, “addObject", "test1");
Assert.assertEquals(1, objectWhiteBoxClass.getObjectList().size());
3.3 验证行为
验证返回值:

Assert.assertFalse、Assert.assertEquals等

Assert.assertEquals("0", mockAppManagerModule.getPkgScanStatus());
Assert.assertFalse(appManagerModule.isIgnoreApp("22"););
验证方法被调用及其频率:(要求对象是mock对象)

Mockito.verify、PowerMotiko.verifyStatic、Mockito.verifyPrivate等

public/protected/default方法:

NormalClassB b = new MockNormalClassB();
 NormalClassB mockB = Mockito.spy(b);
Mockito.verify(mockB).getName();

Mockito.verify(triggerManager, Mockito.times(1)).showDesktopWindowLocked(
        Mockito.any(Context.class), Mockito.any(DesktopWinTrigger.class),
        Mockito.any(DesktopWinCardInfo.class),
        Mockito.anyLong(), Mockito.anyInt());

static方法:

PowerMockito.verifyStatic(Mockito.times(1));
ToastUtils.show(null, 0x7632343, Toast.LENGTH_SHORT);

private方法:

NormalClassA classA = new NormalClassA(); 
NormalClassA mockClassA = PowerMockito.spy(classA);
PowerMockito.verifyPrivate(mockClassA, times(1)).invoke("privateAdd", Mockito.anyInt(), Mockito.anyInt());

四、常见报错问题处理

持续更新中…..

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

推荐阅读更多精彩内容