前言
春節后,事情比較多,沒太多寫作靈感。之前在《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單元測試時,閱讀本文,避免浪費時間。
關于作者
我是鍵盤男。
在廣州生活,在互聯網公司上班,猥瑣文藝碼農。跑步、喜歡科學、歷史,玩玩投資,偶爾旅行。