2016/11/28

設定 build.gradle 來用 Spock 對 Android 元件進行測試

在「使用 Android Studio 開發 Web 程式 - 測試」中說明了為什麼要選擇 Spock Framework 來做為測試時的 Framework。在實作時 build.gradle 要做一定程度上的設定,才能夠使以 Spock 寫出來的程式碼得以運作,以下將會說明相關設定上的細節。

首先,Spock 的測試程式碼需要使用 Groovy 語言來撰寫,所以 build.gradle 中要先引用 Groovy 的 Gradle Plugin。第一步要在 Root 的 build.gradle 中增加以下內容:
第二步是在要使用 Groovy 的專案 build.gradle 中增加 apply plugin: 'groovyx.android’ 的內容,讓 Gradle 可以正確地識別 Groovy 所寫的程式。

第三步要設定在封裝 Apk 的過程中被排除的檔案:
完成以上的工作就可以設定 Dependencies 以便在撰寫程式時可以引用到 Spock Framework 裡的 Class。但是光只有 Spock 也只能對一般的 Class 進行測試,如果要測試 Android 元件,牽涉到 Context 的問題,還是要使用官方的 Espresso 或是 Robolectric

Espresso 的問題不大,只要把 Java 的寫法改成 Groovy 套到 Spock 的 Class 結構裡就行了。Robolectric 則是要改為引用另一個套件:Robospock,依照 RoboSpock 的官方文件的說明,就可以順利的使用 Spock 來測試 Android 的元件。RoboSpock 就已經有引用 Robolectric 所以自己的 build.gradle 並不需要再設定一次。只是必須受限於 RoboSpock 建置時所設定的 Robolectric 版本,不一定能使用最新版的 Robolectric。

最後一個要注意的事項是,Espresso 在執行前是要經過實際封裝 Apk 的過程,所以 Groovy 在引用時要使用特別為 Android 開發的版本 - org.codehaus.groovy:groovy:2.4.7:grooid,否則很容易會出現超出 64K 的問題。

以下是完整的 build.gradle 的示範內容:
撰寫 Spock 測試前要先手動在 androidTest 或是 test 目錄下增加 groovy 的目錄,其下再依據所屬的 Package 產生路徑結構。

最後,由於相容性的關係,在編譯的過程中會持續出現以下的訊息,但實際操作時並沒有阻擋或影響到測試程式的執行,所以可以忽略、不用理會。
:module:transformClassesWithDexForDebugAndroidTest
AGPBI: {"kind":"error","text":"warning: Ignoring InnerClasses attribute for an anonymous inner class","sources":[{}]}
AGPBI: {"kind":"error","text":"(groovyjarjarantlr.TokenStreamRewriteEngine$1) that doesn\u0027t come with an","sources":[{}]}
AGPBI: {"kind":"error","text":"associated EnclosingMethod attribute. This class was probably produced by a","sources":[{}]}
AGPBI: {"kind":"error","text":"compiler that did not target the modern .class file format. The recommended","sources":[{}]}
AGPBI: {"kind":"error","text":"solution is to recompile the class from source, using an up-to-date compiler","sources":[{}]}
AGPBI: {"kind":"error","text":"and without specifying any \"-target\" type options. The consequence of ignoring","sources":[{}]}
AGPBI: {"kind":"error","text":"this warning is that reflective operations on this class will incorrectly","sources":[{}]}
AGPBI: {"kind":"error","text":"indicate that it is *not* an inner class.","sources":[{}]}
AGPBI: {"kind":"error","text":"warning: Ignoring InnerClasses attribute for an anonymous inner class","sources":[{}]}
AGPBI: {"kind":"error","text":"(groovyjarjarantlr.build.ANTLR$1) that doesn\u0027t come with an","sources":[{}]}
AGPBI: {"kind":"error","text":"associated EnclosingMethod attribute. This class was probably produced by a","sources":[{}]}
AGPBI: {"kind":"error","text":"compiler that did not target the modern .class file format. The recommended","sources":[{}]}
AGPBI: {"kind":"error","text":"solution is to recompile the class from source, using an up-to-date compiler","sources":[{}]}
AGPBI: {"kind":"error","text":"and without specifying any \"-target\" type options. The consequence of ignoring","sources":[{}]}
AGPBI: {"kind":"error","text":"this warning is that reflective operations on this class will incorrectly","sources":[{}]}
AGPBI: {"kind":"error","text":"indicate that it is *not* an inner class.","sources":[{}]}
AGPBI: {"kind":"error","text":"warning: Ignoring InnerClasses attribute for an anonymous inner class","sources":[{}]}
AGPBI: {"kind":"error","text":"(groovyjarjarantlr.debug.misc.ASTFrame$1) that doesn\u0027t come with an","sources":[{}]}
AGPBI: {"kind":"error","text":"associated EnclosingMethod attribute. This class was probably produced by a","sources":[{}]}
AGPBI: {"kind":"error","text":"compiler that did not target the modern .class file format. The recommended","sources":[{}]}
AGPBI: {"kind":"error","text":"solution is to recompile the class from source, using an up-to-date compiler","sources":[{}]}
AGPBI: {"kind":"error","text":"and without specifying any \"-target\" type options. The consequence of ignoring","sources":[{}]}
AGPBI: {"kind":"error","text":"this warning is that reflective operations on this class will incorrectly","sources":[{}]}
AGPBI: {"kind":"error","text":"indicate that it is *not* an inner class.","sources":[{}]}






