【问题标题】:Square retrofit server mock for testing用于测试的方形改造服务器模拟
【发布时间】:2013-07-06 20:30:38
【问题描述】:

使用square retrofit framework 时模拟服务器进行测试的最佳方法是什么。

可能的方式:

  1. 创建一个新的改造 client 并将其设置在 RestAdapter.Builder().setClient() 中。这涉及解析 Request 对象并将 json 作为 Response 对象返回。

  2. 将此带注释的接口实现为模拟类,并使用它代替 RestAdapter.create() 提供的版本(不会测试 gson 序列化)

  3. ?

理想情况下,我希望模拟服务器提供 json 响应,以便我可以同时测试 gson 序列化。

任何示例将不胜感激。

【问题讨论】:

  • @JakeWharton,square-oss 的目的是什么?鉴于retrofit,这似乎是多余的。
  • @Alec Holmes:你的问题解决了吗?

标签: android testing mocking retrofit mockwebserver


【解决方案1】:

Mock Retrofit 2.0 测试请求

由于像创建 MockClient 类并从 Client 实现它这样的旧机制不再适用于 Retrofit 2.0,因此我在这里描述了一种新的方法。您现在需要做的就是为 OkHttpClient 添加自定义拦截器,如下所示FakeInterceptor 类只是覆盖 intercept 方法,如果应用程序处于 DEBUG 模式,则返回给定的 JSON。

RestClient.java

public final class RestClient {

    private static IRestService mRestService = null;

    public static IRestService getClient() {
        if(mRestService == null) {
            final OkHttpClient client = new OkHttpClient();
            // ***YOUR CUSTOM INTERCEPTOR GOES HERE***
            client.interceptors().add(new FakeInterceptor());

            final Retrofit retrofit = new Retrofit.Builder()
                            // Using custom Jackson Converter to parse JSON
                            // Add dependencies:
                            // com.squareup.retrofit:converter-jackson:2.0.0-beta2
                    .addConverterFactory(JacksonConverterFactory.create())
                            // Endpoint
                    .baseUrl(IRestService.ENDPOINT)
                    .client(client)
                    .build();

            mRestService = retrofit.create(IRestService.class);
        }
        return mRestService;
    }
}

IRestService.java

public interface IRestService {

    String ENDPOINT = "http://www.vavian.com/";

    @GET("/")
    Call<Teacher> getTeacherById(@Query("id") final String id);
}

FakeInterceptor.java

public class FakeInterceptor implements Interceptor { 
    // FAKE RESPONSES.
    private final static String TEACHER_ID_1 = "{\"id\":1,\"age\":28,\"name\":\"Victor Apoyan\"}";
    private final static String TEACHER_ID_2 = "{\"id\":1,\"age\":16,\"name\":\"Tovmas Apoyan\"}";

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = null;
        if(BuildConfig.DEBUG) {
            String responseString;
            // Get Request URI.
            final URI uri = chain.request().url().uri();
            // Get Query String.
            final String query = uri.getQuery();
            // Parse the Query String.
            final String[] parsedQuery = query.split("=");
            if(parsedQuery[0].equalsIgnoreCase("id") && parsedQuery[1].equalsIgnoreCase("1")) {
                responseString = TEACHER_ID_1;
            }
            else if(parsedQuery[0].equalsIgnoreCase("id") && parsedQuery[1].equalsIgnoreCase("2")){
                responseString = TEACHER_ID_2;
            }
            else {
                responseString = "";
            }

            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();
        }
        else {
            response = chain.proceed(chain.request());
        }

        return response;
    }
}

GitHub上的项目源代码

