読者です 読者をやめる 読者になる 読者になる

φ(.. )メモシテオコウ android:MockWebServerでURLConnectionを使うクラスのテスト

android testing java

androidのアプリを作っていて、URLConnectionを使うクラスをどうやってテストしようかググっていてMockWebServerというものを見つけたのでめも。
AsyncTaskでonPostExecute()まで一通りテストする方法はQiitaで見つけましたので参考にさせてもらいましたm(_ _)m

これはオブジェクトを置き換えるとかではなくて本当にhttpサーバを起動して任意のデータを返すようなことができるようです。
サンプルコードによると一番単純なケースだと4行でOKという。

        MockWebServer server = new MockWebServer();
        server.enqueue(new MockResponse().setBody("hello world"));
        server.play();
        URL url = server.getUrl("/");

クラスをnewしてsetBody()で返すデータを作ってplay()でサーバを起動し、コンテンツのURLはgetUrl()で取得できるという感じです。
注意するのはgetUrl()とplay()の順番はplay()を先に呼んでおかないとだめという所でしょうか。

あと、httpサーバのポートはMockWebServerがランダムに決めていて、JavaDocを軽く見た限りでは任意のポートをセットするというのが出来なさそうでした。
そのためURLConnectionのインスタンスを作るときに必要なURLの文字列にポート番号を適切に入れてあげる必要があります。

さて、これを使ってテストをするんですが普通にサーバを起動させるとandroid.os.NetworkOnMainThreadExceptionが出てしまうのでandroid.os.AsyncTaskをextendsしたクラスを作ってそちらでサーバの起動を行うようにしました。
以下は適当なサンプルです。

このクラスはコンストラクタでリスナークラスを受け取るようにしてonPostExecute()が呼ばれたときにサーバのポートをリスナーを使って渡すようにしてます。

import java.io.IOException;

import android.os.AsyncTask;
import android.util.Log;

import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;

public class MockServerTask extends AsyncTask<String, Void, Long>
{
	
	MockWebServer server = null;
	WebServerSettingListener listener = null;
	
	public MockServerTask(WebServerSettingListener l)
	{
		this.listener = l;
	}
	
	@Override
	protected Long doInBackground(String... params) {
		String responseData = params[0];
				
		this.server = new MockWebServer();
		this.server.enqueue(new MockResponse().setBody(responseData));
		try {
			this.server.play();
		} catch (IOException e) {
			e.printStackTrace();
		}			
		
		return 0L;
	}
	
	@Override
	protected void onPostExecute(Long result) 
	{
		this.listener.serverReady(this.server.getPort());
	}
	
}

リスナーのインターフェースはこんな感じです。

public interface WebServerSettingListener 
{
	public void serverReady(int port);

}

そしてJUnitのテストクラスでこのインターフェースをimplementsします。
テストするクラスはHelloDownloaderで中でAsyncTaskをextendsしたクラスのインスタンスを作ってその中でURLConnectionを使っています。

setUp()の中でテスト用のhtmlファイルを読み込み(プロジェクトのaseetsディレクトリの下に配置)ます。次にMockServerTaskのインスタンスを作ってexecute()に先ほど読み込んだデータを渡しています。

public class TestHello extends ActivityInstrumentationTestCase2<MainActivity> 
implements WebServerSettingListener {

	private MainActivity activity = null;
	private DownloadFinishListener listener = null;
	HelloDownloader downloader = null;
	MockServerTask server = null;
	int port = 0;
	
	public TestHello()
	{
		super(MainActivity.class);
	}

	protected void setUp() throws Exception {
		super.setUp();
		this.activity = getActivity();
		
		InputStream is = getInstrumentation().getContext().getAssets().open("test.html");
		String data = HelloUtility.inputStreemToString(is);
		
		server = new MockServerTask(this);
		server.execute(data);
		
	}

	protected void tearDown() throws Exception {
		super.tearDown();
	}

	public void testDownload()
	{
		try {
			asyncTaskExecute(new DownloadFinishListener() {
				@Override
				public void onFinish(String result) {
					assertNotNull("Check result is null or not", result);
				}
			});
		} catch (Throwable e) {
			e.printStackTrace();
			fail();
		}
	}
	
	private void asyncTaskExecute(DownloadFinishListener l) throws Throwable 
	{
		this.listener = l;
		final CountDownLatch signal = new CountDownLatch(1);
		
		this.activity.runOnUiThread(new Runnable() {
			
			@Override
			public void run() {
				downloader = new HelloDownloader(activity, new DownloadFinishListener() {
					@Override
					public void onFinish(String result) {
						if (listener != null) {
							listener.onFinish(result);
						}
						signal.countDown();
						
					}
				});
			}
		});
		
		
		try {
            		signal.await();
        	} catch (InterruptedException e1) {
            		fail();
            		e1.printStackTrace();
        	}
	}
	
	@Override
	public void serverReady(int port) {
		this.port = port;
		downloader.setUrl("http://localhost:" + this.port + "/");
		downloader.download();
	}
	
}

serverReady()はポート番号を受け取ったらそれからURLを作成し、テスト対象のクラスに渡すようにしています。そのためテスト対象のクラスのインスタンスを作る所と使うところが離れています。。。
テスト対象クラスのインスタンスはasyncTaskExecute()で作ってserverReady()で使う形です。MockWebServer側の処理が終わってなくてサーバにサクセスできない場合もあり、何らかの方法でサーバの起動完了を待たないといけないというところです。
というかandroid.os.NetworkOnMainThreadExceptionを回避するのに頑張った感じですw