Maquette de serveur Square Retrofit pour les tests

97

Quelle est la meilleure façon de simuler un serveur à des fins de test lors de l'utilisation du framework Square Retrofit .

Moyens potentiels:

  1. Créez un nouveau client de retrofit et définissez-le dans RestAdapter.Builder (). SetClient (). Cela implique l'analyse de l'objet Request et le retour du json en tant qu'objet Response.

  2. Implémentez cette interface annotée en tant que classe fictive et utilisez-la à la place de la version fournie par RestAdapter.create () (ne testera pas la sérialisation gson)

  3. ?

Idéalement, je veux que le serveur fictif fournisse des réponses json afin que je puisse tester la sérialisation gson en même temps.

Tous les exemples seraient grandement appréciés.

Alec Holmes
la source
@JakeWharton, quel est le but square-oss? Cela semble redondant donné retrofit.
Charles
@Alec Holmes: Avez-vous résolu votre problème?
AndiGeeky

Réponses:

104

Demandes de test Mock Retrofit 2.0

Comme les anciens mécanismes comme la création de MockClientclasse et son implémentation à partir de Clientne fonctionnent plus avec Retrofit 2.0, je décris ici une nouvelle façon de faire cela. Tout ce que vous devez faire maintenant est d' ajouter vos intercepteurs personnalisés pour OkHttpClient comme indiqué ci-dessous . FakeInterceptorclass remplace simplement la interceptméthode et dans le cas où l'application est en DEBUGmode, retourne le 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;
    }
}

Code source du projet sur GitHub

Victor Apoyan
la source
10
Pour éviter UnsupportedOperationException, utilisez OkHttpClient.Builder. OkHttpClient final okHttpClient = new OkHttpClient.Builder () .addInterceptor (new FakeInterceptor ()) .build ();
John
4
Deux problèmes que j'ai: 1- Il n'y a pas de uri()sous chain.request().uri()(j'ai corrigé cela par String url = chain.request().url().toString();mon cas différent). 2- Je reçois java.lang.IllegalStateException: network interceptor my.package.name.FakeInterceptor must call proceed() exactly once. J'ai ajouté ceci à addNetworkInterceptor()plutôt que addInterceptor().
Hesam
2
utilisez chain.request (). url (). uri ();
Amol Gupta
Comment puis-je simuler l'erreur 401 pour tester la méthode httpClient.authenticator? en mettant simplement le code "401", la méthode d'authentification n'appelle pas. comment puis-je gérer cela?
Mahdi
J'ai pris la fausse approche des intercepteurs pour se moquer des apis Web à un niveau supérieur et publié une petite bibliothèque pour que cela le rende encore plus facile et plus pratique. Voir github.com/donfuxx/Mockinizer
donfuxx
85

J'ai décidé d'essayer la méthode 1 comme suit

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

Et en l'utilisant par:

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

Cela fonctionne bien et vous permet de tester vos chaînes json sans avoir à contacter le vrai serveur!

Alec Holmes
la source
J'ai mis à jour le constructeur Response utilisé car l'ancien était obsolète, qui lançait un IllegalArgumentException url == nullavec Retrofit 1.4.1.
Dan J
1
Il faut également ajouter un point de terminaison au constructeur:builder.setEndpoint("http://mockserver.com").setClient(new MockClient());
codeprogression
J'ai étendu le client fictif ci-dessus pour récupérer la réponse à partir d'un fichier dans le dossier d'actifs en fonction de la demande d'URL.
praveena_kd
21
Retrofit 2 utilise maintenant OkHttpClient pour la couche client, et ce code ne fonctionne pas. ¿Une idée de comment faire une maquette OkHttpClient? Il s'agit probablement de l'étendre et de le remplacer, mais je ne sais pas comment.
GuillermoMP
1
Pouvez-vous également mettre à jour votre réponse en fonction de Retrofit2? merci
Hesam
20

Tester la désérialisation JSON sur vos objets (probablement avec TypeAdapters?) Semble être un problème distinct qui nécessite des tests unitaires séparés.

J'utilise personnellement la version 2. Il offre un code de type sécurisé, convivial pour le refactor, qui peut être facilement débogué et modifié. Après tout, à quoi bon déclarer votre API en tant qu'interfaces si vous n'en créez pas d'autres versions pour les tester! Polymorphisme pour la victoire.

Une autre option consiste à utiliser un Java Proxy. C'est en fait ainsi que Retrofit implémente (actuellement) son interaction HTTP sous-jacente. Cela nécessitera certes plus de travail, mais permettrait des simulations beaucoup plus dynamiques.

Jake Wharton
la source
C'est aussi ma méthode préférée. Il est beaucoup plus simple de déboguer comme indiqué ci-dessus, puis de traiter directement le corps de la réponse. @alec Si vous souhaitez tester la sérialisation GSON, générez / lisez une chaîne json et utilisez un objet gson pour désérialiser. Sous la tête, je crois que c'est de toute façon ce que fait Retrofit.
loeschg
@JakeWharton Pourriez-vous donner un petit exemple de ce que cela voudrait? J'ai du mal à visualiser ça ... Merci!
uncle_tex
1
@uncle_tex Jetez un œil sur github.com/JakeWharton/u2020/blob/master/src/internalDebug/java/…
riwnodennyk
8

Je suis un grand fan d' Apiary.io pour se moquer d'une API avant de passer à un vrai serveur.

Vous pouvez également utiliser des fichiers .json plats et les lire à partir du système de fichiers.

Vous pouvez également utiliser des API accessibles au public comme Twitter, Flickr, etc.

Voici quelques autres excellentes ressources sur la modernisation.

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

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

Exemple de projet: https://github.com/dustin-graham/ucad_twitter_retrofit_sample

jpotts18
la source
7

La moquerie (avertissement: je suis l'auteur) a été conçue précisément pour cette tâche.

Mockery est une bibliothèque de simulation / test axée sur la validation des couches réseau avec prise en charge intégrée de la modernisation. Il génère automatiquement des tests JUnit basés sur les spécifications d'une API donnée. L'idée est de ne pas avoir à écrire manuellement de test; ni mettre en œuvre des interfaces pour se moquer des réponses du serveur.

Víctor Albertos
la source
7
  1. Commencez par créer votre interface 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. Votre demandeur à suivre:

    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. Si vous utilisez le deuxième choix (utilisez l'interface Retrofit pour les données du serveur Mock), vous devez MockRetrofit, utilisez le code suivant:

    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.Mes données proviennent du fichier d'actif (Asset / serveur / EventList.json), ce contenu de fichier est:

    {
      "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.Si vous utilisez l'intercepteur okhttp3, vous devez utiliser l'intercepteur auto-défini, comme ceci:

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.Enfin, vous pouvez demander à votre serveur avec le code:

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

Merci d'avoir lu.

Lai ZuLing
la source
5

En ajoutant à la réponse de @Alec, j'ai étendu le client fictif pour obtenir la réponse directement à partir d'un fichier texte dans le dossier d'actifs en fonction de l'URL de la demande.

Ex

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

Ici, le client fictif comprend que l'URL déclenchée est activée et recherche un fichier nommé activate.txt dans le dossier assets. Il lit le contenu du fichier assets / activate.txt et l'envoie en réponse à l'API.

Voici le prolongé 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()));
    }
}

