サーバのモックが必要
リスト表示でデータが取って来れなかったとき,
正しくEmptyViewが表示されるかのテストを書くことになりました.
そこで,Daggerとretrofitを使って,サーバのレスポンスをテストコードから,
操作できるようにしました.こんな感じです.
@Test
public void testEmptyView_show() {
String empty_response =
"{\"photos\":{\"page\":1,\"pages\":25,\"perpage\":20,\"total\":500,\"photo\":[],\"stat\":\"ok\"}";
setupMockServer(empty_response);
// start Activity manually
MainActivity activity = activityRule.launchActivity(new Intent());
// check if empty view is visible
// ...
欲しいレスポンス(empty_response)を,今回作ったsetupMockServer(empty_response)でセットすると,
そのテスト中のAPIアクセスのレスポンスはempty_responseが返るようになってます.
そのことについて書きます.
サーバとの連携部分の確認
アプリのサーバとの連携部分はretrofitを使ってるので,こんな感じです.
public interface MyService {
@GET(...URL...)
void getPopularPhotos(@Query("page") int page, Callback<ListItem> cb);
@GET(...URL...)
void getPhotoInfo(@Query("photo_id") String id, Callback<PhotoInfo> cb);
//省略
}
MyServiceは,アプリのどこからでもアクセスできるよう
自作Applicationクラスにstaticで置いてます.
public class MyApplication extends Application {
private static RestAdapter rest_adapter =
new RestAdapter.Builder().setEndpoint(ServerURL.END_POINT).build();
public static MyService api = rest_adapter.create(MyService.class);
// 省略
}
では,Daggerを使って上のMyServiceフィールドを差し替えられるようにします.
Daggerで差し替えの仕組みづくり
Daggerでサーバ連携部分を差し替えられるように書き換えていきます.
まず,Applicationクラスを下のように書き換えて,
サーバー連携部分を外から差し込むようにします.
public class MyApplication extends Application {
@Inject MyService api;
// 省略
次は,差し替えで使う以下2つのModuleを作っていきます.
- サーバと連携する
RealAPIModule(アプリ内で使う) - 決められた値をレスポンスとして返す
DummyAPIModule(テストで使う)
サーバと連携するRealAPIModuleは,今まで使っていたものを@Providesで返しているだけです.
@Module(injects = MyApplication.class, library = true)
public class RealAPIModule {
@Provides @Singleton
public MyService provideMyService() {
return new RestAdapter.Builder()
.setEndpoint(ServerURL.END_POINT)
.build()
.create(MyService.class);
}
}
次は,設定した値を返すDummyAPIModuleです.
まず,指定した値を返すMockClientクラスの作成を行います.
ここでは,コンストラクタで受け取った文字列をレスポンスとして返すようしています.
public class MockClient implements Client{
private static final int HTTP_OK_STATUS = 200;
private final String RESPONSE_JSON;
public MockClient(String responce_json) { this.RESPONSE_JSON = responce_json; }
@Override
public Response execute(Request request) throws IOException {
return createResponseWithCodeAndJson(HTTP_OK_STATUS, RESPONSE_JSON);
}
private Response createResponseWithCodeAndJson(int responseCode, String json) {
return new Response("",responseCode, "nothing", Collections.EMPTY_LIST,
new TypedByteArray("application/json", json.getBytes()));
}
}
DummyAPIModuleはこうなります.
クライアントにMockClientを使うことで,「レスポンスをコントロール」しています.
public class DummyAPIModule {
public String mock_response;
public DummyAPIModule(String mock_response) { this.mock_response = mock_response; }
@Provides
@Singleton
public MyService provideMyService() {
return new RestAdapter
.Builder()
.setEndpoint(ServerURL.END_POINT)
.setClient(new MockClient(mock_response)) // set mock client !!
.build()
.create(MyService.class);
}
}
あとは,アプリとテストでModuleを切り替えるようににして完成です.
アプリとテストでサーバ連携部分を差し替え
方針はこうなります.
- 基本的にはサーバにアクセスする
RealAPIModuleを使う - テストの時だけ
DummyAPIModuleに切り替える.
まず,自作アプリケーションクラス MyApplication.javaを次のように書き換えます.
public class MyApplication extends Application {
private ObjectGraph objectGraph = null;
@Inject MyService api;
@Override
public void onCreate() {
super.onCreate();
if(objectGraph == null) {
List modules = Collections.singletonList(new RealAPIModule());
objectGraph = ObjectGraph.create(modules.toArray());
}
}
// used to replace ObjectGraph for test
public void setObjectGraph(ObjectGraph graph) { this.objectGraph = graph; }
public ObjectGraph getObjectGraph() { return objectGraph; }
public MyService api() { return api; }
}
次にベースアクティビティで,ModuleをInjectするように変えます.
これで「基本的にはサーバにアクセスする」ようになってます.
public class BaseActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((MyApplication)getApplication())
.getObjectGraph()
.inject(getApplication());
}
// 省略
テストでは,次のようにして
「テストの時だけDummyAPIModuleに切り替える」ようにします.
@Rule
public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(
MainActivity.class,
true, // initialTouchMode
false); // launchActivity. False so we can customize the intent per test method
private void setupMockServer(String response) {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
MyApplication app =
(MyApplication) instrumentation.getTargetContext().getApplicationContext();
// setup objectGraph to inject Mock API
List modules = Collections.singletonList(new DummyAPIModule(response));
ObjectGraph graph = ObjectGraph.create(modules.toArray());
app.setObjectGraph(graph);
app.getObjectGraph().inject(app);
}
@Test
public void testEmptyView_show() {
String empty_response =
"{\"photos\":{\"page\":1,\"pages\":25,\"perpage\":20,\"total\":500,\"photo\":[],\"stat\":\"ok\"}";
setupMockServer(empty_response);
// start Activity manually
MainActivity activity = activityRule.launchActivity(new Intent());
// check if empty view is visible
// 省略
ActivityTestRuleのおかげで,Activityは自動でスタートしません.
そのため,DummyModuleを差し込んでから,launchActivityを呼ぶことで
差し替えが実現できます.
終わりに
もっと簡単な方法がありそうですが,Daggerを使ってみたかったのでやってみました.
合わせてDIに関していろいろ調べたので,
いいなと思ったURLをいくつか参考URLに貼ります.
ありがとうございました.
参考URL
Dagger (公式 website)
Square Island : Dagger 2 + Espresso 2 + Mockito
Tasting Dagger 2 on Android | Fernando Cejas
