顯示具有 Unit Test 標籤的文章。 顯示所有文章
顯示具有 Unit Test 標籤的文章。 顯示所有文章

2014年4月13日 星期日

Robolectric介紹(二) 如何使用Robolectric 開發 project with SherlockActionBar

在 Robolectric介紹(一) shadow 介紹完簡單的功能之後,我開始思考是否有辦法將這個方法套用在我們的Project上面,在設定完成之後我嘗試run了一次test,結果如下


java.lang.IllegalStateException: there must have been some overlap for resourceIdToResName! expected 5815 but got 5814
at org.robolectric.res.MergedResourceIndex.merge(MergedResourceIndex.java:25)
at org.robolectric.res.MergedResourceIndex.(MergedResourceIndex.java:17)
at org.robolectric.res.RoutingResourceLoader.(RoutingResourceLoader.java:22)
at org.robolectric.RobolectricTestRunner.createAppResourceLoader(RobolectricTestRunner.java:598)
at org.robolectric.RobolectricTestRunner.getAppResourceLoader(RobolectricTestRunner.java:582)
at org.robolectric.internal.ParallelUniverse.setUpApplicationState(ParallelUniverse.java:98)
at org.robolectric.RobolectricTestRunner.setUpApplicationState(RobolectricTestRunner.java:401)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:219)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:174)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

在我花了很久時間debug與search之後,發現在xml內的小差異會造成test fail android:id="@android:id 以及 android:id="@+id 
只有android要求必須使用@android:id的才使用這種設定方式 例如 tabhost,ListFragment
否則一律使用 android:id="@+id ,錯用這個設定會造成Robolectric 無法 test

在projct成功可以test之後我遇到了另一個問題,由於我們的project implement了
ActionBarSherlock 所以在測試的時候常常會看到一些關於actionbar設定的nullpointer
為了解決這個問題,就必須在測試前預先做一些相關的設定

ActionBarSherlockRobolectric 提供了相關的解法,由於他提供的解法是給舊版本的,所以在這邊我們必須做一些小修改,只需要將

shadowOf(mActivity).setContentView(contentView);

修改成

mActivity.getWindow().setContentView(contentView);

就完成了 

接著我就可以開始做相關的測試 例如我希望測試從mainactivity 導向某個fragment之後的測試 
我可以這樣寫
 @Before
 public void init(){
  ActionBarSherlock.registerImplementation(ActionBarSherlockRobolectric.class);
  ActionBarSherlock.unregisterImplementation(ActionBarSherlockNative.class);
  ActionBarSherlock.unregisterImplementation(ActionBarSherlockCompat.class);
  
 }
 @Test @Config(reportSdk = 10, manifest = "AndroidManifest.xml")
    public void shouldNotBeNull() throws Exception {
  
     ShadowApplication shadowApplication = Robolectric.shadowOf(Robolectric.application);
     shadowApplication.declareActionUnbindable("com.google.android.gms.analytics.service.START");
     Fragment myFragment = new myTestFragment();
       
     
        startFragment( myFragment );

       assertNotNull( myFragment );
    }
    
    public static void startFragment( Fragment fragment )
    {

     SherlockFragmentActivity activity = Robolectric.buildActivity( MainActivity.class )
                                               .create()
                                               .start()
                                               .resume()
                                               .get();   

        FragmentManager fragmentManager = activity.getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add( fragment, null );
        fragmentTransaction.commit();
    }

需要注意的是,我在init做了相關的設定之後,接著在test的地方做了一些相關的Config,
AndroidManifest是要測試project的AndroidManifest.xml 否則會出現resourceNameNotFound
由於我們的project有使用google trace analytics所以必須加入下面這行
shadowApplication.declareActionUnbindable("com.google.android.gms.analytics.service.START");

否則會出現
java.lang.NullPointerException
 at com.google.analytics.tracking.android.AnalyticsGmsCoreClient$AnalyticsServiceConnection.onServiceConnected(AnalyticsGmsCoreClient.java:176)
 at org.robolectric.shadows.ShadowApplication$2.run(ShadowApplication.java:246)
 at org.robolectric.util.Scheduler$PostedRunnable.run(Scheduler.java:162)
 at org.robolectric.util.Scheduler.runOneTask(Scheduler.java:107)
 at org.robolectric.util.Scheduler.advanceTo(Scheduler.java:92)
 at org.robolectric.util.Scheduler.advanceToLastPostedRunnable(Scheduler.java:68)
 at org.robolectric.util.Scheduler.unPause(Scheduler.java:25)
 at org.robolectric.shadows.ShadowLooper.unPause(ShadowLooper.java:220)
 at org.robolectric.shadows.ShadowLooper.runPaused(ShadowLooper.java:259)
 at org.robolectric.util.ActivityController.create(ActivityController.java:111)
 at org.robolectric.util.ActivityController.create(ActivityController.java:123)
 at MyFragmentTest.startFragment(MyFragmentTest.java:61)
 at MyFragmentTest.shouldNotBeNull(MyFragmentTest.java:50)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
 at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:233)
 at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
 at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:174)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


接著myTestFragment 其實是使用mock的原理,由於我測試的Fragment在init的時候會去
onCreateOptionsMenu ,並且執行 mItem.getActionView(), 並且做一些相關的設定
但是我們的actionbar是用mock出來的所以在getActionView的時候會回傳null,
由於Menu的相關功能我可以額外寫一個測試,所以在這裡我並不想測試這部分也不想關心這部分,所以我直接mock了我想測試的fragment並且override了onCreateOptionsMenu的method,範例如下

