Kwadratowa makieta serwera modernizacyjnego do testowania

97

Jaki jest najlepszy sposób na sfałszowanie serwera do testowania przy użyciu kwadratowej struktury modernizacji .

Potencjalne sposoby:

  1. Utwórz nowego klienta do modernizacji i ustaw go w RestAdapter.Builder (). SetClient (). Obejmuje to przeanalizowanie obiektu Request i zwrócenie json jako obiektu Response.

  2. Zaimplementuj ten interfejs z adnotacjami jako klasę pozorowaną i użyj jej zamiast wersji dostarczonej przez RestAdapter.create () (nie będzie testować serializacji gson)

  3. ?

Idealnie byłoby, gdyby symulowany serwer zapewniał odpowiedzi json, aby móc testować serializację gson w tym samym czasie.

Wszelkie przykłady byłyby bardzo mile widziane.

Alec Holmes
źródło
@JakeWharton, jaki jest cel square-oss? Wydaje się zbędne, biorąc pod uwagę retrofit.
Charles,
@Alec Holmes: Czy rozwiązałeś swój problem?
AndiGeeky

Odpowiedzi:

104

Mock Retrofit 2.0 Requests for Testing

Ponieważ stare mechanizmy, takie jak tworzenie MockClientklasy i implementacja jej z poziomu, Clientnie działają już z Retrofit 2.0, tutaj opiszę nowy sposób robienia tego. Wszystko, co musisz teraz zrobić, to dodać niestandardowe przechwytywacze dla OkHttpClient, jak pokazano poniżej . FakeInterceptorklasa po prostu przesłania interceptmetodę iw przypadku gdy aplikacja jest w DEBUGtrybie zwraca podany 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;
    }
}

Kod źródłowy projektu na GitHub

Victor Apoyan
źródło
10
Aby uniknąć UnsupportedOperationException, użyj OkHttpClient.Builder. final OkHttpClient okHttpClient = new OkHttpClient.Builder () .addInterceptor (new FakeInterceptor ()) .build ();
John
4
Mam dwa problemy: 1 - nie ma uri()pod chain.request().uri()(naprawiłem to, String url = chain.request().url().toString();ponieważ mój przypadek jest inny). 2- Dostaję java.lang.IllegalStateException: network interceptor my.package.name.FakeInterceptor must call proceed() exactly once. Dodałem to addNetworkInterceptor()raczej do niż addInterceptor().
Hesam
2
użyj chain.request (). url (). uri ();
Amol Gupta,
Jak mogę udawać błąd 401, aby przetestować metodę httpClient.authenticator? po prostu wprowadzając kod "401" uwierzytelnianie metoda nie wywołuje. jak sobie z tym poradzę?
Mahdi
Przeniosłem podejście fałszywego przechwytywacza do wyszydzania interfejsów API sieci Web na wyższy poziom i opublikowałem w tym celu małą bibliotekę, aby było to jeszcze łatwiejsze i wygodniejsze. Zobacz github.com/donfuxx/Mockinizer
donfuxx
85

Postanowiłem wypróbować metodę 1 w następujący sposób

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

I używając go przez:

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

Działa dobrze i pozwala przetestować ciągi json bez konieczności kontaktowania się z prawdziwym serwerem!

Alec Holmes
źródło
Zaktualizowałem konstruktor Response używany, ponieważ stary był przestarzały, który wyrzucał IllegalArgumentException url == nullz Retrofit 1.4.1.
Dan J
1
Konieczne jest również dodanie punktu końcowego do builder.setEndpoint("http://mockserver.com").setClient(new MockClient());
kreatora
Rozszerzyłem powyższego klienta próbnego, aby pobrać odpowiedź z pliku w folderze zasobów w zależności od żądania adresu URL.
praveena_kd
21
Retrofit 2 używa teraz OkHttpClient dla warstwy klienta, a ten kod nie działa. ¿Masz jakiś pomysł, jak zrobić makietę OkHttpClient? Prawdopodobnie chodzi o jego rozszerzenie i zastąpienie, ale nie wiem jak.
GuillermoMP
1
Czy możesz zaktualizować swoją odpowiedź również w oparciu o Retrofit2? dzięki
Hesam
20

Testowanie deserializacji JSON na twoich obiektach (prawdopodobnie z TypeAdapters?) Wydaje się osobnym problemem, który wymaga oddzielnych testów jednostkowych.

Osobiście używam wersji 2. Zapewnia bezpieczny dla typu, przyjazny dla refaktorów kod, który można łatwo debugować i zmienić. W końcu, po co deklarować swoje API jako interfejsy, jeśli nie tworzysz ich alternatywnych wersji do testowania! Polimorfizm do zwycięstwa.

Inną opcją jest użycie języka Java Proxy. W ten sposób Retrofit (obecnie) implementuje swoją podstawową interakcję HTTP. Będzie to wymagało co prawda więcej pracy, ale pozwoliłoby na znacznie bardziej dynamiczne makiety.

Jake Wharton
źródło
To także mój ulubiony sposób. O wiele łatwiej jest debugować, jak wspomniano powyżej, a następnie zajmować się tym bezpośrednio z treścią odpowiedzi. @alec Jeśli chcesz przetestować serializację GSON, wygeneruj / wczytaj ciąg json i użyj obiektu gson do deserializacji. Pod głową myślę, że to i tak robi Retrofit.
loeschg
@JakeWharton Czy możesz podać krótki przykład tego, co by to było? Mam problem z wizualizacją tego ... Dzięki!
uncle_tex
8