2016/11/16

A Tutorial for FluxJava

This document will show you how to use FluxJava step by step. You can find all the sources in Github.

For someone may not familiar with Flux, here is a good place to start.

Start from BDD

In this document, I will demo the usage of FluxJava by developing a simple todo app. The process will start from a BDD flow.

First, we need to define our requirements. Below is the list of requirement for the simple todo app:
  • View todo list.
  • Switch list between different names.
  • Add a todo.
  • Close/Open a todo.
Then we need a story to describe these requirements, it should look like this:
Story: Manage todo items

Narrative:
As a user
I want to manage todo items
So I can track something to be done


Scenario 1: Add a todo
When I tap the add menu on main activity
Then I see the add todo screen

When I input todo detail and press ADD button
Then I see a new entry in list


Scenario 2: Add a todo, but cancel the action
When I tap the add menu on main activity
 And I press cancel button
Then Nothing happen


Scenario 3: Switch user
When I select a different user
Then I see the list changed


Scenario 4: Mark a todo as done
When I mark a todo as done
Then I see the todo has a check mark and strike through on title

Ok, since it is just a demo, the content of story has been simplified and it should be more complicate in the real cases.

Plan how to test

There will be one test class base on Espresso treated as UAT to verify the story, since I only wrote one story. For the non-Android component classes, they will be followed by a test class as unit test. The rest classes that base on Android component will be tested with Robolectric and treated as integration test.

In order to make the covert process from story to the test cases easier, I will use Spock Framework to replace JUnit. And Java will be replaced by Groovy, when writing the test cases.

Here is the dependencies for test:
  • Groovy
  • Spock Framework
  • RoboSpock
  • Espresso
RoboSpock is an Android testing framework which brings Spock Framework and Robolectric work together. It comes with Robolectric, so you don’t need to add the dependency of Robolectric again. But you also limited to use the version of Robolectric that RoboSpock works with.

Here is the sample for how to configure build.gradle with these frameworks:

Create the UI

After created a empty Android project, the first thing is modify MainActivity. In this Activity, we will need a RecyclerView and a Spinner to show the data of todos and users. The list of todo can be switch from Spinner. Here is the layout:

Create UAT

To contain the test files, we need to create a folder named groovy under androidTest and delete java one. If you make the build.gradle right, you will see the groovy folder with background color shows it is a test folder. Now, we can put our test file 'ManageTodoStory.groovy' into groovy folder with package structure.

Here, we can see the advantage brought by Spock, the codes almost the same with the origin story.
Then we can put the real test codes into the class as below:
There are some steps of resources creation have been skipped.

Create the bus

According to Flux pattern, dispatcher is the central hub to deliver messages. In FluxJava, it uses bus to replace the functionality of dispatcher. So, we have to create a bus instead of a  dispatcher.

