Android 自动化测试 - 第3部分

在之前的两篇博文中,我介绍了如何在Android中进行测试,我们创建了一个样本应用,我们将继续在这篇博文中继续开发。如果您错过了这两个帖子,我建议您阅读第1部分和第2部分。

在本文中,我们将从Github API中获取用户列表,并为其编写单元测试。
我们将从这个检查点的下一个repo开始。

创建Web服务调用

要使用Github API,我们将使用翻新和RxJava。
我不打算在本系列中解释RxJava或Retrofit。
如果您不熟悉RxJava,我建议您阅读这些文章
如果你没有使用过Retrofit,我建议阅读这里

为了获得一个搜索词的用户列表,我们需要使用下面的端点:

1
https://api.github.com/search/users?per_page=2&q=rebecca

要获得更多的用户信息(例如用户的个人信息和位置),我们需要进行后续调用:

1
https://api.github.com/users/riggaroo

要开始使用这些端点,我们应该创建它们正在返回的JSON对象,并将它们包含在我们的项目中。
我通常在在线生成它们。
让我们创建以下类:User类和UsersList类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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调用的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

第一个网络调用将执行搜索以获得用户列表,第二个网络调用将获得关于用户的更多信息。

导航到UserRepositoryImpl
在这里,我们将把两个网络调用合并起来,并将数据转换为一个将在前端使用的视图。
这是使用RxJava首先获取搜索词的用户列表,然后为每个用户提供另一个网络调用,以查找更多的用户信息。(如果您自己实现了这个API,我将尝试让一个网络调用返回所有必需的信息——正如我在减少移动数据使用谈话中所讨论的那样)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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对象,这意味着只有当它有一个订阅者(不像Observable.create()在创建时运行)时,才会运行observables代码。
正如下面的注释所纠正的那样,Observable.create()是一个不安全的RxJava API,它不应该被使用。

当有订阅者时,将调用githubUserRestService来搜索提供的searchTerm
从那里,我使用concatMap用户列表,发出他们一个接一个地进入一个新的观察,然后调用githubUserRestService.getUser()为每个用户列表。
然后,这个可观察到的用户就变成了一个用户列表。

在这些网络调用上也定义了一个重试机制
retryWhen()将在抛出IOException时重新尝试可观察的对象。
当用户没有网络时(您可能想要添加一个终止条件,例如仅重新尝试某些次数),就会抛出IOException

您可能会注意到,我在代码中使用lambda表达式,您可以通过使用新的Jack工具链来构建应用程序。
请阅读有关如何在Android上启用Java 8的功能

现在我们有了一个存储库和两个网络调用来获得一个用户列表!我们应该为刚刚编写的代码编写测试

单元测试 — Mockito是什么?

为了对repository对象进行单元测试,我们将使用Mockito
Mockito是什么?
根据MIT许可,它是一个开源的Java开源测试框架。
该框架允许在自动化单元测试中创建测试双对象(模拟对象)。(维基百科)

Mockito允许你stub方法调用,并验证与对象的交互。

当我们编写单元测试时,我们需要考虑单独测试某个组件。
我们不应该测试任何超出这类职责的东西。
Mockito帮助我们实现这一分离

好了,让我们写一些测试吧!

为UserRepositoryImpl编写单元测试

  1. 选择UserRepositoryImpl的类名,并按“ALT+ENTER”键。
    将弹出一个对话框,选择“Create Test”选项。
    选择该选项,将出现一个新的对话框:
  2. 您可以选择生成方法,但我通常会选择未选择的选项。
    然后,它将要求您选择应该放置测试的目录。
    在编写一个不需要Android上下文的JUnit测试时,选择app/src/test目录。
  3. 现在我们已经准备好设置单元测试了。
    要做到这一点,需要创建一个UserRepository对象。
    我们还需要创建一个GithubUserRestService的模拟实例,因为我们不会直接在这个测试中对API进行攻击。
    这个测试将确认在UserRepository中正确地完成了转换。
    下面是设置我们的单元测试的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Mock
    GithubUserRestService githubUserRestService;

    private UserRepository userRepository;

    @Before
    public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
    userRepository = new UserRepositoryImpl(githubUserRestService);
    }
  4. 我们将编写的第一个测试将测试GithubUserRestService是否具有正确的参数。
    它还将测试它是否返回预期的结果。
    下面是我所写的示例测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    @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服务返回某些用户时,当我搜索用户时,结果应该返回并正确地转换。

    我发现测试的命名也很重要。我喜欢遵循的命名结构如下:

    [Name of method under test]_[Conditions of test case]_[Expected Result]

    在这个例子中,方法的名字是

    在本例中,该方法的名称是searchUsers_200OkResponse_InvokesCorrectApiCalls()
    在这个测试用例中,一个TestSubscriber订阅了可观察到的搜索查询。
    然后在TestSubscriber上进行断言,以确保它具有预期的结果。

  5. 下一个单元测试将测试如果一个IOException被搜索服务调用抛出,那么网络调用将被重试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @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被调用两次,而其他的网络调用一次被调用一次。我们还断言订阅者没有终止错误。

UserRepositoryImpl的最终单元测试代码

我已经添加了比上面描述的更多的测试。
它们测试不同的情况,但是它们遵循上一节中描述的相同的概念。
下面是UserRepositoryImpl的完整测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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以显示搜索结果集并为其编写更多的测试。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 朝着牛逼的道路一路狂奔 All Rights Reserved.

访客数 : | 访问量 :