Square Retrofit Server Mock zum Testen


97

Was ist der beste Weg, um einen Server zum Testen zu verspotten, wenn das Square Retrofit Framework verwendet wird ? .

Mögliche Wege:

  1. Erstellen Sie ein neues Nachrüst - Client und stellen Sie ihn in der RestAdapter.Builder (). SetClient (). Dies beinhaltet das Parsen des Anforderungsobjekts und das Zurückgeben des JSON als Antwortobjekt.

  2. Implementieren Sie diese mit Anmerkungen versehene Schnittstelle als Scheinklasse und verwenden Sie diese anstelle der von RestAdapter.create () bereitgestellten Version (testet die gson-Serialisierung nicht).

  3. ?

Idealerweise möchte ich, dass der verspottete Server JSON-Antworten bereitstellt, damit ich gleichzeitig die Gson-Serialisierung testen kann.

Alle Beispiele wäre sehr dankbar.


@ JakeWharton, wozu dient das square-oss? Es scheint überflüssig gegeben retrofit.
Charles

@Alec Holmes: Hast du dein Problem gelöst?
AndiGeeky

Antworten:


104

Mock Retrofit 2.0-Testanforderungen

Da die alten Mechanismen wie das Erstellen MockClientund Implementieren von Klassen Clientmit Retrofit 2.0 nicht mehr funktionieren, beschreibe ich hier eine neue Methode. Alles, was Sie jetzt tun müssen, ist, Ihre benutzerdefinierten Interceptors für OkHttpClient hinzuzufügen, wie unten gezeigt . FakeInterceptorKlasse überschreibt nur interceptMethode und in dem Fall, wenn Anwendung in istDEBUG Modus befindet, wird JSON zurückgegeben.

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;
    }
}

Quellcode des Projekts auf GitHub


9
Verwenden Sie OkHttpClient.Builder, um UnsupportedOperationException zu vermeiden. final OkHttpClient okHttpClient = neuer OkHttpClient.Builder () .addInterceptor (neuer FakeInterceptor ()) .build ();
John

4
Zwei Probleme, die ich habe: 1- Es gibt kein uri()Unter chain.request().uri()(ich habe dies behoben, indem String url = chain.request().url().toString();mein Fall anders war). 2- Ich bekomme java.lang.IllegalStateException: network interceptor my.package.name.FakeInterceptor must call proceed() exactly once. Ich habe dies addNetworkInterceptor()eher hinzugefügt als addInterceptor().
Hesam

2
benutze chain.request (). url (). uri ();
Amol Gupta

Wie kann ich einen 401-Fehler verspotten, um die httpClient.authenticator-Methode zu testen? durch einfaches Setzen des Codes "401" Authentifizierungsmethode nicht aufrufen. Wie kann ich damit umgehen?
Mahdi

Ich habe den Fake-Interceptor-Ansatz zum Verspotten von Web-Apis auf die nächste Ebene gebracht und eine kleine Bibliothek veröffentlicht, um dies noch einfacher und bequemer zu machen. Siehe github.com/donfuxx/Mockinizer
donfuxx

85

Ich habe mich entschlossen, Methode 1 wie folgt auszuprobieren

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()));
    }
}

Und mit:

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

Es funktioniert gut und ermöglicht es Ihnen, Ihre JSON-Zeichenfolgen zu testen, ohne den realen Server kontaktieren zu müssen!


Ich habe den Antwortkonstruktor aktualisiert, der verwendet wurde, da der alte veraltet war und IllegalArgumentException url == nullmit Retrofit 1.4.1 einen ausgelöst hat.
Dan J

1
Außerdem muss dem Builder ein Endpunkt builder.setEndpoint("http://mockserver.com").setClient(new MockClient());
hinzugefügt werden

Ich habe den obigen Mock-Client erweitert, um die Antwort abhängig von der URL-Anforderung aus einer Datei im Asset-Ordner abzurufen.
Praveena_kd

21
Retrofit 2 verwendet jetzt OkHttpClient für die Client-Schicht, und dieser Code funktioniert nicht. ¿Irgendeine Idee, wie man einen OkHttpClient-Mock macht? Wahrscheinlich geht es darum, es zu erweitern und zu überschreiben, aber ich bin mir nicht sicher, wie.
GuillermoMP

1
Können Sie Ihre Antwort auch basierend auf Retrofit2 aktualisieren? danke
Hesam

20

Das Testen der JSON-Deserialisierung für Ihre Objekte (vermutlich mit TypeAdapters?) Scheint ein separates Problem zu sein, das separate Komponententests erfordert.

Ich benutze Version 2 persönlich. Es bietet typsicheren, refaktorfreundlichen Code, der leicht debuggt und geändert werden kann. Was nützt es schließlich, Ihre API als Schnittstellen zu deklarieren, wenn Sie keine alternativen Versionen davon zum Testen erstellen! Polymorphismus für den Sieg.

Eine andere Option ist die Verwendung von Java Proxy. Auf diese Weise implementiert Retrofit (derzeit) die zugrunde liegende HTTP-Interaktion. Dies wird zwar mehr Arbeit erfordern, würde aber viel dynamischere Verspottungen ermöglichen.


Dies ist auch mein bevorzugter Weg. Es ist viel einfacher, wie oben angegeben zu debuggen, als sich so direkt mit dem Antworttext zu befassen. @alec Wenn Sie die GSON-Serialisierung testen möchten, generieren / lesen Sie eine JSON-Zeichenfolge ein und verwenden Sie ein Gson-Objekt zum Deserialisieren. Unter dem Kopf glaube ich, dass Retrofit das sowieso tut.
Loeschg