There is a IFluxBus interface required by FluxJava when you create a bus. In this demo, I used EventBus from greenrobot to make the code simple. Here is the codes:
If you use Rx add-on of FluxJava, you won’t need to create a bus. There is a built-in bus class named RxBus.

Create the models

The models in this demo are two POJOs. They are used to contain single user and todo data. you can check the content of these two class from 'User.java' and 'Todo.java'.

Define the constants

This not the necessary step but it will make codes more clear. As you can see, there are two constants that point to user and todo data type. Each data type has it’s own actions base on the description from story.
DATA_USER and DATA_TODO will be used as key of StoreMap latter. In FluxJava, you can use any type of data as a key of StoreMap not just integer. It could be a string, a type of class, a instance of class.

Create the actions and stores

UserAction and TodoAction inherit from FluxAction directly. The type of their data property is List, so they can contain more then one item when they pushed by dispatcher. you can check the content of these two class from 'UserAction.java' and 'TodoAction.java'.

To create a store, you can inherit from FluxStore in FluxJava (for Rx add-on it will be RxStore) or implement IFluxStore. register and unregister of IFluxStore are provided for UI components to tell store that they need to get the event of data change.

If you have multi-instance of a store in you application, tag will be useful to distinct which instance of store you are relate to.

The methods getItem, findItem, getCount are basic operations for inquiry data to display. The reason of getItem not return a list is to avoid any list modification outside store. It still has possibility that the content of single item changed by non-store class. So, it recommends create a new instance before return from getItem method.
Now, we need to let store get actions from dispatcher. For EventBus case, you can add a method as below:
For RxStore, you need to overwrite onAction method:
The action constants defined in previous step are used here. In UserStore, for example, there is only one action - load user data. All the user data will prepared by ActionCreator in this demo and the data will be sent with action. What store need to do in USER_LOAD is put the data into list.

Then store can emit an event to tell listeners that data is ready. The events store need to emit are define inside the class in this demo.

Both EventBus and Rx case, it will need to put job into background thread to avoid block UI. In EventBus, I added an annotation attribute to specify working thread is background. In Rx, the job will be done by RxStore.

You don’t need to worry about the action type when action pass in. They will be filtered by the parameter type of method or getActionType method from RxStore. But you need to worry when there are more then one instance of store waiting to receive actions. In this case, you need to put tag information into actions before you send.

Below are the results of TodoStore for EventBus and Rx:
Two more cases were added into codes compare to UserStore. TODO_ADD is simply put the new item get from action into list and inform the listeners that list has been updated. TODO_CLOSE need to find the position of list due to getItem return a different instance. For demo purpose, I wrote a very simple loop to do the job. Once the item has been located, it will be replaced by new one and the listeners will be informed.

Create ActionHelper

ActionHelper is used to customize the process of action creation and the process will be done by ActionCreator in FluxJava.

First thing that ActionHelper can do is decide which type of action to create instance. Second part is translate the data format.

In above codes, the ActionHelper choose the type of action by the constants defined by previous step.

There are more jobs to be done in the second part of ActionHelper. In Flux documents said, you can make a remote call to get data in Actions Creator before create an action. To follow this, you can write the remote API calls in wrapData of ActionHelper.

Another usage of wrapData shows in above codes. It could be different format when get data from UI components. For example, it will be an integer when asking load user data, a string when asking load todo data, a todo item when modify todo. It is a good place in wrapData to change these forms into uniform one.

If you don’t like what ActionCreator in FluxJava did, you can inherit it and overwrite the methods. Then put your own ActionCreator instance into FluxContext when initializing it.

Put the components together

It’s time to put all things together. The Flux flow is cover all the application lifecycle in the demo, it is good choice put initialize jobs in Application class.

I wrote a AppConfig inherits from Application and modified the 'AndroidManifest.xml' to make it work. In AppConfig, there is a method call setupFlux as below:
How you put all components together is using builder inside FluxContext and pass the instance of classes that were created in previous steps.

Create the adapters

The adapters are used to feed data to Spinner and RecyclerView in MainActivity. They also play a role to connect the FluxJava framework. In this demo, they will be the views of Flux pattern.

