博客原地址:【译】安卓中的自动化测试(3)
译文原链接:Introduction to Android Testing – Part 3
翻译:Anthony
在之前的两篇文章中我们讲到了如何进行测试的配置以及创建了一个示例项目。这里是第一篇 ,第二篇的链接。
在这片博客中我们将会获取Github API中的用户列表,并且编写相应的单元测试。我们将从这个项目节点开始。
创建API调用
我们将利用Retrofit和Rxjava进行API处理。如果你对Rxjava和Retrofit不怎么熟悉,那么我推荐你阅读这篇文章,如果你从没有使用过Retrofit,那么我推荐 这一篇文章。
为了获取相应的用户列表,我们需要对下面的节点进行访问。(由于API限制,这里我也将限制页面大小为2)。
https://api.github.com/search/users?per_page=2&q=rebecca
如果需要获取用户信息(比如说用户介绍和地理位置),我们需要下面的这个调用。
https://api.github.com/users/riggaroo
1 对这些节点的数据进行处理,我们将会使用JSON对象,通过在项目中返回或者嵌套使用。我通常在这里在线生成代码。这里创建User类和UserList类。
package za.co.riggaroo.gus.data.remote.model;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class User {
@SerializedName("login")
@Expose
private String login;
@SerializedName("id")
@Expose
private Integer id;
@SerializedName("avatar_url")
@Expose
private String avatarUrl;
@SerializedName("gravatar_id")
@Expose
private String gravatarId;
@SerializedName("url")
@Expose
@SerializedName("type")
@Expose
private String type;
@SerializedName("name")
@Expose
private String name;
@SerializedName("location")
@Expose
private String location;
@SerializedName("email")
@SerializedName("bio")
@Expose
private String bio;
@SerializedName("followers")
@Expose
private Integer followers;
@SerializedName("following")
@Expose
private Integer following;
@SerializedName("created_at")
@Expose
private String createdAt;
@SerializedName("updated_at")
@Expose
private String updatedAt;
... //see more at https://github.com/riggaroo/GithubUsersSearchApp/blob/testing-tutorial-part3-complete/app/src/main/java/za/co/riggaroo/gus/data/remote/model/User.java
}
package za.co.riggaroo.gus.data.remote.model;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
public class UsersList {
@SerializedName("total_count")
@Expose
private Integer totalCount;
@SerializedName("items")
@Expose
private List<User> items = new ArrayList<User>();
public Integer getTotalCount() {
return totalCount;
}
public void setTotalCount(Integer totalCount) {
this.totalCount = totalCount;
}
public List<User> getItems() {
return items;
}
public void setItems(List<User> items) {
this.items = items;
}
}
在这些实体类被创建之后,我们进入到GithubUserRestService
类,这也是我们进行Retrofit调用的类。
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
import rx.Observable;
import za.co.riggaroo.gus.data.remote.model.User;
import za.co.riggaroo.gus.data.remote.model.UsersList;
public interface GithubUserRestService {
@GET("/search/users?per_page=2")
Observable<UsersList> searchGithubUsers(@Query("q") String searchTerm);
@GET("/users/{username}")
Observable<User> getUser(@Path("username") String username);
}
第一个网络调用,将会根据搜索关键字检索用户列表。第二个网络访问将会获得用户的详细信息。
2 进入UserRepositoryImpl
类,我们将在这里面将上方的两个网络访问结合在一起并数据转化为前端展示。这里使用Rxjava获取对应搜索关键字的用户列表并且针对每个用户将会调用网络去查找用户的详细信息。(如果你实现了你自己的API,那么我会尽量通过一次网络访问获取相应的用户信息。将在我的Reducing Mobile Data Usage Talk 视频中进行讲解。)
import java.io.IOException;
import java.util.List;
import rx.Observable;
import za.co.riggaroo.gus.data.remote.GithubUserRestService;
import za.co.riggaroo.gus.data.remote.model.User;
public class UserRepositoryImpl implements UserRepository {
private GithubUserRestService githubUserRestService;
public UserRepositoryImpl(GithubUserRestService githubUserRestService) {
this.githubUserRestService = githubUserRestService;
}
@Override
public Observable<List<User>> searchUsers(final String searchTerm) {
return Observable.defer(() -> githubUserRestService.searchGithubUsers(searchTerm).concatMap(
usersList -> Observable.from(usersList.getItems())
.concatMap(user -> githubUserRestService.getUser(user.getLogin())).toList()))
.retryWhen(observable -> observable.flatMap(o -> {
if (o instanceof IOException) {
return Observable.just(null);
}
return Observable.error(o);
}));
}
}
在上面的代码中,我通过Observable.defer()
创建了observable 对象,也就意味着observables代码只有在subscriber 订阅的时候运行一次。(并不像Observable.create()将会在创建的时候运行,这里Observable.create()是一个不安全的Rxjava API,并且不应当被使用)。
当有subscriber的时候, githubUserRestService
通过搜索关键字进行搜索。这里使用concatMap进行获取用户列表(user list),并且依次发送到observable中,并且依次对每个用户user调用githubUserRestService.getUser()
。这个observable对象也就转化为了用户列表。
在网络调用中也有一个再链接(retry)机制。retryWhen()将会在IOException抛出的时候再链接observable。IOException 当没有网络访问的时候被Retrofit抛出(你可能会在几次网络尝试之后进行终止操作。)
你可能注意到了我在代码中用到了lambda表达式。点击这里获取在java 8 ,android上使用Jack toolchain的使用方法。
这里我们有了一个repository对象(UserRepositoryImpl
)并通过两次网络访问获取了一个用户列表。我们接下来编写相应的测试代码。
单元测试-Mockito是什么?
为了对上面的repository对象进行单元测试,这里引入了Mockito。那么什么是Mockito呢?Mockito是一个java中使用的基于MIT协议下的一个开源测试框架。这个框架允许在自动化单元测试中创建模拟对象。 (维基百科)
Mockito允许你对相应的方法进行模拟(stub 桩)并且验证对象之间的交互。当我们编写单元测试的时候,我们需要考虑对组件进行单独的测试。对类的职责之外的事情应当不在考虑范围之内。Mockito正帮我们实现了这个隔离功能。
好吧 ,下面一起来编写测试吧。
对UserRepositoryImpl类编写单元测试
1 选择UserRepositoryImpl 类并且按住“ALT + ENTER”,并在弹出的列表对话框中选择“Create Test”,下面这个创建测试的对话框会弹出来。
2 这里你可以选择生成方法(generate method)但我通常不会勾选这个选项 。接下来会让你选择测试包的放置位置,由于这里只是一个单元测试,并不会设计到 Android Context 相关的代码。所以这里选择 “app/src/test”目录
3 现在对单元测试进行了配置。这里创建了一个UserRepository
对象,由于我们不会对API进行直接操作,这里我们也需要mock(模拟)一个GithubUserRestService
对象。这个测试也就确保了整个UserReposity的转换过程正确的进行。下面是这个单元测试的代码:
@Mock
GithubUserRestService githubUserRestService;
private UserRepository userRepository;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
userRepository = new UserRepositoryImpl(githubUserRestService);
}
4 第一个编写的测试将会对GithubUserRestService
进行测试并且传入相应的正确的参数,并测试是否返回期望的结果值。下面是我写的示例代码:
@Test
public void searchUsers_200OkResponse_InvokesCorrectApiCalls() {
//Given
when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(Observable.just(githubUserList()));
when(githubUserRestService.getUser(anyString()))
.thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
//When
TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
//Then
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
List<List<User>> onNextEvents = subscriber.getOnNextEvents();
List<User> users = onNextEvents.get(0);
Assert.assertEquals(USER_LOGIN_RIGGAROO, users.get(0).getLogin());
Assert.assertEquals(USER_LOGIN_2_REBECCA, users.get(1).getLogin());
verify(githubUserRestService).searchGithubUsers(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
}
private UsersList githubUserList() {
User user = new User();
user.setLogin(USER_LOGIN_RIGGAROO);
User user2 = new User();
user2.setLogin(USER_LOGIN_2_REBECCA);
List<User> githubUsers = new ArrayList<>();
githubUsers.add(user);
githubUsers.add(user2);
UsersList usersList = new UsersList();
usersList.setItems(githubUsers);
return usersList;
}
private User user1FullDetails() {
User user = new User();
user.setLogin(USER_LOGIN_RIGGAROO);
user.setName("Rigs Franks");
user.setAvatarUrl("avatar_url");
user.setBio("Bio1");
return user;
}
private User user2FullDetails() {
User user = new User();
user.setLogin(USER_LOGIN_2_REBECCA);
user.setName("Rebecca Franks");
user.setAvatarUrl("avatar_url2");
user.setBio("Bio2");
return user;
}
整个测试被分为三个部分:given(前置条件),when(执行),then.(最终判断)。我会将我的测试分为这三个部分为让代码更加有序并明确测试的功能性。在这个测试中,前置条件是让Github service 返回一些用户。执行过程是搜索用户,结果应当被返回和正确的执行。
这里对测试的命名也很重要。我通常采取的方式是
[被测试的方法名][测试用例的条件][期待值]
所以在这里这个方法是issearchUsers_200OkResponse_InvokesCorrectApiCalls()
在这个测试当中,TestSubscriber
被搜索的observable订阅。Assertion(断言)用于TestSubscriber上用于判断是否是期望的值。
5 接下来的单元测试用于测试搜索的时候抛出IOException,然后网络会被再链接。
@Test
public void searchUsers_IOExceptionThenSuccess_SearchUsersRetried() {
//Given
when(githubUserRestService.searchGithubUsers(anyString()))
.thenReturn(getIOExceptionError(), Observable.just(githubUserList()));
when(githubUserRestService.getUser(anyString()))
.thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
//When
TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
//Then
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
verify(githubUserRestService, times(2)).searchGithubUsers(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
}
在这个测试中,我们断言githubUserRestService
将会被调用两次,另外一个网络连接会调用一次。也断言subscriber上不会有终止错误。
对UserRepositoryImpl进行最终的单元测试
我添加了更多不同于上面的测试用例,他们针对不同的测试用例但是都遵循上面的方法和概念。下面是UserRepositoryImpl
的完整测试代码。
public class UserRepositoryImplTest {
private static final String USER_LOGIN_RIGGAROO = "riggaroo";
private static final String USER_LOGIN_2_REBECCA = "rebecca";
@Mock
GithubUserRestService githubUserRestService;
private UserRepository userRepository;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
userRepository = new UserRepositoryImpl(githubUserRestService);
}
@Test
public void searchUsers_200OkResponse_InvokesCorrectApiCalls() {
//Given
when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(Observable.just(githubUserList()));
when(githubUserRestService.getUser(anyString()))
.thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
//When
TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
//Then
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
List<List<User>> onNextEvents = subscriber.getOnNextEvents();
List<User> users = onNextEvents.get(0);
Assert.assertEquals(USER_LOGIN_RIGGAROO, users.get(0).getLogin());
Assert.assertEquals(USER_LOGIN_2_REBECCA, users.get(1).getLogin());
verify(githubUserRestService).searchGithubUsers(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
}
private UsersList githubUserList() {
User user = new User();
user.setLogin(USER_LOGIN_RIGGAROO);
User user2 = new User();
user2.setLogin(USER_LOGIN_2_REBECCA);
List<User> githubUsers = new ArrayList<>();
githubUsers.add(user);
githubUsers.add(user2);
UsersList usersList = new UsersList();
usersList.setItems(githubUsers);
return usersList;
}
private User user1FullDetails() {
User user = new User();
user.setLogin(USER_LOGIN_RIGGAROO);
user.setName("Rigs Franks");
user.setAvatarUrl("avatar_url");
user.setBio("Bio1");
return user;
}
private User user2FullDetails() {
User user = new User();
user.setLogin(USER_LOGIN_2_REBECCA);
user.setName("Rebecca Franks");
user.setAvatarUrl("avatar_url2");
user.setBio("Bio2");
return user;
}
@Test
public void searchUsers_IOExceptionThenSuccess_SearchUsersRetried() {
//Given
when(githubUserRestService.searchGithubUsers(anyString()))
.thenReturn(getIOExceptionError(), Observable.just(githubUserList()));
when(githubUserRestService.getUser(anyString()))
.thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
//When
TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
//Then
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
verify(githubUserRestService, times(2)).searchGithubUsers(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
}
@Test
public void searchUsers_GetUserIOExceptionThenSuccess_SearchUsersRetried() {
//Given
when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(Observable.just(githubUserList()));
when(githubUserRestService.getUser(anyString()))
.thenReturn(getIOExceptionError(), Observable.just(user1FullDetails()),
Observable.just(user2FullDetails()));
//When
TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
//Then
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
verify(githubUserRestService, times(2)).searchGithubUsers(USER_LOGIN_RIGGAROO);
verify(githubUserRestService, times(2)).getUser(USER_LOGIN_RIGGAROO);
verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
}
@Test
public void searchUsers_OtherHttpError_SearchTerminatedWithError() {
//Given
when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(get403ForbiddenError());
//When
TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
//Then
subscriber.awaitTerminalEvent();
subscriber.assertError(HttpException.class);
verify(githubUserRestService).searchGithubUsers(USER_LOGIN_RIGGAROO);
verify(githubUserRestService, never()).getUser(USER_LOGIN_RIGGAROO);
verify(githubUserRestService, never()).getUser(USER_LOGIN_2_REBECCA);
}
private Observable getIOExceptionError() {
return Observable.error(new IOException());
}
private Observable<UsersList> get403ForbiddenError() {
return Observable.error(new HttpException(
Response.error(403, ResponseBody.create(MediaType.parse("application/json"), "Forbidden"))));
}
}
运行单元测试
现在编写完成这些测试之后,我们需要运行。
1 通过右键相应的测试类并且选择“Run UserRepositoryImplTest with Coverage”
2 你会在Android Studio的右边窗口发现运行结果。
这里UserRepositoryImpl
类有100%的代码单元测试覆盖率。在下一篇博客中,将会针对UI展示搜索结果,并且编写相应的测试。
【译者将在下一篇文章翻译中对本系列的下一篇文章进行翻译】