1. 程式人生 > >Android單元測試(五):網路介面測試

Android單元測試(五):網路介面測試

溫馨提示:如果你不太熟悉單元測試,可以先看下之前四篇基礎框架使用。便於你更好的理解下面的內容。

在平日的開發中,我們用後臺寫好給我們介面去獲取資料。雖然我們有一些請求介面的工具,可以快速的拿到返回資料。但是在一些異常情況的處理上就不太方便了。我列出以下幾個痛點:

  • 快速的檢視返回資料與資料的處理。(一般我們都是將寫好的程式碼跑到手機上,點選到對應頁面的對應按鈕)

  • 異常資訊的返回與處理。比如一個介面返回一個列表資料,如果列表為空呢?(找後臺給我們模擬資料)網速不好呢?(不知道怎麼搞…)網路異常呢?(關閉網路)

  • 後臺有部分介面沒有寫好,你就只能等他了。

不知道上面的這三點有沒有戳到你的痛處。如果扎心了,那麼老鐵你就有必要掌握今天的內容。

1.請求介面

我們就使用網路請求三件套(retrofit + okhttp + rxjava2)來舉例。

首先新增一下依賴,同時記得加網路許可權。

    //RxJava
    compile 'io.reactivex.rxjava2:rxjava:2.1.7'
    //RxAndroid
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    //okhttp
    compile "com.squareup.okhttp3:okhttp:3.9.1"
    //Retrofit
    compile ("com.squareup.retrofit2:retrofit:2.3.0"
){ exclude module: 'okhttp' } compile ("com.squareup.retrofit2:adapter-rxjava2:2.3.0"){ exclude module: 'rxjava' } compile "com.squareup.retrofit2:converter-gson:2.3.0"

測試介面:

public interface GithubApi {

    String BASE_URL = "https://api.github.com/";

    @GET("users/{username}"
) Observable<User> getUser(@Path("username") String username); }

Retrofit的初始化,我們使用LoggingInterceptor來列印返回資料。

public class GithubService {

    private static Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(GithubApi.BASE_URL)
            .client(getOkHttpClient())
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build();

    public static GithubApi createGithubService() {
        return retrofit.create(GithubApi.class);
    }

    private static OkHttpClient getOkHttpClient(){
        return new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())
                .build();
    }
}

測試程式碼:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class ResponseTest {

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        initRxJava2();
    }

    private void initRxJava2() {
        RxJavaPlugins.reset();
        RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });
        RxAndroidPlugins.reset();
        RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });
    }

    @Test
    public void getUserTest() {
        GithubService.createGithubService()
                .getUser("simplezhli")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<User>() {
                    @Override
                    public void onSubscribe(Disposable d) {}

                    @Override
                    public void onNext(User user) {
                        assertEquals("唯鹿", user.name);
                        assertEquals("http://blog.csdn.net/qq_17766199", user.blog);
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("Test", e.toString());
                    }

                    @Override
                    public void onComplete() {}
                });
    }
}

上面的程式碼中,因為網路請求是非同步的,所以我們直接測試是不能直接拿到資料,因此無法打印出Log以及測試。所以我們使用initRxJava2()方法將非同步轉化為同步。這樣我們就可以看到返回資訊。測試結果如下:

這裡寫圖片描述

上面的例子為了簡單直觀的說明,所以將請求介面的方法寫到了測試類中,實際中我們可以Mock方法所在的類直接呼叫請求方法。配合Robolectric對View控制元件的狀態進行測試。

如果你覺得每次測試都要加initRxJava2這段方法很麻煩,你可以抽象出來,或者使用@Rule

public class RxJavaRule implements TestRule {

    @Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                RxJavaPlugins.reset();
                RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
                    @Override
                    public Scheduler apply(Scheduler scheduler) throws Exception {
                        return Schedulers.trampoline();
                    }
                });
                RxAndroidPlugins.reset();
                RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
                    @Override
                    public Scheduler apply(Scheduler scheduler) throws Exception {
                        return Schedulers.trampoline();
                    }
                });

                base.evaluate();
            }
        };
    }
}

2.模擬資料

1.使用攔截器模擬資料

利用okhttp的攔截器模擬響應資料。

public class MockInterceptor implements Interceptor {

    private final String responseString; //你要模擬返回的資料

    public MockInterceptor(String responseString) {
        this.responseString = responseString;
    }

    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {

        Response response = new Response.Builder()
                .code(200)
                .message(responseString)
                .request(chain.request())
                .protocol(Protocol.HTTP_1_0)
                .body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
                .addHeader("content-type", "application/json")
                .build();
        return response;
    }
}

測試程式碼:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class MockGithubServiceTest {

    private GithubApi mockGithubService;

    @Rule
    public RxJavaRule rule = new RxJavaRule();

    @Before
    public void setUp() throws URISyntaxException {

        ShadowLog.stream = System.out;

        //定義Http Client,並新增攔截器
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())
                .addInterceptor(new MockInterceptor("json資料"))//<-- 新增攔截器
                .build();

        //設定Http Client
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(GithubApi.BASE_URL)
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();

        mockGithubService = retrofit.create(GithubApi.class);
    }

    @Test
    public void getUserTest() throws Exception {
        mockGithubService.getUser("weilu") //<-- 這裡傳入錯誤的使用者名稱
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<User>() {
                    @Override
                    public void onSubscribe(Disposable d) {}

                    @Override
                    public void onNext(User user) {
                        assertEquals("唯鹿", user.name);
                        assertEquals("http://blog.csdn.net/qq_17766199", user.blog);
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("Test", e.toString());
                    }

                    @Override
                    public void onComplete() {}
                });
    }

}

雖然我們傳入了錯誤的使用者名稱,但是我們模擬的響應資訊已經提前設定好了,所以測試結果不變。

利用這個思路,我們可以修改MockInterceptor的code,模擬404的情況。

這裡寫圖片描述

2.MockWebServer

MockWebServer是square出品的跟隨okhttp一起釋出,用來Mock伺服器行為的庫。MockWebServer能幫我們做的事情:

  • 可以設定http response的header、body、status code等。
  • 可以記錄接收到的請求,獲取請求的body、header、method、path、HTTP version。
  • 可以模擬網速慢的網路環境。
  • 提供Dispatcher,讓mockWebServer可以根據不同的請求進行不同的反饋。

新增依賴:

testCompile 'com.squareup.okhttp3:mockwebserver:3.9.1'

測試程式碼:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class MockWebServerTest {

    private GithubApi mockGithubService;
    private MockWebServer server;

    @Rule
    public RxJavaRule rule = new RxJavaRule();

    @Before
    public void setUp(){
        ShadowLog.stream = System.out;

        // 建立一個 MockWebServer
        server = new MockWebServer();

        //設定響應,預設返回http code是 200
        MockResponse mockResponse = new MockResponse()
                .addHeader("Content-Type", "application/json;charset=utf-8")
                .addHeader("Cache-Control", "no-cache")
                .setBody("{\"id\": 12456431, " +
                         " \"name\": \"唯鹿\"," +
                         " \"blog\": \"http://blog.csdn.net/qq_17766199\"}");

        MockResponse mockResponse1 = new MockResponse()
                .addHeader("Content-Type", "application/json;charset=utf-8")
                .setResponseCode(404)
                .throttleBody(5, 1, TimeUnit.SECONDS) //一秒傳遞5個位元組,模擬網速慢的情況
                .setBody("{\"error\": \"網路異常\"}");

        server.enqueue(mockResponse); //成功響應
        server.enqueue(mockResponse1);//失敗響應

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://" + server.getHostName() + ":" + server.getPort() + "/") //設定對應的Host與埠號
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();

        mockGithubService = retrofit.create(GithubApi.class);

    }

    @Test
    public void getUserTest() throws Exception {
        //請求不變
        mockGithubService.getUser("simplezhli")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<User>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }

                    @Override
                    public void onNext(User user) {
                        assertEquals("唯鹿", user.name);
                        assertEquals("http://blog.csdn.net/qq_17766199", user.blog);
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("Test", e.toString());
                    }

                    @Override
                    public void onComplete() {
                    }
                });

        //驗證我們的請求客戶端是否按預期生成了請求
        RecordedRequest request = server.takeRequest();
        assertEquals("GET /users/simplezhli HTTP/1.1", request.getRequestLine());
        assertEquals("okhttp/3.9.1", request.getHeader("User-Agent"));

        // 關閉服務
        server.shutdown();
    }
}

程式碼中的註釋寫的很清楚了,就不用過多的解釋了,使用起來非常的簡單。

執行結果(成功):

這裡寫圖片描述

失敗結果:

這裡寫圖片描述

預設情況下 MockWebServer 預置的響應是先進先出的。這樣可能對你的測試有限制,這時可以通過Dispatcher來處理,比如通過請求的路徑來選擇轉發。

        Dispatcher dispatcher = new Dispatcher() {

            @Override
            public MockResponse dispatch(RecordedRequest request) throws InterruptedException {

                if (request.getPath().equals("/users/simplezhli")){
                    return new MockResponse()
                            .addHeader("Content-Type", "application/json;charset=utf-8")
                            .addHeader("Cache-Control", "no-cache")
                            .setBody("{\"id\": 12456431, " +
                                    " \"name\": \"唯鹿\"," +
                                    " \"blog\": \"http://blog.csdn.net/qq_17766199\"}");
                } else {
                    return new MockResponse()
                            .addHeader("Content-Type", "application/json;charset=utf-8")
                            .setResponseCode(404)
                            .throttleBody(5, 1, TimeUnit.SECONDS) //一秒傳遞5個位元組
                            .setBody("{\"error\": \"網路異常\"}");
                }
            }
        };

        server.setDispatcher(dispatcher); //設定Dispatcher

通過上面的例子,是不是可以很好的解決你的痛點,希望對你有幫助。只要我們有和後臺有開發文件,約定好資料格式與欄位名,那麼我們可以更敏捷的去做我們的開發。本篇所有程式碼已上傳至Github。希望大家多多點贊支援!

3.參考