单元测试番外(多渠道包)

Android Studio 自带了一个功能,叫做 Product Flavor,它可以让开发者更加便捷地切换代码源
集,不用再像之前一样做多渠道开发需要通过注释来实现,现在只需要在AS 界面上点击一下即可切换源代码集合。

一、Build Variants

新建工程后,点击 AS 左下角的 Build Variants(顾名思义,这个功能是用来设置编译变量以切换代码集合) 菜单按钮,在弹出的工具栏中点击右上角的小箭头,可以看到我们目前的编译可选变量为debugrelease,且默认选中debug变量,因为现在我们的app module 处于开发环境。

图一.png

这两个编译变量是 AS 默认提供且无法删除因为在 AS 自带的关于编译的部分功能需要用到这两个值,比如Build部分相关指令。这两个变量又称为Build Type,我们可以通过操作 AS Build -> Edit Build Types查看当前 module 有哪些Build Type值(默认就只有debugrelease且无法删除),我们还可以自己增加新的编译类型,如图二所示

图二.png

当然,我们也可以直接在 app 的 build.gradle中新增新的编译类型,格式如下,在这里我们可以把buildTypes下面的releasedebug删除,但AS 还是会默认提供的,我们可以通过上一步操作进入edit BuildTypes界面,会发现依然看得到这两个编译类型和新增的编译类型。

