在Android模擬器或設備運行測試很慢!構建,部署和啓動應用程序每每須要一分鐘或更長時間。
在Android直接從您的IDE內部運行測試豈不是很好?但容易碰到java.lang.RuntimeException: Stub!
Robolectric不依賴Android SDK jar的安卓單元測試框架,測試在JVM上運行,能夠在幾秒內完成。
示例:
html
@RunWith(RobolectricTestRunner.class) public class MyActivityTest { @Test public void clickingButton_shouldChangeResultsViewText() throws Exception { MyActivity activity = Robolectric.setupActivity(MyActivity.class); Button button = (Button) activity.findViewById(R.id.button); TextView results = (TextView) activity.findViewById(R.id.results); button.performClick(); assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!"); } }
Robolectric改寫Android SDK中的類,使他們可能在普通的JVM上運行。
Robolectric視圖和資源及本地C代碼實現等東東加載,很容易地提供咱們本身的特定的SDK方法實現,好比模擬錯誤狀況或現實世界的sensor行爲。
運行測試模擬器以外
Robolectric,您能夠在工做站上運行測試,或在常規JVM持續集成環境,沒有一個模擬器。正由於如此,德興,包裝和安裝 - 上的仿真器的步驟是沒有必要的,減小分鐘的測試周期秒鐘,這樣能夠快速迭代,並有信心重構代碼。
Robolectric的風格更接近黑盒測試,一般不須要Mockito等模擬框架,固然依舊能夠與Mockito配合使用。java
Robolectric與Gradle或Maven配合比較好。新項目推薦使用Gradle。
build.gradle
python
testCompile「org.robolectric:robolectric:3.0」
測試使用註解便可:
android
@RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class) public class SandwichTest { }
注意必須指定constants指向由生成系統生成的BuildConfig.class。Robolectric使用類的常量來計算輸出路徑(Gradle使用), 不然Robolectric將沒法找到manifest,resources或asset。
使用Maven構建
pom.xml:
git
<dependency> <groupId>org.robolectric</groupId> <artifactId>robolectric</artifactId> <version>3.0</version> <scope>test</scope> </dependency>
測試使用註解便可:
github
@RunWith(RobolectricTestRunner.class) public class SandwichTest { }
若是你引用項目之外的資源(即在AAR依賴)項目之外的資源,須要提供Robolectric一個指針指向AAR在構建系統。
Android Studioapache
Robolectric適用於Android Studio 1.1.0及之後版本。在"Build Variants" 中開啓 unit test支持便可。Linux和Mac中須要編輯「run configurations」,設置工做目錄爲$MODULE_DIR$。api
Eclipse已經不推薦使用,可是能夠安裝m2e-android(不支持AAR)插件,導入Maven 工程, 點擊 "Plugin execution not covered by lifecycle configuration"。
示例工程參見:https://github.com/robolectric/robolectric-samples。
app
activity的layout以下:
框架
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/login" android:text="Login" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
咱們要編寫測試斷言當用戶點擊按鈕時應用程序啓動LoginActivity。
public class WelcomeActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.welcome_activity); final View button = findViewById(R.id.login); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startActivity(new Intent(WelcomeActivity.this, LoginActivity.class)); } }); } }
檢查啓動了對應的intent便可:
@RunWith(RobolectricTestRunner.class) public class WelcomeActivityTest { @Test public void clickingLogin_shouldStartLoginActivity() { WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class); activity.findViewById(R.id.login).performClick(); Intent expectedIntent = new Intent(activity, WelcomeActivity.class); assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent); } }
注意:目前Robolectric只有API 16/19/21的實例。貌似還不支持API 23等。上述代碼不能實際執行,實際執行的參見demo。
後期有空會把上面代碼串成實例。
配置註解
定製Robolectric的主要方式是經過@Config註解。註釋能夠應用到的類和方法,同事也能夠在基類中定製。
配置SDK級別
Robolectric基於manifest的targetSdkVersion運行代碼,可更改設置SDK版本:
@Config(sdk = Build.VERSION_CODES.JELLY_BEAN) public class SandwichTest { @Config(sdk = Build.VERSION_CODES.KITKAT) public void getSandwich_shouldReturnHamSandwich() { } }
配置應用程序類
@Config(application = CustomApplication.class) public class SandwichTest { @Config(application = CustomApplicationOverride.class) public void getSandwich_shouldReturnHamSandwich() { } }
配置資源路徑
Gradle和Maven有默認值,也可自定義:
@Config(manifest = "some/build/path/AndroidManifest.xml") public class SandwichTest { @Config(manifest = "other/build/path/AndroidManifest.xml") public void getSandwich_shouldReturnHamSandwich() { } }
默認資源目錄res和assets。經過添加resourceDir和assetDir選項到@Config可改變這些值。
配置屬性
@Config註解的內容能夠在文件中定義robolectric.properties:
sdk=18 manifest=some/build/path/AndroidManifest.xml shadows=my.package.ShadowFoo,my.package.ShadowBar
系統屬性
robolectric.offline - Set to true to disable runtime fetching of jars.
robolectric.dependency.dir - When in offline mode, specifies a folder containing runtime dependencies.
robolectric.dependency.repo.id - Set the ID of the Maven repository to use for the runtime dependencies (default sonatype).
robolectric.dependency.repo.url - Set the URL of the Maven repository to use for the runtime dependencies (default https://oss.sonatype.org/content/groups/public/).
robolectric.logging.enabled - Set to true to enable debug loggin
Gradle可在all部分爲單元測試配置系統屬性。例如,覆蓋Maven倉庫URL和ID:
android { testOptions { unitTests.all { systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo' systemProperty 'robolectric.dependency.repo.id', 'local' } } }
參考資料:
http://tools.android.com/tech-docs/unit-testing-support。
Robolectric2.2以前,大多數的測試直接調用構造函數 (new MyActivity()),而後手動調用的生命週期方法,如OnCreate()。ShadowActivity的方法(例如ShadowActivity.callOnCreate())也常常使用,他們是的ActivityController的前身。ActivityController在Robolectric2.0中引入的。
如今不會直接建立ActivityController,而是使用Robolectric.buildActivity()開始。一般能夠這樣:
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();
上面建立MyAwesomeActivity實例並調用onCreate()。
要檢查onResume()也簡單:
ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start(); Activity activity = controller.get(); // assert that something hasn't happened activityController.resume(); // assert it happened!
相似的方法也包括start(), pause(), stop(), and destroy(),好比測試整個建立生命週期:
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().start().resume().visible().get();
能夠帶intent啓動Activity:
Intent intent = new Intent(Intent.ACTION_VIEW); Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
或恢復已保存實例的狀態:
Bundle savedInstanceState = new Bundle(); Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class) .create() .restoreInstanceState(savedInstanceState) .get();
更多資料參見http://robolectric.org/javadoc/latest/org/robolectric/util/ActivityController.html。
爲了減小對應用外部依賴,Robolectric的shadow被分紅各類附加包。主Robolectric模塊只提供基礎的Android SDK提供的shadow。
SDK Package | Robolectric Add-On Package |
---|---|
com.android.support.support-v4 | org.robolectric:shadows-support-v4 |
com.android.support.multidex | org.robolectric:shadows-multidex |
com.google.android.gms:play-services | org.robolectric:shadows-play-services |
com.google.android.maps:maps | org.robolectric:shadows-maps |
org.apache.httpcomponents:httpclient | org.robolectric:shadows-httpclient |
開發代碼:
import android.app.Activity; import android.os.*; import android.view.View; import android.widget.Toast; import com.oppo.acs.st.STManager; import com.oppo.acs.st.demo.R; import java.util.HashMap; import java.util.Map; public class MainActivity extends Activity { /** Those data is just for test. */ private static final String DATA_TYPE_EXPOSE="cpd-app-expose"; private static final String DATA_TYPE_CLICK="cpd-app-click"; private static final String DATA_TYPE_DOWNLOAD="cpd-app-down"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); } public void onBnExpose(View view){ Toast.makeText(this, "expose", Toast.LENGTH_SHORT).show(); /** *Record the expose event. */ STManager.getInstance().onEvent(this, wrapKeyMap(view.getId())); } public void onBnClick(View view){ Toast.makeText(this, "click", Toast.LENGTH_SHORT).show(); /** *Record the click event. */ STManager.getInstance().onEvent(this, wrapKeyMap(view.getId())); } public void onBnDownload(View view){ Toast.makeText(this,"download", Toast.LENGTH_SHORT).show(); /** *Record the download event. */ STManager.getInstance().onEvent(this, wrapKeyMap(view.getId())); } public void onBnBatchExpose(View view){ Toast.makeText(this,"batch expose.", Toast.LENGTH_SHORT).show(); int i=0; while (i++<100){ STManager.getInstance().onEvent(this,wrapKeyMap(view.getId())); } } private Map<String,String> wrapKeyMap(int viewId){ Map<String,String> keyMap=new HashMap<String,String>(); //those data is just for test. keyMap.put(STManager.KEY_ENTER_ID,"demo"); keyMap.put(STManager.KEY_TAB_ID,"MainActivity"); keyMap.put(STManager.KEY_AD_POS_ID,"1001"); keyMap.put(STManager.KEY_CATEGORY_ID,"10001"); keyMap.put(STManager.KEY_PAR_TAB_ID,"1001"); keyMap.put(STManager.KEY_PAR_POS_ID,"10001"); keyMap.put(STManager.KEY_AD_ID,"200"); keyMap.put(STManager.KEY_AD_TYPE,"cpd"); keyMap.put(STManager.KEY_AD_OWNER,"OPPO"); keyMap.put(STManager.KEY_CONTENT_ID,"contentId"); keyMap.put(STManager.KEY_CONTENT_CLS,"test"); keyMap.put(STManager.KEY_CONTENT_SIZE,"20K"); keyMap.put(STManager.KEY_PLAN_ID,"100"); keyMap.put(STManager.KEY_PRICE,"200.00"); keyMap.put(STManager.KEY_PAR_EVENT_ID,"001"); keyMap.put(STManager.KEY_TRACE_ID,"001"); keyMap.put(STManager.KEY_AB_TEST,"ATest"); keyMap.put(STManager.KEY_GRADE,"good"); keyMap.put(STManager.KEY_PROPERTY,"test demo"); keyMap.put(STManager.KEY_DISPLAY_TIME, String.valueOf(System.currentTimeMillis())); // keyMap.put(STManager.KEY_MODULE_ID,"Test"); keyMap.put(STManager.KEY_PAR_MODULE_ID,"Test_Par"); keyMap.put(STManager.KEY_CHANNEL,"Test"); keyMap.put(STManager.KEY_APP_ID,"1001"); // switch (viewId){ case R.id.expose_bn: case R.id.batch_expose_bn: /** *Data type of expose event. */ keyMap.put(STManager.KEY_DATA_TYPE, DATA_TYPE_EXPOSE); break; case R.id.click_bn: /** *Data type of click event. */ keyMap.put(STManager.KEY_DATA_TYPE,DATA_TYPE_CLICK); break; case R.id.download_bn: /** *Data type of download event. */ keyMap.put(STManager.KEY_DATA_TYPE,DATA_TYPE_DOWNLOAD); break; default: break; } return keyMap; } @Override public void onBackPressed() { super.onBackPressed(); /** *Report all statistics data before application exit. */ STManager.getInstance().onExit(this, new STManager.ExitListener() { @Override public void onFinish(boolean result) { /** * Exit the application when report statistics data finished. */ android.os.Process.killProcess(android.os.Process.myPid()); } }); } }
測試代碼:
import android.app.Activity; import android.view.Menu; import android.widget.Button; import com.oppo.acs.st.demo.BuildConfig; import com.oppo.acs.st.demo.R; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowToast; import junit.framework.Assert; import static org.robolectric.Shadows.shadowOf; @RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class) public class MainActivityTest { Activity activity; @Before public void setUp() throws Exception { activity = Robolectric.setupActivity(MainActivity.class); final Menu menu = shadowOf(activity).getOptionsMenu(); } @After public void tearDown() throws Exception { } @Test public void testOnBnExpose() throws Exception { Button button = (Button) activity.findViewById(R.id.expose_bn); button.performClick(); Assert.assertEquals("expose", ShadowToast.getTextOfLatestToast()); } @Test public void testOnBnClick() throws Exception { Button button = (Button) activity.findViewById(R.id.click_bn); button.performClick(); Assert.assertEquals("click", ShadowToast.getTextOfLatestToast()); } @Test public void testOnBnDownload() throws Exception { Button button = (Button) activity.findViewById(R.id.download_bn); button.performClick(); Assert.assertEquals("download", ShadowToast.getTextOfLatestToast()); } @Test public void testOnBnBatchExpose() throws Exception { Button button = (Button) activity.findViewById(R.id.batch_expose_bn); button.performClick(); Assert.assertEquals("batch expose.", ShadowToast.getTextOfLatestToast()); } }package com.oppo.acs.st; import android.app.Application; import java.util.List; import com.oppo.acs.st.demo.BuildConfig; import org.junit.runner.RunWith; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; import static org.junit.Assert.*; @RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class) @PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) @PrepareForTest({STManager.class}) public class STManagerTest { private STManager sTManager; private Application application; @Before public void setUp() throws Exception { sTManager = STManager.getInstance(); sTManager.enableDebugLog(); application = RuntimeEnvironment.application; ShadowLog.stream = System.out; } @After public void tearDown() throws Exception { } @Test public void init() throws Exception { sTManager.init(application); List logs = ShadowLog.getLogs(); System.out.println(logs.size()); String result = readFile(); assertTrue(result.contains("has no initted.init!!")); } @Test (expected = Exception.class) public void initWithNullRaiseException() throws Exception { sTManager.init(null); } public String readFile() throws Exception { String result = ""; List logs = ShadowLog.getLogs(); for (int i = 0; i < logs.size(); i++) { // System.out.println(logs.get(i).toString()); result = result + ((ShadowLog.LogItem) logs.get(i)).msg; } return result; } }
安卓端基於日誌的測試方法
package com.oppo.acs.st; import android.app.Application; import java.util.List; import com.oppo.acs.st.demo.BuildConfig; import com.oppo.acs.st.utils.Utils; import org.junit.Rule; import org.junit.runner.RunWith; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; import static org.junit.Assert.*; @RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) @PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) @PrepareForTest(Utils.class) public class STManagerTest { private STManager sTManager; private Application application; @Rule public PowerMockRule rule = new PowerMockRule(); @Before public void setUp() throws Exception { sTManager = STManager.getInstance(); sTManager.enableDebugLog(); application = RuntimeEnvironment.application; ShadowLog.stream = System.out; } @After public void tearDown() throws Exception { } @Test public void init() throws Exception { sTManager.init(application); List logs = ShadowLog.getLogs(); System.out.println(logs.size()); String result = readFile(); assertTrue(result.contains("has no initted.init!!")); } @Test public void initWithoutNetwork() throws Exception { PowerMockito.mockStatic(Utils.class); PowerMockito.when(Utils.isNetworkAvailable(application)).thenReturn(false); assertFalse(Utils.isNetworkAvailable(application)); sTManager.init(application); String result = readFile(); assertTrue(result.contains("no net!")); } @Test (expected = Exception.class) public void initWithNullRaiseException() throws Exception { sTManager.init(null); } public String readFile() throws Exception { String result = ""; List logs = ShadowLog.getLogs(); for (int i = 0; i < logs.size(); i++) { // System.out.println(logs.get(i).toString()); result = result + ((ShadowLog.LogItem) logs.get(i)).msg; } return result; } }
上面例子要特別注意必需要添加Rule才行。