vitormarcal

Contribua com este conteúdo! Mande sua correção ou sugestão, via Pull Request no Github!💗

Como mockar requisições do retrofit

Uma simples maneira de mockar requisições ultilizando Retrofit, OkHttp e continuar com seus testes unitários.

Escrito por Vítor Marçal em 27 de Agosto de 2019
4 min de leitura
#retrofit #mock #unit test

Retrofit é uma biblioteca muito poderosa que facilita muito a vida ao fazer integrações com web services. Quando a conheci, logo me deparei com um problema: como continuar meus testes unitários mockando as respostas do retrofit? É isso que veremos nesse artigo.

Imagine que em sua aplicação existe uma integração a uma Api chamada StoreApi. Nessa hipotética API, você consegue listar produtos e acessar seus detalhes. Por exemplo, ao consultar a uri products/, é retornado uma lista de produtos; ao fazer uma requisição a products/{id} o retorno é o detalhe de determinado produto. Prosseguindo, em nossa aplicação, temos uma funcionalidade que em determinado ponto busca um produto na API passando o id do recurso, para logo após imprimir suas informações. Para nosso exemplo, não importa o que método print faz:

class ProductService

// package, imports omitidos
class ProductService {
    private final StoreApi storeApi;
    private static final Logger log = Logger.getLogger(ProductService.class.getName());

    ProductService(StoreApi storeApi) {
        this.storeApi = storeApi;
    }

    void printProductOfId(Long id) {
        Optional<Product> optionalProduct = findInStoreApi(id);
        if (optionalProduct.isPresent()) {
            print(optionalProduct.get());
        } else {
            throw new RuntimeException("Product not found");
        }
    }

    Optional<Product> findInStoreApi(Long id) {
        final Response<Product> response;
        try {
            response = this.storeApi.getDetail(id).execute();
            if (response.isSuccessful()) {
                return Optional.ofNullable(response.body());
            } else {
                log.log(Level.WARNING, "The response body is empty with id " + id);
                return Optional.empty();
            }
        } catch (IOException e) {
            log.log(Level.WARNING, "Error with id " + id, e);
            return Optional.empty();
        }

    }

    private void print(Product product) {
        System.out.println(product);
    }
}

Você deseja criar um teste unitário para o método printProductOfId, mas por algum motivo você não pode fazer uma chamada real a StoreApi e talvez você nem queira realmente disparar uma requisição verdadeira: vamos supor que cada requisição gere um custo para você! Podemos continuar nossos testes simplesmente mockando a StoreApi com um interceptor:

class RestClientMock

// package, imports da classe omitidos
class RestClientMock {
    private static StoreApi storeApi;

    static StoreApi getClient() {
        if (storeApi == null) {
            final OkHttpClient client = new OkHttpClient.Builder()
                    .addInterceptor(new FakeInterceptor())
                    .build();

            final Retrofit retrofit = new Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create())
                    .baseUrl("https://wwww.storemock-api.com.br")
                    .client(client)
                    .build();

            storeApi = retrofit.create(StoreApi.class);
        }
        return storeApi;
    }
}

Perceba que adicionamos um novo interceptor chamado FakeInterceptor. Ele intercepta a requisição e devolve uma resposta mockada, assim como o nome da classe diz:

class FakeInterceptor

// package, imports omitidos
public class FakeInterceptor implements Interceptor {
    private static final String productDetail = "{\"id\":1,\"name\":\"Book\"}";


    @Override
    public Response intercept(Chain chain) {
        Response response;
        String responseString = "{}";
        final List<String> paths = chain.request().url().pathSegments();

        Response.Builder builder = new Response.Builder();
        builder.code(404);
        if (paths != null && !paths.isEmpty()) {
            String lastPath = paths.get(paths.size() - 1);
            if (lastPath.equals("1")) {
                builder.code(200);
                responseString = productDetail;
            }
        }

        response = builder
                .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;
    }
}

Agora só precisamos utilizar o cliente mockado em nossos testes unitários:

class ProductServiceTest

// package, imports omitidos
public class ProductServiceTest {
    private static final StoreApi storeApi = RestClientMock.getClient();

    @Test
    public void printProductOfId() {
        ProductService productService = new ProductService(storeApi);
        productService.printProductOfId(1L);
        // some test to validate the print
    }

    @Test(expected = RuntimeException.class)
    public void giveProductNotFound_ThenThrowRuntimeException() {
        ProductService productService = new ProductService(storeApi);
        productService.printProductOfId(2L);
    }

    @Test
    public void productIsPresent() {
        ProductService productService = new ProductService(storeApi);
        Optional<Product> optionalProduct = productService.findInStoreApi(1L);
        assertTrue(optionalProduct.isPresent());
    }

    @Test
    public void productIsNotPresent() {
        ProductService productService = new ProductService(storeApi);
        Optional<Product> optionalProduct = productService.findInStoreApi(2L);
        assertFalse(optionalProduct.isPresent());
    }
}

A implementação completa deste exemplo pode ser encontrada neste repositório do GitHub.