Pour une explication détaillée, vous pouvez consulter mon blog
http://www.cumulations.com/blogs/13/Mock-API-response-in-Retrofit-using-custom-clients

praveena_kd
la source
salut, Lorsque j'écris une classe de test en utilisant robolectric et en utilisant un client simulé pour se moquer de l'API de mise à niveau, il ne me donne aucune réponse. Pourriez-vous me guider sur la manière de procéder.
Dory
Salut @Dory, assurez-vous d'avoir la partie de fin d'URL et le nom de fichier dans le dossier des actifs. Par exemple, disons que votre URL est comme ci-dessous (en utilisant Reftrofit ici) @POST ("/ redeemGyft") public void redeemGyft (@Body MposRequest reqdata, Callback <RedeemGyftResponse> callback); puis le nom du fichier correspondant dans le dossier des ressources est redeemgyft.txt
praveena_kd
J'ai donné un nom de fichier statique, dans mon MockClientfichier, écrit une classe de test en utilisant robolectric. Mais je ne peux pas obtenir de réponse du fichier json.
Dory
si vous avez conservé le fichier dans le dossier des ressources, il doit le récupérer.
praveena_kd
1

JSONPlaceholder: Fake Online REST API for Testing and Prototyping

https://jsonplaceholder.typicode.com/

ReqresIn: une autre API REST en ligne

https://reqres.in/

Serveur factice de facteur

Si vous souhaitez tester la charge utile de réponse personnalisée, les deux ci-dessus peuvent ne pas répondre à vos besoins, alors vous pouvez essayer le serveur factice postman. Il est assez facile à configurer et flexible pour définir votre propre charge utile de demande et de réponse.

entrez la description de l'image ici https://learning.getpostman.com/docs/postman/mock_servers/intro_to_mock_servers/ https://youtu.be/shYn3Ys3ygE

li2
la source
1

La simulation d'appels d'API avec Retrofit est désormais encore plus facile avec Mockinizer, ce qui rend le travail avec MockWebServer vraiment simple:

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

)

Créez simplement une carte de RequestFilter et MockResponses , puis branchez-la dans votre chaîne de générateur OkHttpClient:

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

Vous n'avez pas à vous soucier de la configuration de MockWebServer, etc. Ajoutez simplement vos simulacres, tout le reste est fait par Mockinizer pour vous.

(Avertissement: je suis l'auteur de Mockinizer)

donfuxx
la source
0

Pour moi, le client de rénovation personnalisé est excellent en raison de sa flexibilité. Surtout lorsque vous utilisez n'importe quel framework DI, vous pouvez activer / désactiver rapidement et simplement des simulations. J'utilise le client personnalisé fourni par Dagger également dans les tests unitaires et d'intégration.

Edit: Ici vous trouvez un exemple de mise à niveau moqueuse https://github.com/pawelByszewski/retrofitmock

Paweł Byszewski
la source