android {
    defaultConfig {}
    buildTypes {
        release {}
        //新增的编译类型
        define {
            debuggable false
            minifyEnabled false
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

现在我们重新打开 AS 左下角的 Build Variants界面,可以看到右上角的Build Variant下拉菜单多出一个define选项,我们在开发中就是通过切换Build Variant的值来实现多渠道包的开发或者是测试环境和正式环境的切换。当然我们现在切换编译类型是看不出效果的,我们还需要进一步的配置。

图三.png

二、Product Flavor

Product Flavor其实和Build Type很是类似。我们可以通过操作 AS 的Build -> Edit Flavor查看app对应的flavor,界面如图四

图四.png

通过对比图二和图四,我们发现两者大部分属性是比较相近的,但是Product Flavor默认支持自定义完整的Application IdTarget SDK Version,而且从命名的角度来看,这个功能更多地是实现多样化开发,即多语言、多地区等多渠道开发。我们可以再图四界面上点击左下角的+按钮添加新的 Product Flavor,结果如图五

图五.png

在AS 3.0之后,要求productFlavors必须有flavorDimension属性,否则编译器会报错,通过上步操作后我们打开app对应的build.gradle,可以看到在android目录下多了一个productFlavors代码块

android {
    compileSdkVersion 26
    defaultConfig {}
    buildTypes {}
    productFlavors {
        china {
        }
        usa {
        }
    }
}

由于我们没有给productFlavors设置flavorDimensions属性,所以编译出错,接下来我们补充完整的属性, sync 之后没有报错

flavorDimensions "country"
productFlavors {
    china {
        dimension "country"
    }
    usa {
        dimension "country"
    }
}

通过这样的设置之后我们就可以在Build Variants界面中切换不同的Build Variant来达到切换不同的开发环境这一个目的,现在我们可以看到Build Variant下拉列表中有6个值可以选择,如图六

图六.png

之所以有6个值,是因为Gradle使用以下命名方案组合创建总共 6 个构建变体:
构建变体:[china, usa][Debug, Release, Define]
对应 APK:app-[china, usa]-[debug, release, Define].apk

我们可以通过以下步骤测试一下

  1. 选择Build VariantchinaRelease
  2. 点击 AS 顶部菜单栏Build -> Clean Project //清除之前可能产生的脏文件
  3. 点击 AS 顶部菜单栏Build -> Build APK(s)

切换工程的查看模式为Project,在app--build--outputs--apk目录下我们可以看到有一个china文件夹,里面有一个release子文件夹,里面有一个apk,名字为app-china-release-unsigned.apk,是不是和我们上面说的构造变体生成的规则是很相似的?只不过多了一个-unsigned部分,因为我们没有选择Build -> Generated Signed APK操作。

所以我们就可以通过上面的操作来实现发布6个渠道包,分别是chinausa 对应的 releasedebugdefine渠道包,只需要切换Build Variant即可,是不是很简单?

不过我们目前build出来的 apk在功能上却是没有区别的,因为执行的代码都一样,这一点后面说,现在继续补充Product Falovr的内部组合用法。
其实flavorDimensionslist类型,我们可以指定多个值,现在我们为其增加一个food类型,并且为productFlavors增加两个声明为food的字段,代码如下

flavorDimensions "country", "food"
productFlavors {
    china {
        dimension "country"
    }
    usa {
        dimension "country"
    }
    fruits {
        dimension "food"
    }
    preservedegg { //皮蛋
        dimension "food"
    }
}

现在我们看到Build Variant有12个编译类型,其实完整的构建变体方案如下

构建变体:[china, usa][fruits, preservedegg][Debug, Release, Define] //组合2 * 2 * 3 = 12
对应 APK:app-[china, usa]-[fruits, preservedegg]-[debug, release, define].apk

flavorDimensions列表的属性是有优先级的,越先写的值优先级越高,表现在构件变体的时候命名的位置越前,所以构件的变体中,country类型的字段永远排在food类型前面。

我们也可以排除某些Product Flavor的组合,以美国人不吃皮蛋为例,我们把美国皮蛋这一变体去掉,代码如下

variantFilter { variant ->
    def names = variant.flavors*.name
    //如果要判断是否包含某个 build type, 使用 variant.buildType.name == "<buildType>"
    if (names.contains("usa") && names.contains("preservedegg")) {
        // Gradle 不会生成包含以上条件的变体
        setIgnore(true)
    }
}

现在我们再次查看Build Variants,可以看到只剩下9个Build Variant了,文章到这里,关于ProductFlavors的相关使用就介绍完毕,接下去我们就需要写点代码来体现不同渠道包功能的差异了。

三、Source Set

当使用 AS 创建一个 Android,工程的时候,app源码是存放在app/src/main/java目录下的,当我们创建了编译变体之后,以chinaFruitsDebug为例,其源码的位置默认是app/src/chinaFruitsDebug/java(我们可以通过点击 AS 右上角的 Gradle菜单按钮,执行:app--Tasks--android--sourceSets,然后点击AS 右下角的Gradle Console查看chinaFruitsDebug对应的Java sources的值)。
chinaFruitsDebug编译变体为例,在未设置 sourceSets的前提下,编译器寻找代码的顺序如下

1. 首先去`app/src/chinaFruitsDebug/java/`目录下寻找,如果找不到则往下走
2. 去`app/src/{case}/java/`目录下找,如果找不到则往下走,
3. 去`app/src/main/java`目录下找,如果找不到则提示找不到目标的错误

其中{case}就复杂了,这个组合关系我有点难以描述,以`chinaFruitsDebug`为例,case 可以是,china、fruits、debug、chinaFruits、chinaFruitsDebug。[flavor]的两个属性可以单独存在或者组合在一起,debug也可以单独存在,但是组合的话只能和完整的 flavor 组合,不能出现类似 chinaDebug之类的。

幸好我们可以选择不去记忆这种规则,在`Project`目录结构下,我们可以看到`src/china|fruits|chinaFruits/`目录下面的`java`文件夹的颜色为蓝色!如果我们这个时候新建一个`src/usa/java`的源集,可以看到其`java`文件夹为灰色,蓝色就表示在当前编译变体的环境下,编译器会去这些源集中寻找目标代码

以chinaFruitsDebug为例,我们创建app/src/chinaFruitsDebug/java文件夹

  1. 切换工程目录格式为Project

  2. app--src文件夹右键选择创建Java Folder,如图七

    图七.png

  3. 在弹出的创建文件夹界面中进行如下操作,记得文件路径写src/chinaFruitsDebug/java,如图八所示,这里简单说明一下,通过勾选Change Folder Location我们可以修改文件夹路径,文本输入框就是输入我们想要存放的路径,这里以编译变体名命名,方便理解和查看。至于第三个Target Source Set,它的作用是指定编译变体对应的源代码目录,其结果就是在appbuild.gradle中的android代码块中生成一个sourceSets代码块,这点待会介绍

    图八.png

  4. src/chinaFruitsDebug/java/目录下我们新建一个包,包名和app module的包名保持一致(方便无缝切换),我的包名是com.mango.multiproduct,在这个包下面创建一个类,代码如下

public class Descrption {

    public static String desc() {
        return "chinaFruitsDebug";
    }
}
  1. 打开Build Variants界面,将app对应的Build Variant选择为chinaFruitsDebug
  2. 新建一个test目录下的单元测试类(可以看我前面写的单元测试相关文章),代码如下
public class DescTest {

    @Test
    public void onCreate() throws Exception {
        System.out.println("fetch info:" + Descrption.desc());
    }
}

执行单元测试,最后可以在控制台看到以下输入信息,表示我们在切换Build Variant之后成功找到了目标代码

fetch info:chinaFruitsDebug
  1. 打开Build Variants界面,将app对应的Build Variant选择为chinaFruitsRelease,这个时候我们就直接看到Descrption变成红色字体了,如图九所示。

    图九.png

  2. 为了验证我上面所说的在/src/chinaFruitsRelease/java找不到Descrption类的时候编译器最终会去/src/main/java/目录下找(其它 {case}目录优先main目录,这点大家可以去试试),我们把Descrption.java复制到目标位置,并且让desc方法返回return "MainDescrption";,这个时候可以看到单元测试没有提示找不到Descrption类了,这个时候我们Clean Project后再执行一次单元测试,可以看到控制台输出我们想要的信息了

fetch info:MainDescrption

回到本节的正题,我们查看appbuild.gradle文件,可以看到多出了sourceSets代码块,如下

android {
    compileSdkVersion 26
    defaultConfig {}
    buildTypes {}

    flavorDimensions "country", "food"
    productFlavors {}

    variantFilter {}
    sourceSets {
        chinaFruitsDebug {
            java.srcDirs = ['src/chinaFruitsDebug/java', 'src/chinaFruitsDebug/java/']
        }
    }
}

sourceSets中的字段就是Build Variant值了,意思就是某个编译变体所对应的源集(源代码集合)位置,可以是java代码,也可以是jni源码、也可以是资源文件架。当然我们把上面的sourceSets删除也是没关系的,因为我们给chinaFruitsDebug设置的关联目录就是编译器默认去寻找的目录。来试一下,我们直接把sourceSets代码块删除,然后打开Build Variants界面,切换Build VarinantchinaFruitsDebug,这个时候Descption.java报错了,提示重复定义类了,说明编译器确实是去src/chinaFruitsDebug/java/目录下找到了Descrption.java,这个时候我们把main/java目录下重复的类删除,然后clean一下工程(否则可能单元测试不过),然后执行单元测试便可以看到如上第6个步奏输出的结果。

sourceSets已经解释完毕,需不需要用到,就看我们在创建编译变体源集的时候的路径是不是 AS 默认的路径了,一旦我们为某个编译变体明确指定了源集,那么编译器第一次尝试寻找的位置就变成我们指定的位置,如果找不到的话就继续往上层去寻找。

四、蓝色、绿色和灰色?

不同的源集在不同的编译变体环境中的java文件夹的颜色是不同的,它们分别代表什么呢?
如图十所示,我们可以看到这几种颜色的java文件夹

图十.png

灰色我们已经说过了,灰色表示该源集在当前编译变体的环境下,编译器不会去该源集中寻找目标代码。
蓝色则表示表示该源集在当前编译变体的环境下,编译器会去该源集中寻找目标代码。
那么绿色呢?绿色表示的不是源集,而是测试集合,意味着这些代码是不能上传到服务器的。而如果是绿色并且右下角有一个android机器人小图标,则表示该文件夹的单元测试是运行在Android运行环境的

那么为什么编译器知道哪些是源集哪些是测试集合?因为它自己制定了一个命名规则:

那就是[test/Android + B + C],其中 B 是 Flavor 中的任一个值,允许为空;C 是 BuildType 中的任一个值,允许为空

我们进行一步操作来验证我们猜想的规则,这里可以跳过不看,看看编译器默认为module设置的sourceSets是什么,点击AS右上角的Gradle菜单按钮,执行:app--Tasks--android--sourceSets,然后点击AS 右下角的Gradle Console我们可以看到如下内容,由于信息太多,所以我只展示部分关键信息

------------------------------------------------------------
Project :app
------------------------------------------------------------

androidTest
-----------
Java sources: [app/src/androidTest/java]

androidTestChina
----------------
Java sources: [app/src/androidTestChina/java]

china
-----
Java sources: [app/src/china/java]

test
----
Java sources: [app/src/test/java]

testChina
---------
Java sources: [app/src/testChina/java]

通过上面的输出可以看到,AS 默认的 sourceSet 中已经有这些测试源集的名称和源码位置了,以testChina为例,我们可以看到其位置是app/src/testChina/java,我们新建一个以test开头的目录,以testChina为例,结果我们看到确实java文件夹确实是绿色的。如图十一

图十一.png

反之我们新建一个testABC的目录,在其目录下新建一个java目录,可以看到其颜色是灰色的,因为这样的命名不符合 AS 的规矩,所以 AS 不认可它作为测试源集。

在这里我们引出另外一条命名规矩:
BuildType 和 ProductFlavor 中的名字不能以 test、AndroidTest 开头

下面验证一下,可以不看我们在appbuild.gradle中的builtTypes代码块中新增一个字段,字段名为testMango,如果我们猜测是对的,那么待会去创建对应的源集的时候java文件夹应该是绿色的。不过 AS 很智能,我们的猜测也是对的,因为sync之后,编译器报错了,错误信息如下:

Error:(25, 0) BuildType names cannot start with 'test'

同样的,我们在productFlavors代码块中增加一个字段,字段名为testMango,点击sync,结果编译器也报错了,错误信息也类似,如下:

Error:(44, 0) ProductFlavor names cannot start with 'test'

同样的,我们也可以做同样的试验,以androidTest开头添加字段到buildTypesproductFlavors代码块中,也同样报相似的错误信息。
至于怎么创建蓝色或者灰色的源集就不再赘述了,因为本章大部分时候都是在创建这类源集。

测试集合和其它源集有一个非常重要的区别,那就是前者里面的代码并不会在编译的时候打包到程序中,所以不会出现类重定义的编译异常。

五、废了这么大周章,搞这些有什么用?

文章一开始就简单说明了一些快速切换渠道包的好处,可以不通过修改注释直接简单配置不同渠道所使用的代码,降低这方面代码的耦合。
然后还有一种作用就是用来切换mockprodmock表示数据仿真,prod表示实际的生产环境。
举个例子,在开发网络这一块的时候我们在开发的时候可以切换到mock 分支,对后台数据进行模拟,这样可以快速验证我们业务模块的整体逻辑,等到逻辑理顺了,切换回prod联调,确保聚焦当下,排除无用干扰。

同样的我们在单元测试的时候也常常需要去打桩(mock),以覆盖分支为手段,快速验证某些与桩对象具体实现无关的逻辑,比如验证程序调用流程是否符合时序图的流程。那么需要打桩的单元测试放在testMock中,不需要的单元测试放在test中,也让代码目录更加清晰。

之所以写这篇文章,也是为了后面测试做准备,原本的想法很简单,测试代码多了,种类多了总不能都放在一个地方吧,这样显得多么臃肿。

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

推荐阅读更多精彩内容

  • 转载注明出处://www.greatytc.com/p/5255b100930e 0. 前言 完全由个人翻...
    王三的猫阿德阅读 2,509评论 0 4
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,646评论 18 139
  • 1、财务方面: 1、本月开票截止到27号下午三点。 2、本月只开5.6.7三月发票,请各位及时整理。以免延期而 未...
    松松劲柏阅读 115评论 0 0
  • 事件:这几天都在反复回忆和觉察父母对我的影响~(因为前几天回家和父母对话)有感而发写了近四千字的重大事件回顾和自己...
    胡晓梅阅读 137评论 0 0
  • 许久没有如此酣畅淋漓的一口气读完一本书,一本外国文学。 我对国外作品,不知为什么,有种与生俱来的排斥感。翻译过来的...
    媛小医阅读 254评论 0 0