public class myTestFragment extends MyFragment{

 @Override
 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  // TODO Auto-generated method stub
  //super.onCreateOptionsMenu(menu, inflater);
 }
}

在相關的設定都設定完成之後,我run了一次test結果卻依舊是紅燈


android.view.InflateException: <merge /> can be used only with a valid ViewGroup root and attachToRoot=true
 at android.view.LayoutInflater.inflate(LayoutInflater.java:455)
 at android.view.LayoutInflater.inflate(LayoutInflater.java:396)
 at android.view.LayoutInflater.inflate(LayoutInflater.java:352)
 at ActionBarSherlockRobolectric.setContentView(ActionBarSherlockRobolectric.java:40)
 at com.actionbarsherlock.app.SherlockFragmentActivity.setContentView(SherlockFragmentActivity.java:261)
 at com.nineyi.MainActivity.onCreate(MainActivity.java:268)
 at android.app.Activity.performCreate(Activity.java:5104)
 at org.fest.reflect.method.Invoker.invoke(Invoker.java:112)
 at org.robolectric.util.ActivityController$1.run(ActivityController.java:116)
 at org.robolectric.shadows.ShadowLooper.runPaused(ShadowLooper.java:257)
 at org.robolectric.util.ActivityController.create(ActivityController.java:111)
 at org.robolectric.util.ActivityController.create(ActivityController.java:123)
 at MyFragmentTest.startFragment(MyFragmentTest.java:61)
 at MyFragmentTest.shouldNotBeNull(MyFragmentTest.java:50)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
 at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:233)
 at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
 at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:174)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


這個地方我目前還想不到解法,原因是在MainActivity使用了<merge> 當作我們的root,造成了他的錯誤,由於目前還找不到特別的解法,所以目前這部分我也只能先將source改成 releativeLayout,希望之後有其他的方法可以解決這個問題

在經過了一番折騰努力之後終於看到了期待已久的綠燈...
不過這只不過是個開始,測試的路才剛開通而已~


相關參考文件
https://gist.github.com/JakeWharton/3803294
https://github.com/playhaven/playhaven-robolectric
http://robolectric.blogspot.mx/2013/05/configuring-robolectric-20.html
http://robolectric.blogspot.tw/2013/04/the-test-lifecycle-in-20.html
http://stackoverflow.com/questions/17993239/getsupportactionbar-returns-null-with-robolectric/18034473#18034473

2014年1月22日 星期三

Robolectric介紹(一) shadow

Why Use Robolectric

因為使用android 模擬器跑test的速度實在是太慢了, Robolectric讓我們可以再IDE上面run TDD
run test的速度也快得多。

安裝的介紹在  Android 開發 (十六) 使用Mockito和Robolectric寫 test case 裡面有詳細的介紹

今天要介紹的是shadowOf的應用


What is shadowOf  

有時候 android並未提供某些method Robolectric的 shadowOf就提供的那些method供我們測試用
舉例來說

在MainActivity的  UI為


    <ImageView
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

我們希望測試ImageView 的 src在程式中是否為 R.drawable.ic_launcher
我們寫出下面的測試

 public void TestDrawable()
 { 
  MainActivity activity = Robolectric.buildActivity(MainActivity.class).create().visible().get();
  ImageView img = (ImageView) activity.findViewById(R.id.img);
  ShadowImageView shadowImageView = Robolectric.shadowOf(img);
  assertEquals(Robolectric.shadowOf(
    shadowImageView.getDrawable()).getCreatedFromResId(),
    R.drawable.ic_launcher);
 }


上面是測試案例,先取得imageview接著
使用shadowOf 取得 shadowImageView
然後比較android:src 是否為  R.drawable.ic_launcher
使用test之後可以看到是綠燈。


shadowOf還有其他的功能
例如以下的程式


 @Override
 public void onClick(View arg0) {
  // TODO Auto-generated method stub
  startActivity(RecentActivityActivity.class);
 }

在click的時候切換到名為 RecentActivityActivity 的Activity
我們希望在測試的時候測試當click之後是否會切換到正確的activity


 @Test
 public void TestActivity()
 {
  MainActivity activity = Robolectric.buildActivity(MainActivity.class).create().visible().get();
  Button btn= (Button)activity.findViewById(R.id.btn);
  btn.performClick();
  ShadowActivity shadowActivity = Robolectric.shadowOf(activity);
  Intent startedIntent = shadowActivity.getNextStartedActivity();
     ShadowIntent shadowIntent =Robolectric.shadowOf(startedIntent);
  assertThat(shadowIntent.getComponent().getClassName(), equalTo(RecentActivityActivity.class.getName()));
 }

上面的程式碼使用 btn.performClick() 觸發 onclick event
接著取得 shadowIntent 並且  判斷新的 intent名稱是否為 RecentActivityActivity


另外一個常用的測試項目,判斷字串是否正確


 @Test
 public void TestHelloWorld()
 {
  MainActivity activity = Robolectric.buildActivity(MainActivity.class).create().visible().get();
  TextView txt = (TextView)activity.findViewById(R.id.txt);
  
  ShadowTextView shadowTextView = Robolectric.shadowOf(txt);

  assertEquals(shadowTextView.innerText(),activity.getString(R.string.hello_world));

 }

使用 shadowTextView 的 innerText 來判斷 字串是否相同
最後附上 sample code