【讨论】:

  • 为了避免 UnsupportedOperationException 使用 OkHttpClient.Builder。 final OkHttpClient okHttpClient = new OkHttpClient.Builder() .addInterceptor(new FakeInterceptor()) .build();
  • 我有两个问题:1-在chain.request().uri() 下没有uri()(我通过String url = chain.request().url().toString(); 解决了这个问题,因为我的情况不同)。 2-我收到java.lang.IllegalStateException: network interceptor my.package.name.FakeInterceptor must call proceed() exactly once。我已将此添加到 addNetworkInterceptor() 而不是 addInterceptor()
  • 使用chain.request().url().uri();
  • 如何模拟 401 错误来测试 httpClient.authenticator 方法?只需将代码“401”验证方法不要调用。我该如何处理?
  • 我已经将伪造的拦截器方法用于模拟 web api 到一个新的水平,并为此发布了一个小库,使其更容易和更方便。见github.com/donfuxx/Mockinizer
【解决方案2】:

我决定尝试如下方法1

public class MockClient implements Client {

    @Override
    public Response execute(Request request) throws IOException {
        Uri uri = Uri.parse(request.getUrl());

        Log.d("MOCK SERVER", "fetching uri: " + uri.toString());

        String responseString = "";

        if(uri.getPath().equals("/path/of/interest")) {
            responseString = "JSON STRING HERE";
        } else {
            responseString = "OTHER JSON RESPONSE STRING";
        }

        return new Response(request.getUrl(), 200, "nothing", Collections.EMPTY_LIST, new TypedByteArray("application/json", responseString.getBytes()));
    }
}

并通过以下方式使用它:

RestAdapter.Builder builder = new RestAdapter.Builder();
builder.setClient(new MockClient());

它运行良好,允许您测试您的 json 字符串,而无需联系真实的服务器!

【讨论】:

  • 我已经更新了 Response 构造函数,因为旧的构造函数已被弃用,它在 Retrofit 1.4.1 中抛出了 IllegalArgumentException url == null
  • 还需要在builder中添加端点:builder.setEndpoint("http://mockserver.com").setClient(new MockClient());
  • 我已经扩展了上面的模拟客户端以根据 URL 请求从资产文件夹中的文件中获取响应。
  • Retrofit 2 现在使用 OkHttpClient 作为客户端层,这段代码不起作用。 ¿ 知道如何制作 OkHttpClient 模拟吗?可能这一切都是关于扩展和覆盖它,但我不确定如何。
  • 您能否根据 Retrofit2 更新您的答案?谢谢
【解决方案3】:

测试对象的 JSON 反序列化(可能使用 TypeAdapters?)似乎是一个单独的问题,需要单独的单元测试。

我个人使用版本 2。它提供了可以轻松调试和更改的类型安全、重构友好的代码。毕竟,如果您不创建 API 的替代版本进行测试,那么将您的 API 声明为接口有什么好处!多态性的胜利。

另一种选择是使用 Java Proxy。这实际上是 Retrofit(当前)实现其底层 HTTP 交互的方式。诚然,这需要更多的工作,但会允许更多的动态模拟。

【讨论】:

  • 这也是我的首选方式。如上所述调试要简单得多,然后必须直接处理响应体。 @alec 如果要测试 GSON 序列化,请生成/读入 json 字符串并使用 gson 对象进行反序列化。无论如何,我相信这就是 Retrofit 所做的。
  • @JakeWharton 你能提供一个简短的例子来说明这会是什么样子吗?我无法想象这个...谢谢!
【解决方案4】:

您还可以使用 Squareup 的 Webservermock 之类的东西! --> https://github.com/square/okhttp/tree/master/mockwebserver

【讨论】:

  • 是的 mockwebserver 很棒!我在其上构建了一个小型库,以使模拟 api 更加容易:Mockinizer
【解决方案5】:

我是Apiary.io 的忠实拥护者,因为我在迁移到真实服务器之前模拟了一个 API。

您还可以使用平面 .json 文件并从文件系统中读取它们。

您还可以使用可公开访问的 API,例如 Twitter、Flickr 等。

这里有一些其他关于改造的重要资源。

