前言
春节后,事情比较多,没太多写作灵感。之前在《App组件化与业务拆分那些事》说过要写一篇怎么Android怎么做业务拆分的技术文,由于开发中遇到一些繁琐问题,打算延后一点再写。
为了及时给点干货读者们,今天笔者写写如雷贯耳的 Robolectric 吧!
给Robolectric**的第一次
从我做单元测试开始,一直有小伙伴在群上反映第一次robolectric运行太慢了,大半天都更新不完依赖库。
上两天把项目的robolectric从3.1.2升到3.2.2,本来已经下好的第三方依赖库,3.2.2要求更高版本,只能再下更高版本的库。用过robolectric都懂的,如下图(gif):
笔者的第一次用robolectric,翻了墙,大概用了半小时下载依赖库。之前除了翻墙,也没什么好办法,后来研究一下,解决的办法还不止一种,接下来分析一下。
Robolectric到底在做什么?
简单的robolectric test case:
@RunWith(RobolectricTestRunner.class)
public class RoboTest {
@Test
public void firstTest() {
System.out.println("first test");
}
}
分析日志
截取其中一部分日志:
WARNING: No manifest file found at .\AndroidManifest.xml.
Falling back to the Android OS resources only.
...
Downloading: org/robolectric/android-all/4.1.2_r1-robolectric-0/android-all-4.1.2_r1-robolectric-0.pom from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 1K from sonatype
Downloading: org/robolectric/android-all/4.1.2_r1-robolectric-0/android-all-4.1.2_r1-robolectric-0.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 30702K from sonatype
...
只要英文不太烂,都知道日志说“正在从 https://oss.sonatype.org/content/groups/public/ 下载 android-all-4.1.2_r1-robolectric-0.pom、android-all-4.1.2_r1-robolectric-0.jar ...”。
oss.sonatype.org 是什么?
(已科学上网)
在浏览器输入https://oss.sonatype.org/ :
综合判断:
oss.sonatype.org是一个Nexus搭建的maven仓库。robolectric第一次运行,从https://oss.sonatype.org/ 下载一些必要的依赖包。
oss.sonatype.org服务器在哪?
ping oss.sonatype.org:
C:\Users\kkmike999>ping oss.sonatype.org
正在 Ping oss.sonatype.org [52.22.249.229] 具有 32 字节的数据:
请求超时。
请求超时。
请求超时。
请求超时。52.22.249.229 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失),
没错,oss.sonatype.org是外国的网站,百度一下52.22.249.229这个IP:
IP地址: 52.22.249.229美国
笔者甚至用国外的vp*服务器(vultr.com)来ping oss.sonatype.org,也一直超时。
迅雷下载......想太多
那我们找“4.1.2_r1-robolectric-0”在oss.sonatype.org上的路径,浏览https://oss.sonatype.org/content/groups/public/org/robolectric/android-all/4.1.2_r1-robolectric-0/,如下图:
可以看到 android-all-4.1.2_r1-robolectric-0.pom、android-all-4.1.2_r1-robolectric-0.jar等文件。
小白:“既然知道android-all-4.1.2_r1-robolectric-0.jar网址,直接下载吧,我有迅雷会员,离线下载,妥妥的!”
1小时后,小白下载并看完两集 波多野老师。再看看android-all-4.1.2_r1-robolectric-0.jar的迅雷任务,呃...
Gradle、Jcenter、第三方库
gradle从哪里下载第三方库
我们尝试用gradle下载android-all-4.1.2_r1-robolectric-0。在http://mvnrepository.com/ 找到 android-all-4.1.2_r1-robolectric-0,找到gradle引用它的语句testCompile group: 'org.robolectric', name: 'android-all', version: '4.1.2_r1-robolectric-0'
。
在app/build.gradle加入引用:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.1.1'
testCompile 'junit:junit:4.12'
testCompile "org.robolectric:robolectric:3.2.2"
testCompile group: 'org.robolectric', name: 'android-all', version: '4.1.2_r1-robolectric-0'
}
Sync gradle后,Android Studio底部显示下载进度:
可以看到gradle从 https://jcenter.bintray.com下载android-all-4.1.2_r1-robolectric-0依赖库。这个jar有30MB,jcenter有几十KB速度,需要点时间才能下载完。
从jcenter下载的库本地目录
Android Studio project窗口,External Libraries已经有android-all-4.1.2_r1-robolectric-0
,证明已经把库下载到本地。
右键->Library Properties
原来jar保存在 C:\Users\{User Name}\.gradle\caches\modules-2\files-2.1\
目录下。
再次运行robolectric单元测试
小白:“既然gradle从jcenter下好了android-all-4.1.2_r1-robolectric-0.jar,那这下robolectric就能依赖了吧!?”
于是,小白跑一次刚才的test case...
非常遗憾!robolectric显然不认~/.gradle/
的账。
robolectric依赖的本地目录 与 gradle依赖的本地目录 不相同。
robolectric的依赖库,本地放在哪?
用过eclipse或者inteliJ的同学应该知道,从maven仓库同步回来的库,会存在本地一个目录,这个目录就是~/.m2/
。
默认情况:
windows:C:\Users{用户名}.m2\repository
mac:\Users{用户名}.m2\repository\
如果你自定义了maven本地路径,那就找到设置后的~/.m2/
目录。
如果刚才通过gradle从oss.sonatype.org同步了一点点文件回来,这时应该存在 C:\Users\{用户名}\.m2\repository\org\robolectric\android-all\4.1.2_r1-robolectric-0\
,目录下有几个文件:
android-all-4.1.2_r1-robolectric-0.jar.tmp
android-all-4.1.2_r1-robolectric-0.pom
android-all-4.1.2_r1-robolectric-0.pom.sha1
android-all-4.1.2_r1-robolectric-0.pom.tmp.sha1.tmp
结论:
robolectric下载的库放在本地目录 ~/.m2/repository/
至于为什么robolectric会依赖~/.m2/
,在下一节源码剖析,会说明一下。
robolectric源代码
RobolectricTestRunner
public class RobolectricTestRunner extends BlockJUnit4ClassRunner {
private DependencyResolver dependencyResolver;
protected DependencyResolver getJarResolver() {
if (dependencyResolver == null) {
if (Boolean.getBoolean("robolectric.offline")) {
String dependencyDir = System.getProperty("robolectric.dependency.dir", ".");
dependencyResolver = new LocalDependencyResolver(new File(dependencyDir));
} else {
File cacheDir = new File(new File(System.getProperty("java.io.tmpdir")), "robolectric");
if (cacheDir.exists() || cacheDir.mkdir()) {
Logger.info("Dependency cache location: %s", cacheDir.getAbsolutePath());
dependencyResolver = new CachedDependencyResolver(new MavenDependencyResolver(), cacheDir, 60 * 60 * 24 * 1000);
} else {
dependencyResolver = new MavenDependencyResolver();
}
}
URL buildPathPropertiesUrl = getClass().getClassLoader().getResource("robolectric-deps.properties");
if (buildPathPropertiesUrl != null) {
Logger.info("Using Robolectric classes from %s", buildPathPropertiesUrl.getPath());
FsFile propertiesFile = Fs.fileFromPath(buildPathPropertiesUrl.getFile());
try {
dependencyResolver = new PropertiesDependencyResolver(propertiesFile, dependencyResolver);
} catch (IOException e) {
throw new RuntimeException("couldn't read " + buildPathPropertiesUrl, e);
}
}
}
return dependencyResolver;
}
}
我们找到DependencyResolver dependencyResolver
成员和跟dependencyResolver
密切相关的getJarResolver()
方法。
debug一下test case,并在getJarResolver()
里面打Breakpoints:
你发现调用了:
dependencyResolver = new CachedDependencyResolver(new MavenDependencyResolver(), cacheDir, 60 * 60 * 24 * 1000);
运行完getJarResolver()
,在Android Studio Debug工具查看RobolectricTestRunner
的变量:
关键的东西在这里,CachedDependencyResolver dependencyResolver
里面还有一个变量MavenDependencyResolver dependencyResolver
,这个MavenDependencyResolver
有变量及其值:
repositoryUrl = https://oss.sonatype.org/content/groups/public
repositoryId = sonatype
这个就是robolectric为什么从https://oss.sonatype.org下载依赖库的原因,只要把repositoryUrl替换其他url,就可以改变maven仓库网址了。
CachedDependencyResolver、MavenDependencyResolver
CachedDependencyResolver:
public class CachedDependencyResolver implements DependencyResolver {
private final DependencyResolver dependencyResolver;// MavenDependencyResolver
@Override
public URL[] getLocalArtifactUrls(DependencyJar... dependencies) {
...
final URL[] urls = dependencyResolver.getLocalArtifactUrls(dependencies);
...
return urls;
}
@Override
public URL getLocalArtifactUrl(DependencyJar dependency) {
...
final URL url = dependencyResolver.getLocalArtifactUrl(dependency);
...
return url;
}
}
MavenDependencyResolver(重点):
public class MavenDependencyResolver implements DependencyResolver {
private final String repositoryUrl;
private final String repositoryId;
// 默认从RoboSetting获取repositoryUrl和repositoryId,RoboSettings相当于Hook
public MavenDependencyResolver() {
this(RoboSettings.getMavenRepositoryUrl(), RoboSettings.getMavenRepositoryId());
}
public MavenDependencyResolver(String repositoryUrl, String repositoryId) {
this.repositoryUrl = repositoryUrl;
this.repositoryId = repositoryId;
}
@Override
public URL[] getLocalArtifactUrls(DependencyJar... dependencies) {
DependenciesTask dependenciesTask = createDependenciesTask();// AbstractArtifactTask子类
...
RemoteRepository remoteRepository = new RemoteRepository();
remoteRepository.setUrl(repositoryUrl);// 默认https://oss.sonatype.org/content/groups/public/
remoteRepository.setId(repositoryId);// 默认sonatype
dependenciesTask.addConfiguredRemoteRepository(remoteRepository);
...
dependenciesTask.execute(); // 调用AbstractArtifactTask.execute()
...
}
}
RoboSettings :
public class RoboSettings {
private static String mavenRepositoryId;
private static String mavenRepositoryUrl;
static {
mavenRepositoryId = System.getProperty("robolectric.dependency.repo.id", "sonatype");
mavenRepositoryUrl = System.getProperty("robolectric.dependency.repo.url", "https://oss.sonatype.org/content/groups/public/");// 看到默认以https://oss.sonatype.org/content/groups/public/为resitoryUrl
}
public static String getMavenRepositoryId() {
return mavenRepositoryId;
}
public static void setMavenRepositoryId(String mavenRepositoryId) {
RoboSettings.mavenRepositoryId = mavenRepositoryId;
}
public static String getMavenRepositoryUrl() {
return mavenRepositoryUrl;
}
public static void setMavenRepositoryUrl(String mavenRepositoryUrl) {
RoboSettings.mavenRepositoryUrl = mavenRepositoryUrl;
}
}
AbstractArtifactTask:
public abstract class AbstractArtifactTask extends Task{
public void execute()
{
...
initSettings();
doExecute(); // 下载或从本地读取依赖库
...
}
private File newFile( String parent, String subdir, String filename )
{
return new File( new File( parent, subdir ), filename );
}
private void initSettings()
{
if ( userSettingsFile == null )
{
File tempSettingsFile = newFile( System.getProperty( "user.home" ), ".ant", "settings.xml" );
if ( tempSettingsFile.exists() )
{
userSettingsFile = tempSettingsFile;
}
else
{
tempSettingsFile = newFile( System.getProperty( "user.home" ), ".m2", "settings.xml" );
if ( tempSettingsFile.exists() )
{
userSettingsFile = tempSettingsFile;
}
}
}
if ( globalSettingsFile == null )
{
File tempSettingsFile = newFile( System.getProperty( "ant.home" ), "etc", "settings.xml" );
if ( tempSettingsFile.exists() )
{
globalSettingsFile = tempSettingsFile;
}
else
{
// look in ${M2_HOME}/conf
List<String> env = Execute.getProcEnvironment();
for ( String var: env )
{
if ( var.startsWith( "M2_HOME=" ) )
{
String m2Home = var.substring( "M2_HOME=".length() );
tempSettingsFile = newFile( m2Home, "conf", "settings.xml" );
if ( tempSettingsFile.exists() )
{
globalSettingsFile = tempSettingsFile;
}
break;
}
}
}
}
Settings userSettings = loadSettings( userSettingsFile );// 读取并解析配置
Settings globalSettings = loadSettings( globalSettingsFile );// 读取并解析配置
SettingsUtils.merge( userSettings, globalSettings, TrackableBase.GLOBAL_LEVEL );
settings = userSettings;
if ( StringUtils.isEmpty( settings.getLocalRepository() ) )
{
String location = newFile( System.getProperty( "user.home" ), ".m2", "repository" ).getAbsolutePath();// 默认maven目录
settings.setLocalRepository( location );// 设置默认maven目录
}
...
}
}
initSetting()
主要任务,就是找到默认或setting.xml配置的maven目录,代码大致意思是:
1.加载
$user.home/.ant/setting.xml
或$user.home/.m2/setting.xml
或$M2_HOME/conf/setting.xml
,读取并解析配置文件,获取配置的maven目录;
2.如果没找到setting.xml,则默认$user.home/.m2/repository/
为maven本地目录。
$user.home
变量对应windows默认是C:\Users\{用户名}\
,mac默认\Users\{用户名}\
。这就知道默认.m2
目录是C:\Users\{用户名}\.m2\repository\
或 \Users\{用户名}\.m2\repository\
了。
DependenciesTask.doExecute()
处理从maven服务器下载依赖库到本地,读取本地依赖库等逻辑,本文不详述了,有兴趣的读者自己看看源码。
加速终极大招
大招1——把依赖文件拷贝到maven目录
既然我们知道robolectric依赖$user.home\.m2\repository\
,那直接把下载好的jar拷贝到该目录。例如4.1.2_r1-robolectric-0:
拷贝
C:\Users\kkmike999\.gradle\caches\modules-2\files-2.1\org.robolectric\android-all\4.1.2_r1-robolectric-0\aecc8ce5119a25fcea1cdf8285469c9d1261a352\android-all-4.1.2_r1-robolectric-0.jar
到$user.home\.m2\repository\org\robolectric\android-all\4.1.2_r1-robolectric-0\
或者到http://mvnrepository.com/artifact/org.robolectric/android-all/4.1.2_r1-robolectric-0下载android-all-4.1.2_r1-robolectric-0.jar,再拷贝到该目录。
robolectric有好几个依赖,必须把所有依赖都拷全。笔者不推荐这种做法。
大招2——把oss.sonatype.org改成阿里云maven仓库(推荐)
(2017.3.5更新)
先把
$user.home\.m2\repository\org\robolectric\
里面未下载完的目录删掉。因为这里可能有pom配置文件,里面的配置还是指向oss.sonatype.org,所以必须删除。
MyRobolectricTestRunner:
public class MyRobolectricTestRunner extends RobolectricTestRunner {
static {
// 从源码知道MavenDependencyResolver默认以RoboSettings的repositoryUrl和repositoryId为默认值,因此只需要对RoboSetting进行赋值即可
MavenRoboSettings.setMavenRepositoryUserName("");
MavenRoboSettings.setMavenRepositoryPassword("");
MavenRoboSettings.setMavenRepositoryId("alimaven");
MavenRoboSettings.setMavenRepositoryUrl("http://maven.aliyun.com/nexus/content/groups/public/");
}
public MyRobolectricTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
}
test case:
@Config(manifest = "./src/main/AndroidManifest.xml")
@RunWith(MyRobolectricTestRunner.class)
public class RoboTest {
@Test
public void firstTest() {
System.out.println("first test");
}
}
运行单元测试:
速度2M/s左右,有时更快。依赖库下载完,并完成单元测试,耗时17s:
(注意,这个速度测试,笔者仅删掉android-all-4.1.2_r1-robolectric-0.jar,实际robolectric还有好些依赖包,实际耗时要更长一些)
启发
可以在project/build.gradle添加阿里云maven仓库:
build.gradle
allprojects {
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()
}
}
速度扛扛的!
小结
Robolectric确实是不错的android单元测试第三方库,尽管运行起来有点慢。它能做挺多事情,例如直接测试sqlite(《Android单元测试 - Sqlite、SharedPreference、Assets、文件操作 怎么测?》)。
笔者写本文时,曾反复琢磨,究竟要慢慢分析问题,以实验形式来引出解决方法,还是剖析源码中,寻找解决方法呢?最终平衡了两个需求,成了本文这个样子。
希望更多的同学,在第一次做robolectric单元测试时,阅读本文,避免浪费时间。
关于作者
我是键盘男。
在广州生活,在互联网公司上班,猥琐文艺码农。跑步、喜欢科学、历史,玩玩投资,偶尔旅行。