I wrote two adapters UserAdapter and TodoAdapter. The former provides user data for Spinner to display. And the other provides todo data for RecyclerView. Before create these adapters, we need to create item layout first. Check the content in  'item_user.xml' and 'item_todo.xml'.

Here is complete UserAdapter:
The UserAdapter get the store instance in constructor. The first parameter of getStore is USER_DATA that was defined in 'Constants.java' and also a key of StoreMap. So FluxContext can get the store that we want.

The second one is tag for getting store instance, but we don’t use here, so pass null. The last one need pass a instance of view that want to get events from store. You also can do it by your self after get store instance using it’s register method.

In the rest part of UserAdapter, store keeps the data list for adapter, so UserAdapter just call store when it need.

There is a similar method with the stores in the end of UserAdapter. It utilize EventBus to get events too. In the same manner, the parameter type will limit the events passed in. By specify the annotation attribute, the execution back to the main thread in order to access UI components and only one thing need to do in this demo - notify Spinner that data has been changed.

You also need to put tag into events, if you have more than one store will emit events.

The last one method is used to release references that will avoid memory leak issue.

For Rx add-on, you won’t need to add a method to receive events, instead you can call  subscribe from the Observable that exposed by toObservable of RxStore.
The content of another adapter, TodoAdapter, show as below:
There are not much need to explain, just one method was added and it is used to get another event. It can make RecyclerView to distinct what to refresh, the whole list or just one item.

Want to work with Rx add-on? Like UserAdapter, just use subscribe instead of add methods. See the sources here.

Ok, we are ready to connect adapters with UI components. Here is the MainActivity:
As you see, nothing special when setup RecyclerView and Spinner. There will be a chain reaction from onStart. First, a request will be sent out to load user data. After user data loaded, Spinner will trigger a request to load todo data. Then you can see the data shows in the Spinner and RecyclerView.

Why did I put the request in onStart  I want to make sure the Activity get the most update data when it back to foreground, no matter it was changed or not.

The dispose method of Adapters are called in onStop of Activity to release resource before it close, then we will get free from memory leak issue.

Create integration test

To make apps more testable is one of the reasons I introduce Flux in my codes. When running a integration test, we don’t want to use real data. Isolate stores is an option when we make integration test.

In this demo, it shows how to archive this goal in FluxJava. Robolectric provides a convenient way to inject stub Application when a test start. So I wrote a StubAppConfig and specify in annotation attribute:
In StubAppConfig, I replaced the production classes with test ones. After did this, the data retrieved by UI components are from the test data source.
If you want to do the same thing in UAT or Espresso, you can replace the Application class too. But the different is you have to create a custom JUnitRunner as below:
The last step is specify the new JUnitRunner in Configurations of Android Studio:

When you run the UAT, you will see the different with the production one:
Production   Test
 

 

Create add todo feature

Let’s continue to enhance the app. It is a simple process for how to add a todo, just let user tap the add item in menu and show a AlertDialog to get data, then done. Obviously we need create a menu layout first:
Then put the menu into MainActivity:
I used DialogFragment to show the AlertDialog, so here is the layout:
And the codes:
Add codes in MainActivity to show the AlertDialog when user tap the menu:
After running the test, we know we finished the add todo feature, cool!

Create close todo feature

It’s the last feature we are not complete. To make it happen just back to TodoAdapter, modify onBindViewHolder as below:
Run the test, you will find all the cases passed. All done, have fun!



2016/11/12

FluxJava: 給 Java 使用的 Flux 函式庫

為何選擇 Flux


設計上遇到的問題

最初在接觸 Flux 時就有一種驚豔的感覺,長久以來在設計上所出現的困擾似乎出現了曙光。在 Flux 還沒有出現之前,MVx 系列 (MVC、MVP、MVVM) 的 Design Pattern 就一直引領風潮。這類型的 Design Pattern 成功地解決了特定的問題,但卻也形成了某些尾大不掉的隱憂。在畫面不多、顯示資訊單純的應用程式中問題不容易顯現,但隨著程式複雜度的昇高,設計上所隱含的矛盾也不住地增強。

