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

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

一、Build Variants

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

单元测试番外(多渠道包)_第1张图片
图一.png

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

单元测试番外(多渠道包)_第2张图片
图二.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的值来实现多渠道包的开发或者是测试环境和正式环境的切换。当然我们现在切换编译类型是看不出效果的,我们还需要进一步的配置。

单元测试番外(多渠道包)_第3张图片
图三.png

二、Product Flavor

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

单元测试番外(多渠道包)_第4张图片
图四.png

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

单元测试番外(多渠道包)_第5张图片
图五.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个值可以选择,如图六

单元测试番外(多渠道包)_第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 == ""
    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,如图七

    单元测试番外(多渠道包)_第7张图片
    图七.png

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

    单元测试番外(多渠道包)_第8张图片
    图八.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变成红色字体了,如图九所示。

    单元测试番外(多渠道包)_第9张图片
    图九.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文件夹

单元测试番外(多渠道包)_第10张图片
图十.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文件夹确实是绿色的。如图十一

单元测试番外(多渠道包)_第11张图片
图十一.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中,也让代码目录更加清晰。

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

你可能感兴趣的:(单元测试番外(多渠道包))