幻灯片:https://docs.google.com/presentation/d/12Eb8OPI0PDisCjWne9-0qlXvp_-R4HmqVCjigOIgwfY/edit#slide=id.p

视频:http://www.youtube.com/watch?v=UtM06W51pPw&feature=g-user-u

示例项目:https://github.com/dustin-graham/ucad_twitter_retrofit_sample

【讨论】:

    【解决方案6】:

    Mockery(免责声明:我是作者)专为这项任务而设计。

    Mockery 是一个模拟/测试库,专注于验证网络层,内置对 Retrofit 的支持。它根据给定 Api 的规范自动生成 JUnit 测试。这个想法是不必手动编写任何测试;既没有实现模拟服务器响应的接口。

    【讨论】:

      【解决方案7】:
      1. 首先,创建你的 Retrofit 界面。

        public interface LifeKitServerService {
            /**
             * query event list from server,convert Retrofit's Call to RxJava's Observerable
             *
             * @return Observable<HttpResult<List<Event>>> event list from server,and it has been convert to Obseverable
             */
            @GET("api/event")
            Observable<HttpResult<List<Event>>> getEventList();
        }
        
      2. 您的请求者如下:

        public final class HomeDataRequester {
            public static final String TAG = HomeDataRequester.class.getSimpleName();
            public static final String SERVER_ADDRESS = BuildConfig.DATA_SERVER_ADDR + "/";
            private LifeKitServerService mServerService;
        
            private HomeDataRequester() {
                OkHttpClient okHttpClient = new OkHttpClient.Builder()
                        //using okhttp3 interceptor fake response.
                        .addInterceptor(new MockHomeDataInterceptor())
                        .build();
        
                Retrofit retrofit = new Retrofit.Builder()
                        .client(okHttpClient)
                        .baseUrl(SERVER_ADDRESS)
                        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                        .addConverterFactory(GsonConverterFactory.create(new Gson()))
                        .build();
        
                //using okhttp3 inteception to fake response.
                mServerService = retrofit.create(LifeKitServerService.class);
        
                //Second choice,use MockRetrofit to fake data.
                //NetworkBehavior behavior = NetworkBehavior.create();
                //MockRetrofit mockRetrofit = new MockRetrofit.Builder(retrofit)
                //        .networkBehavior(behavior)
                //        .build();
                //mServerService = new MockLifeKitServerService(
                //                    mockRetrofit.create(LifeKitServerService.class));
            }
        
            public static HomeDataRequester getInstance() {
                return InstanceHolder.sInstance;
            }
        
            public void getEventList(Subscriber<HttpResult<List<Event>>> subscriber) {
                mServerService.getEventList()
                        .subscribeOn(Schedulers.io())
                        .unsubscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(subscriber);
            }
        }
        
      3. 如果你使用第二种选择(使用Retrofit接口来Mock服务器数据),你需要MockRetrofit,使用代码如下:

        public final class MockLifeKitServerService implements LifeKitServerService {
        public static final String TAG = MockLifeKitServerService.class.getSimpleName();
        private BehaviorDelegate<LifeKitServerService> mDelegate;
        private Gson mGson = new Gson();
        
        public MockLifeKitServerService(BehaviorDelegate<LifeKitServerService> delegate) {
            mDelegate = delegate;
        }
        
        @Override
        public Observable<HttpResult<List<Event>>> getEventList() {
            List<Event> eventList = MockDataGenerator.generateEventList();
            HttpResult<List<Event>> httpResult = new HttpResult<>();
            httpResult.setCode(200);
            httpResult.setData(eventList);
        
            LogUtil.json(TAG, mGson.toJson(httpResult));
        
            String text = MockDataGenerator.getMockDataFromJsonFile("server/EventList.json");
            if (TextUtils.isEmpty(text)) {
                text = mGson.toJson(httpResult);
            }
            LogUtil.d(TAG, "Text:\n" + text);
        
            text = mGson.toJson(httpResult);
        
            return mDelegate.returningResponse(text).getEventList();
        }
        

      4.我的数据来自资产文件(Asset/server/EventList.json),这个文件内容是:

          {
            "code": 200,
            "data": [
              {
                "uuid": "e4beb3c8-3468-11e6-a07d-005056a05722",
                "title": "title",
                "image": "http://image.jpg",
                "goal": 1500000,
                "current": 51233,
                "hot": true,
                "completed": false,
                "createdAt": "2016-06-15T04:00:00.000Z"
              }
            ]
          }
      

      5.如果使用okhttp3拦截器,需要自定义拦截器,如下:

      public final class MockHomeDataInterceptor implements Interceptor {
          public static final String TAG = MockHomeDataInterceptor.class.getSimpleName();
      
          @Override
          public Response intercept(Chain chain) throws IOException {
              Response response = null;
      
              String path = chain.request().url().uri().getPath();
              LogUtil.d(TAG, "intercept: path=" + path);
      
              response = interceptRequestWhenDebug(chain, path);
              if (null == response) {
                  LogUtil.i(TAG, "intercept: null == response");
                  response = chain.proceed(chain.request());
              }
              return response;
          }
      
          private Response interceptRequestWhenDebug(Chain chain, String path) {
              Response response = null;
              if (BuildConfig.DEBUG) {
                  Request request = chain.request();
                  if (path.equalsIgnoreCase("/api/event")) {
                      //get event list
                      response = getMockEventListResponse(request);
                  }
          }
      
          private Response getMockEventListResponse(Request request) {
              Response response;
      
              String data = MockDataGenerator.getMockDataFromJsonFile("server/EventList.json");
              response = getHttpSuccessResponse(request, data);
              return response;
          }
      
          private Response getHttpSuccessResponse(Request request, String dataJson) {
              Response response;
              if (TextUtils.isEmpty(dataJson)) {
                  LogUtil.w(TAG, "getHttpSuccessResponse: dataJson is empty!");
                  response = new Response.Builder()
                          .code(500)
                          .protocol(Protocol.HTTP_1_0)
                          .request(request)
                          //protocol&request be set,otherwise will be exception.
                          .build();
              } else {
                  response = new Response.Builder()
                          .code(200)
                          .message(dataJson)
                          .request(request)
                          .protocol(Protocol.HTTP_1_0)
                          .addHeader("Content-Type", "application/json")
                          .body(ResponseBody.create(MediaType.parse("application/json"), dataJson))
                          .build();
              }
              return response;
          }
      }
      

      6.最后,您可以使用代码请求您的服务器:

      mHomeDataRequester.getEventList(new Subscriber<HttpResult<List<Event>>>() {
          @Override
          public void onCompleted() {
      
          }
      
          @Override
          public void onError(Throwable e) {
              LogUtil.e(TAG, "onError: ", e);
              if (mView != null) {
                  mView.onEventListLoadFailed();
              }
          }
      
          @Override
          public void onNext(HttpResult<List<Event>> httpResult) {
              //Your json result will be convert by Gson and return in here!!!
          });
      }
      

      感谢阅读。

      【讨论】:

        【解决方案8】:

        除了@Alec 的答案之外,我还扩展了模拟客户端,以根据请求 URL 直接从资产文件夹中的文本文件中获取响应。

        @POST("/activate")
        public void activate(@Body Request reqdata, Callback callback);
        

        这里的模拟客户端知道被触发的 URL 是激活的,并在 assets 文件夹中查找名为 activate.txt 的文件。 它从 assets/activate.txt 文件中读取内容并将其作为 API 的响应发送。

        这是扩展的MockClient

        public class MockClient implements Client {
            Context context;
        
            MockClient(Context context) {
                this.context = context;
            }
        
            @Override
            public Response execute(Request request) throws IOException {
                Uri uri = Uri.parse(request.getUrl());
        
                Log.d("MOCK SERVER", "fetching uri: " + uri.toString());
        
                String filename = uri.getPath();
                filename = filename.substring(filename.lastIndexOf('/') + 1).split("?")[0];
        
                try {
                    Thread.sleep(2500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        
                InputStream is = context.getAssets().open(filename.toLowerCase() + ".txt");
                int size = is.available();
                byte[] buffer = new byte[size];
                is.read(buffer);
                is.close();
                String responseString = new String(buffer);
        
                return new Response(request.getUrl(), 200, "nothing", Collections.EMPTY_LIST, new TypedByteArray("application/json", responseString.getBytes()));
            }
        }
        

        详细解释可以查看我的博客
        http://www.cumulations.com/blogs/13/Mock-API-response-in-Retrofit-using-custom-clients

        【讨论】:

        • 嗨,当我使用 robolectric 编写测试类并使用模拟客户端模拟改造 api 时,它没有给我任何响应。你能指导我如何做到这一点。
        • 嗨 @Dory 确保您在资产文件夹中有 URL 结尾部分和文件名。例如,假设您的 URL 如下(在此处使用 Reftrofit)@POST("/redeemGyft") public void redeemGyft(@Body MposRequest reqdata, Callback callback);那么asset文件夹中对应的文件名就是redeemgyft.txt
        • 我在MockClient 文件中给出了静态文件名,使用robolectric 编写了一个测试类。但我无法从 json 文件中得到任何响应。
        • 如果您将文件保存在 assets 文件夹中,它应该会拾取它。
        【解决方案9】:

        现在使用 Mockinizer 可以更轻松地使用 Retrofit 模拟 api 调用,这使得使用 MockWebServer 变得非常简单:

        import com.appham.mockinizer.RequestFilter
        import okhttp3.mockwebserver.MockResponse
        
        val mocks: Map<RequestFilter, MockResponse> = mapOf(
        
            RequestFilter("/mocked") to MockResponse().apply {
                setResponseCode(200)
                setBody("""{"title": "Banana Mock"}""")
            },
        
            RequestFilter("/mockedError") to MockResponse().apply {
                setResponseCode(400)
            }
        
        )
        

        只需创建 RequestFilter 和 MockResponses 的映射,然后将其插入到 OkHttpClient 构建器链中:

        OkHttpClient.Builder()
                    .addInterceptor(loggingInterceptor)
                    .mockinize(mocks) // <-- just plug in your custom mocks here
                    .build()
        

        您不必担心配置 MockWebServer 等。只需添加您的模拟,其余的一切由 Mockingizer 为您完成。

        (免责声明:我是Mockinizer的作者)

        【讨论】:

          【解决方案10】:

          JSONPlaceholder:用于测试和原型设计的假在线 REST API

          https://jsonplaceholder.typicode.com/

          ReqresIn:另一个在线 REST API

          https://reqres.in/

          邮递员模拟服务器

          如果你想测试自定义的响应负载,上面两个可能不适合你的要求,那么你可以试试 postman mock server。 它很容易设置并且可以灵活地定义您自己的请求和响应负载。

          https://learning.getpostman.com/docs/postman/mock_servers/intro_to_mock_servers/ https://youtu.be/shYn3Ys3ygE

          【讨论】:

            【解决方案11】:

            对我来说,自定义 Retrofit Client 非常棒,因为它具有灵活性。特别是当您使用任何 DI 框架时,您可以快速简单地打开/关闭模拟。我也在单元和集成测试中使用 Dagger 提供的自定义客户端。

            编辑: 在这里你可以找到模拟改造的例子 https://github.com/pawelByszewski/retrofitmock

            【讨论】:

              猜你喜欢
              • 2016-04-27
              • 2016-01-09
              • 1970-01-01
              • 1970-01-01
              • 2019-07-20
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2021-05-14
              相关资源
              最近更新 更多