MVx 系列的設計在概念上是一個畫面對應一種資料類型,畫面專責顯示與處理該類型的資料。很直覺、也很有效地把功能區分成一組、一組的單元。水能載舟亦能覆舟,正所謂成也蕭何、敗也蕭何。就是因為每一組 MVx 太過獨立、區隔性太強,當出現整合式畫面的需求時,會造成在設計上進退兩難的抉擇。

假設程式中有一個畫面叫 Dashboard,需要整合客戶、訂單、存貨的資料。試問,這時是要設計一個新的 Model 納入所有資料?還是打破規則讓一個 View 對應多個 Model?

有人也許會問:這是問題嗎?

如果只是期望程式能夠執行,那的確算不上是個問題。但是如果要考慮到程式碼的可維護性,就必須要維持在設計上的一致性,這點在程式愈複雜的情況下愈顯重要。否則就不需要搞什麼 Design Pattern,就隨性而為、讓一切都歸於渾沌就好了。

再舉另一個例子,假設要開發的是線上購物的訂單畫面,下單時要提供客戶資料、訂單資料、刷卡資料。依據之前的原則,所有的資訊都會被設計納在一個單一的 Model 內。當某一天高層突然下指令要把購物流程改成 Wizard 的方式,每個步驟各自獨立成一個畫面。試問在這樣的情況下,開發新畫面時是讓 Model 拆解成多個?還是維持原本的樣子?

如果要維持原本的樣子,由於每一組的 MVx 都是獨立的,如何傳遞 Model?誰要負責控制傳遞的順序?又該如何保留 Model 的狀態?好吧!那就拆開...

拆開之後,問題似乎解決了,但此時高層又說了,這個程式要跨平台,所以二種類型的畫面都要有...

Flux 所提供的效果

Flux 的架構則是打破這層膠著的狀態,在其單向資料流的原則之下,View 只要管顯示資料,不管資料的來源是一個還是多個。而被通知資料有異動時,也是依循相同的方式來獲取資料,刷新畫面。至於要如何異動資料與 View 無關,只要把異動的資訊傳出去,接著就像戰機上的飛彈一樣可以射後不理。

在這樣的設計之下,以之前 Dashboard 的例子,不管是單一的畫面負責顯示所有的資料,還是畫面上分割成許多不同的元件來分別顯示特定的資料,都不會有設計上的違和感。而另一個例子同樣也適用,無關後端的資料規劃方式,View 只要專注在選墿合適的資料來源、考量如何顯示資料上即可。

Flux 只能用在有 UI 的情境之下?不儘然,並不是只有人才會輸入或需要取得回應。在有明確的邊界之狀況下,像是網路或是因設計的考量所形成邏輯上的 Layer,這種可以用來把資料供給端及接收端做有效的分離,以便進行分工、測試等等作業的架構,都可以考慮套用 Flux 的概念。

如何實作

俗話說得好,知易行難。了解 Flux 的運作過程是一回事,但要把這些過程落實到設計之中、形成程式碼又是另外一回事。Facebook 並沒有為 Java 的開發環境開發一套符合 Flux 的函式庫,而 Java 的環境相較於 JavaScript 又更加地多元化,加大了使用上的不確定性。為了避免在開發上每次都要反覆進行類似的工作,於是就依據過去的工作經驗,利用抽象化的手法及自動生成的概念,實作了一個 Framework,讓想要在 Java 的專案中使用 Flux 的人可以輕易的上手。

接下來會針對這個 Framework 做個簡單的說明。

取得 Binary

最新版本的 Jar 檔可以在 Github 的 Release 頁面中下載。

設定

如果是使用 Gradle 來建構程式,則所下載到的檔案可以送到 build.gradle 設定參照的目錄下。如果是 Android 的專案,則是放到 libs 的目錄下即可。

在專案中有使用 fluxjava-rx 時,應該也會需要在 build.gradle 中增加以下的內容:

在使用之前

在 Github 的 Repository 中,FluxJava 的函式庫程式碼放在 fluxjava 的目錄下,並且在 demo-eventbus 目錄下搭配一個示範用的 Android 專案。這個示範的專案是一個只有單一 Activity 的簡易 Todo 應用程式。在這個 App 中可以展示以下的功能:
  • 顯示 Todo 清單
  • 在不同使用者間切換 Todo 清單
  • 新增 Todo
  • 關閉/重啟 Todo