@JakeWharton Können Sie ein kurzes Beispiel dafür geben, was dies möchte? Ich habe Probleme, mir das vorzustellen ... Danke!
Onkel_tex



8

Ich bin ein großer Fan von Apiary.io weil eine API verspottet habe, bevor auf einen echten Server .

Sie können auch flache .json-Dateien verwenden und diese aus dem Dateisystem lesen.

Sie können auch öffentlich zugängliche APIs wie Twitter, Flickr usw. verwenden.

Hier sind einige andere großartige Ressourcen zu Retrofit.

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

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

Beispielprojekt: https://github.com/dustin-graham/ucad_twitter_retrofit_sample


7

Spott (Haftungsausschluss: Ich bin der Autor) wurde genau für diese Aufgabe entwickelt.

Mockery ist eine Verspottungs- / Testbibliothek, die sich auf die Validierung von Netzwerkebenen mit integrierter Unterstützung für Retrofit konzentriert. Es generiert automatisch JUnit-Tests basierend auf den Spezifikationen einer bestimmten API. Die Idee ist, keinen Test manuell schreiben zu müssen; Weder Implementieren von Schnittstellen zum Verspotten von Serverantworten.


7
  1. Erstellen Sie zunächst Ihre Nachrüstschnittstelle.

    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. Ihr Antragsteller folgt:

    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. Wenn Sie die zweite Option verwenden (Retrofit-Schnittstelle zum Verspotten von Serverdaten verwenden), müssen Sie MockRetrofit verwenden. Verwenden Sie den folgenden Code:

    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. Meine Daten stammen aus einer Asset-Datei (Asset / Server / EventList.json). Der Inhalt dieser Datei lautet:

    {
      "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.Wenn Sie den Okhttp3-Interceptor verwenden, müssen Sie den Interceptor wie folgt selbst definieren:

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. Schließlich können Sie Ihren Server mit Code anfordern:

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!!!
    });
}

Danke fürs Lesen.


5

Durch Hinzufügen zur Antwort von @Alec habe ich den Mock-Client erweitert, um die Antwort abhängig von der Anforderungs-URL direkt aus einer Textdatei im Asset-Ordner abzurufen.

Ex

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

Hier versteht der Mock-Client, dass die URL, die ausgelöst wird, aktiviert ist, und sucht im Ordner "Assets" nach einer Datei mit dem Namen "activ.txt". Es liest den Inhalt aus der Datei assets / enabled.txt und sendet ihn als Antwort für die API.

Hier ist die erweiterte 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()));
    }
}

Eine ausführliche Erklärung finden Sie in meinem Blog unter
http://www.cumulations.com/blogs/13/Mock-API-response-in-Retrofit-using-custom-clients


Hallo, wenn ich eine Testklasse mit Robolectric und Mock Client schreibe, um die Nachrüst-API zu verspotten, gibt es mir keine Antwort. Könnten Sie mir zeigen, wie das geht?
Dory

Hallo @Dory, stellen Sie sicher, dass Sie den URL-Endteil und den Dateinamen im Assets-Ordner haben. Angenommen, Ihre URL lautet wie folgt (hier mit Reftrofit) @POST ("/ redeemGyft") public void redeemGyft (@Body MposRequest-Anforderungsdaten, Rückruf <RedeemGyftResponse> Rückruf); dann ist der entsprechende Dateiname im Asset-Ordner redeemgyft.txt
praveena_kd

Ich habe in meiner MockClientDatei einen statischen Dateinamen angegeben und eine Testklasse mit robolectric geschrieben. Ich kann jedoch keine Antwort von der JSON-Datei erhalten.
Dory

Wenn Sie die Datei im Ordner "Assets" aufbewahrt haben, sollte sie abgeholt werden.
Praveena_kd

1

JSONPlaceholder: Gefälschte Online-REST-API zum Testen und Prototyping

https://jsonplaceholder.typicode.com/

ReqresIn: Eine weitere Online-REST-API

https://reqres.in/

Postbote Mock Server

Wenn Sie angepasste Antwortnutzdaten testen möchten, entsprechen die beiden oben genannten möglicherweise nicht Ihren Anforderungen. Dann können Sie den Postman-Mock-Server ausprobieren. Es ist recht einfach einzurichten und flexibel, um Ihre eigenen Anforderungs- und Antwortnutzdaten zu definieren.

Geben Sie hier die Bildbeschreibung ein https://learning.getpostman.com/docs/postman/mock_servers/intro_to_mock_servers/ https://youtu.be/shYn3Ys3ygE


1

Das Verspotten von API-Aufrufen mit Retrofit ist mit Mockinizer jetzt noch einfacher, was die Arbeit mit MockWebServer wirklich einfach macht:

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)
    }

)

Erstellen Sie einfach eine Karte mit RequestFilter und MockResponses und fügen Sie sie dann in Ihre Builder-Kette OkHttpClient ein:

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

Sie müssen sich nicht um die Konfiguration von MockWebServer usw. kümmern. Fügen Sie einfach Ihre Mocks hinzu, der Rest wird von Mockinizer für Sie erledigt.

(Haftungsausschluss: Ich bin der Autor von Mockinizer)


0

Für mich ist der benutzerdefinierte Retrofit-Client aufgrund seiner Flexibilität großartig. Insbesondere wenn Sie ein DI-Framework verwenden, können Sie das Mock schnell und einfach ein- und ausschalten. Ich verwende den von Dagger bereitgestellten benutzerdefinierten Client auch für Unit- und Integrationstests.

Bearbeiten: Hier finden Sie ein Beispiel für eine spöttische Nachrüstung https://github.com/pawelByszewski/retrofitmock

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.