溫馨提示:如果你不太熟悉單元測試,可以先看下之前四篇基礎框架使用。便於你更好的理解下面的內容。
在平日的開發中,我們用後臺寫好給我們接口去獲取數據。雖然我們有一些請求接口的工具,可以快速的拿到返回數據。但是在一些異常情況的處理上就不太方便了。我列出以下幾個痛點:
快速的查看返回數據與數據的處理。(一般我們都是將寫好的代碼跑到手機上,點擊到對應頁面的對應按鈕)
異常信息的返回與處理。比如一個接口返回一個列表數據,如果列表爲空呢?(找後臺給我們模擬數據)網速不好呢?(不知道怎麼搞…)網絡異常呢?(關閉網絡)
後臺有部分接口沒有寫好,你就只能等他了。
不知道上面的這三點有沒有戳到你的痛處。如果扎心了,那麼老鐵你就有必要掌握今天的內容。
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。希望大家多多點贊支持!