在這個示範專案中使用 greenrobot 的 EventBus 來協助 Dispatcher 和 Store 發送訊息。

如果想要與 RxJava 搭配使用,可以看一下 fluxjava-rx 目錄,裡面有 FluxJava 為 RxJava 所開發的 Addon。同時,有一個與之配對的示範專案在 demo-rx 目錄下,是由 demo-eventbus 複製過來修改的。在這個示範專案中,原本的 EventBus 以 fluxjava-rx 所提供的 RxBus 取代。而基於 RxJava 1.x 函式庫的 RxBus 所提供的功能和 EventBus 的功能相同。

如何使用


準備工作

  • Bus
    Dispatcher 和 Store 會呼叫 Bus 來傳送訊息。Bus 必須要實作 IFluxBus 的介面,實作時可以使用任何的 Bus 方案,像是:Otto、EventBus,或是自行開發的方案。如果有同時引用 fluxjava-rx,則可以直接使用 RxBus 來提供傳送訊息的功能。
  • Action
    Dispatcher 使用 Action 來通知 Store 要進行的工作。在 Action 中有二個屬性,一個是 Type、一個是 Data。Type 用來讓 Store 識別要對資料進行的動作,Data 則是該動作的附屬資訊。以示範的專案來說,當一個新的 Todo 從介面上被傳進來,則新 Todo 的內容會被放在 Data 欄位中。
  • ActionHelper
    ActionHelper 協助 ActionCreator 決定產生何種 Action,並且協助 ActionCreator 將目前傳進來的資料格式轉成可被處理的格式。
  • Store
    Store 負責截收由 Dispatcher 所送出的 Action,並根據 Action 上的資訊進行對應的資料處理。當資料處理完成,Store 會再送出一個資料異動的事件,讓事件的接收者可用以反應新的資料狀態。
  • StoreMap
    StoreMap 是一個一對一的對照表,在 Framework 中使用這一個對照表來產生需要的 Store Instance。假設 Action 和 Store 的關係是一對一的,則 Action 的型別可以用來做為 Store 型別的鍵值。像是在示範的專案中可以看到有二個 Action,分別是 UserAction 及 TodoAction,並且各自會對應到一個 Store 的型別。因此,與 TodoAction 配對的 TodoStore 就會被產生來負責處理與 Todo 相關的資料要求。

初始化程序

在 FluxJava 中,FluxContext 是用來做為整個程序開始的進入點。FluxContext 被設計成 Singleton,負責整合 Framework 中相關的元件,並且管理特定元件的 Instance。

FluxContext 的 Instance 可以由其內含的 Builder 來建立,示範的程式碼如下:

開始發送要求

在取得使用者透過 UI 元件所輸入的資料後,接下來可以利用 ActionCreator 來推送 Action,ActionCreator 的 Instance 可經由 FluxContext 來取得。Framework 預設所提供的 ActionCreator 只有一項功能 sendRequest,呼叫的程式碼要傳入 Id 及使用者輸入的資料。其中,Id 是用來決定要產生的 Action 型別。使用者輸入的資料可以在呼叫 sendRequest 後,經由 ActionHelper 轉成 Store 所需的格式。

以下為示範的程式碼:
sendRequest 有提供二種版本的實作,同步和非同步。非同步的版本會先建立一個新的 Thread 之後,在新的 Thread 中執行。如果需要特別管控 Thread 的使用或是想要使用 Thread Pool,則可以呼叫同步的版本來達到目的。

進行資料處理

要進行資料處理需在 Store 中攔截指定的 Action,攔截的方法會依據所使用的 Bus 方案而不同。以示範專案的例子來說,要在 Store 中新增一個搭配特定 Annotation 的方法。相關的程式範例如下:
如果是使用 fluxjava-rx,則 Store 可以繼承自 RxStore,此時只要覆寫 RxStore 中的 onAction 方法即可。相關的程式範例如下:

反應資料異動

跟 Store 一樣,UI 元件要依據使用的 Bus 方案來接收由 Store 所發出的資料異動事件。在 EventBus 的例子中:
在 RxBus 的例子中: