为单元测试创建 Retrofit 的模拟。

huangapple go评论71阅读模式
英文:

Creating mocks for unit test - retrofit

问题

以下是翻译好的部分:

有一个命令行应用程序使用公共API显示明天的天气预报

示例输出可能如下所示

明天(2019/05/01)在城市XYZ:
晴朗
气温:26.5℃
风速:7.6英里/小时
湿度:61%


问题:您将如何创建一个测试用例,使测试不会触及真实的服务,并且可以在没有互联网的情况下工作。

我尝试创建了同样的JUnit测试,只要我直接使用API,它就可以正常工作。

有人可以帮忙看看我如何为单元测试创建模拟吗?

App.java

import api.ForecastServiceImpl;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import java.io.IOException;
import java.time.LocalDate;

public class App {

public static void main(String[] args) throws IOException {
    if (args.length < 1) {
        System.out.println("请将城市名作为参数传递");
        System.exit(1);
    }

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://www.metaweather.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    LocalDate tomorrow = LocalDate.now().plusDays(1);

    ForecastServiceImpl service = new ForecastServiceImpl(retrofit);
    System.out.println(service.getForecast(args[0], tomorrow));
}

}


ForecastServiceImpl.java

package api;

import model.City;
import model.Forecast;
import retrofit2.Call;
import retrofit2.Retrofit;
import util.PathDate;

import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;

public class ForecastServiceImpl {

private Retrofit retrofit;

public ForecastServiceImpl(Retrofit retrofit) {
    this.retrofit = retrofit;
}

public String getForecast(String cityName, LocalDate date) throws IOException {
    PathDate pathDate = new PathDate(date);

    ForecastService service = retrofit.create(ForecastService.class);
    Call<List<City>> findCityCall = service.findCityByName(cityName.toLowerCase());
    City city = Objects.requireNonNull(findCityCall.execute().body())
            .stream()
            .findFirst()
            .orElseThrow(() -> new RuntimeException(String.format("找不到%s的城市ID", cityName)));

    Call<List<Forecast>> forecastCall = service.getForecast(city.getWoeid(), pathDate);
    Forecast forecast = Objects.requireNonNull(forecastCall.execute().body())
            .stream()
            .findFirst()
            .orElseThrow(() -> new RuntimeException(String.format("无法获取%s的天气预报", cityName)));

    return String.format("天气预报(%s)在%s:\n%s", pathDate, city.getTitle(), forecast);
}

}


ForecastService.java

package api;

import model.City;
import model.Forecast;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
import util.PathDate;

import java.util.List;

public interface ForecastService {

@GET("/api/location/{city_id}/{date}/")
Call<List<Forecast>> getForecast(@Path("city_id") Long cityId, @Path("date") PathDate date);

@GET("/api/location/search/")
Call<List<City>> findCityByName(@Query("query") String city);

}


City.java

package model;

public class City {

private String title;
private Long woeid;

public String getTitle() {
    return title;
}

public void setTitle(String title) {
    this.title = title;
}

public Long getWoeid() {
    return woeid;
}

public void setWoeid(Long woeid) {
    this.woeid = woeid;
}

}


Forecast.java

package model;

import com.google.gson.annotations.SerializedName;

public class Forecast {

private Long id;
@SerializedName("weather_state_name")
private String weatherState;
@SerializedName("wind_speed")
private Double windSpeed;
@SerializedName("the_temp")
private Double temperature;
private Integer humidity;

public Long getId() {
    return id;
}

public Forecast setId(Long id) {
    this.id = id;
    return this;
}

public String getWeatherState() {
    return weatherState;
}

public Forecast setWeatherState(String weatherState) {
    this.weatherState = weatherState;
    return this;
}

public Double getWindSpeed() {
    return windSpeed;
}

public Forecast setWindSpeed(Double windSpeed) {
    this.windSpeed = windSpeed;
    return this;
}

public Double getTemperature() {
    return temperature;
}

public Forecast setTemperature(Double temperature) {
    this.temperature = temperature;
    return this;
}

public Integer getHumidity() {
    return humidity;
}

public Forecast setHumidity(Integer humidity) {
    this.humidity = humidity;
    return this;
}

@Override
public String toString() {
    return String.format("%s\n气温:%.1f℃\n风速:%.1f英里/小时\n湿度:%d%%",
            weatherState, temperature, windSpeed, humidity);
}

}