Jestem wielkim fanem Apiary.io za kpiny z API przed przejściem na prawdziwy serwer.

Możesz także użyć płaskich plików .json i odczytać je z systemu plików.

Możesz także użyć publicznie dostępnych API, takich jak Twitter, Flickr itp.

Oto kilka innych przydatnych zasobów dotyczących modernizacji.

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

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

Przykładowy projekt: https://github.com/dustin-graham/ucad_twitter_retrofit_sample

jpotts18
źródło
7

Mockery (zastrzeżenie: jestem autorem) zostało zaprojektowane właśnie do tego właśnie zadania.

Mockery to biblioteka do próbowania / testowania, która koncentruje się na weryfikacji warstw sieciowych z wbudowaną obsługą modernizacji. Automatycznie generuje testy JUnit na podstawie specyfikacji danego interfejsu API. Chodzi o to, aby nie musieć ręcznie pisać żadnego testu; ani implementacja interfejsów do naśladowania odpowiedzi serwera.

Víctor Albertos
źródło
7
  1. Najpierw utwórz interfejs modernizacji.

    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. Twój wnioskodawca obserwuje:

    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. Jeśli korzystasz z drugiego wyboru (użyj interfejsu Retrofit do danych serwera Mock), musisz MockRetrofit, użyj następującego kodu:

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

Moje dane pochodzą z pliku aktywów (Asset / server / EventList.json), zawartość tego pliku to:

    {
      "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"
        }
      ]
    }

Jeśli używasz przechwytywacza okhttp3, musisz samodzielnie zdefiniować przechwytywacz, na przykład:

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. Na koniec możesz zażądać serwera z kodem:

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

Dziękuje za przeczytanie.

Lai ZuLing
źródło
5

Dodając do odpowiedzi @Alec, rozszerzyłem klienta próbnego, aby uzyskać odpowiedź bezpośrednio z pliku tekstowego w folderze zasobów w zależności od adresu URL żądania.

Dawny

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

W tym przypadku klient pozorowany rozumie, że uruchamiany adres URL jest aktywowany i szuka pliku o nazwie activ.txt w folderze zasobów. Odczytuje zawartość z pliku asset / activ.txt i wysyła ją jako odpowiedź do API.

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

Szczegółowe wyjaśnienie można znaleźć na moim blogu
http://www.cumulations.com/blogs/13/Mock-API-response-in-Retrofit-using-custom-clients

praveena_kd
źródło
Cześć, Kiedy piszę klasę testową za pomocą robolectric i używam fałszywego klienta do mockowania api modernizacji, nie daje mi to żadnej odpowiedzi. Czy możesz mi pomóc, jak to zrobić.
Dory
Cześć @Dory, upewnij się, że w folderze zasobów znajduje się końcówka adresu URL i nazwa pliku. Na przykład, powiedzmy, że Twój adres URL jest taki jak poniżej (używając tutaj Reftrofit) @POST ("/ redeemGyft") public void redeemGyft (@Body MposRequest reqdata, Callback <RedeemGyftResponse> callback); wówczas odpowiednia nazwa pliku w folderze zasobów to redeemgyft.txt
praveena_kd
MockClientPodałem statyczną nazwę pliku, w moim pliku napisałem klasę testową za pomocą robolectric. Ale nie mogę uzyskać żadnej odpowiedzi z pliku json.
Dory
jeśli przechowujesz plik w folderze zasobów, powinien go odebrać.
praveena_kd
1

JSONPlaceholder: fałszywy interfejs API REST online do testowania i prototypowania

https://jsonplaceholder.typicode.com/

ReqresIn: inny interfejs API REST online

https://reqres.in/

Serwer próbny listonosza

Jeśli chcesz przetestować ładunek odpowiedzi niestandardowej, powyższe dwa mogą nie odpowiadać Twoim wymaganiom, możesz wypróbować serwer próbny listonosza. Definiowanie własnych żądań i odpowiedzi jest dość łatwe w konfiguracji i elastyczne.

wprowadź opis obrazu tutaj https://learning.getpostman.com/docs/postman/mock_servers/intro_to_mock_servers/ https://youtu.be/shYn3Ys3ygE

li2
źródło
1

Mockowanie wywołań interfejsu API za pomocą funkcji Retrofit jest teraz jeszcze łatwiejsze dzięki Mockinizerowi, który sprawia, że ​​praca z MockWebServer jest naprawdę prosta:

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

)

Po prostu utwórz mapę RequestFilter i MockResponses, a następnie podłącz ją do łańcucha konstruktorów OkHttpClient:

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

Nie musisz martwić się konfiguracją MockWebServer itp. Po prostu dodaj swoje makiety, cała reszta zostanie wykonana przez Mockinizer za Ciebie.

(Zastrzeżenie: jestem autorem Mockinizera)

donfuxx
źródło
0

Dla mnie niestandardowy klient retrofitu jest świetny ze względu na elastyczność. Zwłaszcza gdy używasz dowolnego frameworka DI, możesz szybko i prosto włączyć / wyłączyć makietę. Używam niestandardowego klienta dostarczonego przez Dagger również w testach jednostkowych i integracyjnych.

Edycja: tutaj znajdziesz przykład kpiącej modernizacji https://github.com/pawelByszewski/retrofitmock

Paweł Byszewski
źródło