PathDate.java

package util;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class PathDate {

private final LocalDate date;

public PathDate(LocalDate date) {
    this.date = date;
}

@Override public String toString() {
    return date.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
}

}


Utils.java

package util;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Utils {

public static byte[] readResourceFileToBytes(String filename) {
    byte[] fileBytes = new byte[0];
    try {
        Path path = Paths.get(Utils.class.getClassLoader().getResource(filename).toURI());
        fileBytes = Files.readAllBytes(path);
    } catch (URISyntaxException|IOException|NullPointerException e) {
        e.printStackTrace();
    }

    return fileBytes;
}

}



<details>
<summary>英文:</summary>

There is commandline app to show tomorrow&#39;s forecast using a public API

Sample output could be as follows:

Tomorrow (2019/05/01) in city XYZ:
Clear
Temp: 26.5 °C
Wind: 7.6 mph
Humidity: 61%


Question : How will you create a test case such that tests should not touch the real service and work without the Internet.

I tried creating the junit test for the same and its working fine till the time i am using the api directly.

Can someone please help how can i create a mock for my unit testing.

App.java

import api.ForecastServiceImpl;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import java.io.IOException;
import java.time.LocalDate;

public class App {

public static void main(String[] args) throws IOException {
    if (args.length &lt; 1) {
        System.out.println(&quot;Pass city name as an argument&quot;);
        System.exit(1);
    }

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(&quot;https://www.metaweather.com&quot;)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    LocalDate tomorrow = LocalDate.now().plusDays(1);

    ForecastServiceImpl service = new ForecastServiceImpl(retrofit);
    System.out.println(service.getForecast(args[0], tomorrow));
}

}


ForecastServiceImpl.java 

package api;

import model.City;
import model.Forecast;
import retrofit2.Call;
import retrofit2.Retrofit;
import util.PathDate;

import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;

public class ForecastServiceImpl {

private Retrofit retrofit;

public ForecastServiceImpl(Retrofit retrofit) {
    this.retrofit = retrofit;
}

public String getForecast(String cityName, LocalDate date) throws IOException {
    PathDate pathDate = new PathDate(date);

    ForecastService service = retrofit.create(ForecastService.class);
    Call&lt;List&lt;City&gt;&gt; findCityCall = service.findCityByName(cityName.toLowerCase());
    City city = Objects.requireNonNull(findCityCall.execute().body())
            .stream()
            .findFirst()
            .orElseThrow(() -&gt; new RuntimeException(String.format(&quot;Can&#39;t find city id for %s&quot;, cityName)));

    Call&lt;List&lt;Forecast&gt;&gt; forecastCall = service.getForecast(city.getWoeid(), pathDate);
    Forecast forecast = Objects.requireNonNull(forecastCall.execute().body())
            .stream()
            .findFirst()
            .orElseThrow(() -&gt; new RuntimeException(String.format(&quot;Can&#39;t get forecast for %s&quot;, cityName)));

    return String.format(&quot;Weather on (%s) in %s:\n%s&quot;, pathDate, city.getTitle(), forecast);
}

}


ForecastService.java

package api;

import model.City;
import model.Forecast;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
import util.PathDate;

import java.util.List;

public interface ForecastService {

@GET(&quot;/api/location/{city_id}/{date}/&quot;)
Call&lt;List&lt;Forecast&gt;&gt; getForecast(@Path(&quot;city_id&quot;) Long cityId, @Path(&quot;date&quot;) PathDate date);

@GET(&quot;/api/location/search/&quot;)
Call&lt;List&lt;City&gt;&gt; findCityByName(@Query(&quot;query&quot;) String city);

}


City.java

package model;

public class City {

private String title;
private Long woeid;

public String getTitle() {
    return title;
}

public void setTitle(String title) {
    this.title = title;
}

public Long getWoeid() {
    return woeid;
}

public void setWoeid(Long woeid) {
    this.woeid = woeid;
}

}


Forecast.java

package model;

import com.google.gson.annotations.SerializedName;

public class Forecast {

private Long id;
@SerializedName(&quot;weather_state_name&quot;)
private String weatherState;
@SerializedName(&quot;wind_speed&quot;)
private Double windSpeed;
@SerializedName(&quot;the_temp&quot;)
private Double temperature;
private Integer humidity;

public Long getId() {
    return id;
}

public Forecast setId(Long id) {
    this.id = id;
    return this;
}

public String getWeatherState() {
    return weatherState;
}

public Forecast setWeatherState(String weatherState) {
    this.weatherState = weatherState;
    return this;
}

public Double getWindSpeed() {
    return windSpeed;
}

public Forecast setWindSpeed(Double windSpeed) {
    this.windSpeed = windSpeed;
    return this;
}

public Double getTemperature() {
    return temperature;
}

public Forecast setTemperature(Double temperature) {
    this.temperature = temperature;
    return this;
}

public Integer getHumidity() {
    return humidity;
}

public Forecast setHumidity(Integer humidity) {
    this.humidity = humidity;
    return this;
}

@Override
public String toString() {
    return String.format(&quot;%s\nTemp: %.1f &#176;C\nWind: %.1f mph\nHumidity: %d%%&quot;,
            weatherState, temperature, windSpeed, humidity);
}

}


PathDate.java

package util;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class PathDate {

private final LocalDate date;

public PathDate(LocalDate date) {
    this.date = date;
}

@Override public String toString() {
    return date.format(DateTimeFormatter.ofPattern(&quot;yyyy/MM/dd&quot;));
}

}


Utils.java 

package util;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Utils {

public static byte[] readResourceFileToBytes(String filename) {
    byte[] fileBytes = new byte[0];
    try {
        Path path = Paths.get(Utils.class.getClassLoader().getResource(filename).toURI());
        fileBytes = Files.readAllBytes(path);
    } catch (URISyntaxException|IOException|NullPointerException e) {
        e.printStackTrace();
    }

    return fileBytes;
}

}


</details>


# 答案1
**得分**: 0

`service.getForecast(city.getWoeid(), pathDate);` 返回一个 `Call<List<Forecast>>` 对象。当我们在这个对象上调用 `execute` 时,会发起实际的 API 调用。由于我们不想进行实际的 API 调用,我们可以尝试模拟 `Call<List<Forecast>>` 对象。

我们可以这样模拟 `Call` 类:

```java
Call<List<Forecast>> mockedListForecast = mock(Call.class);

上面的语句创建了一个 Call<List<Forecast>> 的模拟对象。我们可以使用 when 来定义在模拟对象上调用方法时应该发生什么事情。

// 这里我返回了一个单例列表,您可以返回一个预测列表
when(mockedListForecast.execute()).thenReturn(Response.success(Collections.singletonList()));

上面的代码表示当在模拟对象上调用 execute 函数时,返回一个空的预测列表。

通过这种方式,我们模拟了API响应,而不需要进行实际的API调用。

编辑

您还可以使用 Retrofit Mock 来模拟您的 Retrofit API。

英文:

The service.getForecast(city.getWoeid(), pathDate); returns us a Call&lt;List&lt;Forecast&gt;&gt; object. And when we call execute on this object then an actual API call is made. Since we don't want to do an actual API call, we can try mocking the Call&lt;List&lt;Forecast&gt;&gt; object.

We can mock the Call class like

Call&lt;List&lt;Forecast&gt;&gt; mockedListForeCast = mock(Call.class);

The above statement creates a mock object of Call&lt;List&lt;Forecast&gt;&gt;. We can use when to define what should happen when a method is called on the mocked object.

// here I am returning the singleton list, you can return a list of forecast
when(mockedListForeCast.execute()).thenReturn(Response.success(Collections.singletonList()));

The above line says that return an empty list of forecast when the execute function was called on the mocked object.

This way we are mocking the API response and we don't have to do an actual API call.

Edit:

You can also mock your retrofit API using Retrofit Mock also.

huangapple
  • 本文由 发表于 2020年10月3日 14:34:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/64181410.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定