1
1
# AndroidUnitTest
2
2
3
3
4
+
4
5
单元测试是应用程序测试策略中的基本测试,通过对代码进行单元测试,可以轻松地验证单个单元的逻辑是否正确,在每次构建之后运行单元测试,可以帮助您快速捕获和修复因代码更改(重构、优化等)带来的回归问题。本文主要聊聊Android中的单元测试,主要内容如下:
5
6
6
7
1 . 单元测试的目的以及测试内容
@@ -84,7 +85,7 @@ import static org.hamcrest.core.Is.is;
84
85
import static org.junit.Assert.assertThat;
85
86
86
87
public class EmailValidatorTest {
87
-
88
+
88
89
@Test
89
90
public void isValidEmail() {
90
91
assertThat(EmailValidator.isValidEmail("name@email.com"), is(true));
@@ -98,6 +99,25 @@ public class EmailValidatorTest {
98
99
2 . 运行一个测试类中的所有测试方法:打开类文件,在类的范围内右键选择** Run** ,或者直接选择类文件直接右键** Run** ;
99
100
3 . 运行一个目录下的所有测试类:选择这个目录,右键** Run** 。
100
101
102
+ - 运行前面测试验证邮箱格式的例子,测试结果会在** Run** 窗口展示,如下图:
103
+
104
+ ![ 本地单元测试-通过] ( https://upload-images.jianshu.io/upload_images/3631399-c21d2da0f7d88b9e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
105
+
106
+ 从结果可以清晰的看出,测试的方法为 ``` EmailValidatorTest ``` 类中的 ``` isValidEmail() ``` 方法,测试状态为passed,耗时12毫秒。
107
+
108
+ 修改一下前面的例子,传入一个非法的邮箱地址:
109
+ ```
110
+ @Test
111
+ public void isValidEmail() {
112
+ assertThat(EmailValidator.isValidEmail("#name@email.com"), is(true));
113
+ }
114
+ ```
115
+
116
+ ![ 本地单元测试-失败] ( https://upload-images.jianshu.io/upload_images/3631399-bfe8506dd335f1ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
117
+
118
+ 测试状态为failed,耗时14毫秒,同时也给出了详细的错误信息:在15行出现了断言错误,错误原因是期望值(Expected)为true,但实际(Actual)结果为false。
119
+
120
+
101
121
也可以通过命令 ``` gradlew test ``` 来运行所有的测试用例,这种方式可以添加如下配置,输出单元测试过程中各类测试信息:
102
122
103
123
```
@@ -112,8 +132,15 @@ android {
112
132
}
113
133
}
114
134
```
135
+ 还是验证邮箱地址格式的例子 ``` gradlew test ``` :
136
+
137
+ ![ gradlew test] ( https://upload-images.jianshu.io/upload_images/3631399-92fe6f1019f445bb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
138
+
139
+ 在单元测试中通过System.out或者System.err打印的也会输出。
140
+
141
+
115
142
- 通过模拟框架模拟依赖,隔离依赖
116
- 前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,本地JVM将无法提供这样的环境,这时候模拟框架[ Mockito] [ 1 ] 就该上场了 。
143
+ 前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,本地JVM将无法提供这样的环境,这时候模拟框架[ Mockito] [ 1 ] 就派上用场了 。
117
144
118
145
- 一个Context#getString(int)的测试用例
119
146
```
@@ -133,16 +160,23 @@ public class MockUnitTest {
133
160
//模拟方法调用的返回值,隔离对Android系统的依赖
134
161
when(mMockContext.getString(R.string.app_name)).thenReturn(FAKE_STRING);
135
162
assertThat(mMockContext.getString(R.string.app_name), is(FAKE_STRING));
163
+
164
+ when(mMockContext.getPackageName()).thenReturn("com.jdqm.androidunittest");
165
+ System.out.println(mMockContext.getPackageName());
136
166
}
137
167
}
138
168
```
139
169
170
+ ![ read string from context] ( https://upload-images.jianshu.io/upload_images/3631399-6562a503401d7f03.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
171
+
140
172
通过模拟框架[ Mockito] [ 1 ] ,指定调用context.getString(int)方法的返回值,达到了隔离依赖的目的,其中[ Mockito] [ 1 ] 使用的是[ cglib] [ 2 ] 动态代理技术。
141
173
142
174
143
175
# 五、仪器化测试
144
176
145
- 通过模拟的手段来隔离Android依赖有时候代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量,但由于要跑到真机或模拟器上,所以会慢一些。
177
+ 在某些情况下,虽然可以通过模拟的手段来隔离Android依赖,但代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量。
178
+
179
+ 仪器化测试是在真机或模拟器上运行的测试,它们可以利用Android framework APIs 和 supporting APIs。如果测试用例需要访问仪器(instrumentation)信息(如应用程序的Context),或者需要Android框架组件的真正实现(如Parcelable或SharedPreferences对象),那么应该创建仪器化单元测试,由于要跑到真机或模拟器上,所以会慢一些。
146
180
147
181
- 配置
148
182
@@ -162,45 +196,89 @@ android {
162
196
}
163
197
}
164
198
```
165
- - Android中Parcelabe的读写操作,这个就不太好模拟
199
+ - Example
200
+ 这里举一个操作SharedPreference的例子,这个例子需要访问Context类以及SharedPreference的具体实现,采用模拟隔离依赖的话代价会比较大,所以采用仪器化测试比较合适。
201
+
202
+ 这是业务代码中操作SharedPreference的实现
203
+ ```
204
+ public class SharedPreferenceDao {
205
+ private SharedPreferences sp;
206
+
207
+ public SharedPreferenceDao(SharedPreferences sp) {
208
+ this.sp = sp;
209
+ }
210
+
211
+ public SharedPreferenceDao(Context context) {
212
+ this(context.getSharedPreferences("config", Context.MODE_PRIVATE));
213
+ }
214
+
215
+ public void put(String key, String value) {
216
+ SharedPreferences.Editor editor = sp.edit();
217
+ editor.putString(key, value);
218
+ editor.apply();
219
+ }
220
+
221
+ public String get(String key) {
222
+ return sp.getString(key, null);
223
+ }
224
+ }
225
+ ```
166
226
227
+ 创建仪器化测试类(app/src/androidTest/java)
167
228
```
168
229
// @RunWith 只在混合使用 JUnit3 和 JUnit4 需要,若只使用JUnit4,可省略
169
230
@RunWith(AndroidJUnit4.class)
170
- @SmallTest
171
- public class LogHistoryAndroidUnitTest {
231
+ public class SharedPreferenceDaoTest {
232
+
233
+ public static final String TEST_KEY = "instrumentedTest";
234
+ public static final String TEST_STRING = "玉刚说";
172
235
173
- public static final String TEST_STRING = "This is a string";
174
- public static final long TEST_LONG = 12345678L;
175
- private LogHistory mLogHistory;
236
+ SharedPreferenceDao spDao;
176
237
177
238
@Before
178
239
public void setUp() {
179
- mLogHistory = new LogHistory( );
240
+ spDao = new SharedPreferenceDao(App.getContext() );
180
241
}
181
242
182
243
@Test
183
- public void logHistory_ParcelableWriteRead() {
184
- mLogHistory.addEntry(TEST_STRING, TEST_LONG);
185
-
186
- // 写数据
187
- Parcel parcel = Parcel.obtain();
188
- mLogHistory.writeToParcel(parcel, mLogHistory.describeContents());
244
+ public void sharedPreferenceDaoWriteRead() {
245
+ spDao.put(TEST_KEY, TEST_STRING);
246
+ Assert.assertEquals(TEST_STRING, spDao.get(TEST_KEY));
247
+ }
248
+ }
249
+ ```
250
+ 运行方式和本地单元测试一样,这个过程会向连接的设备安装apk,测试结果将在Run窗口展示,如下图:
189
251
190
- // 为接下来的读操作,写完数据后需要重置parcel
191
- parcel.setDataPosition(0);
252
+ ![ instrumented test passed] ( https://upload-images.jianshu.io/upload_images/3631399-af13137ec347a9b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
192
253
193
- // 读数据
194
- LogHistory createdFromParcel = LogHistory.CREATOR.createFromParcel(parcel);
195
- List<Pair<String, Long>> createdFromParcelData = createdFromParcel.getData();
254
+ 通过测试结果可以清晰看到状态passed,仔细看打印的log,可以发现,这个过程向模拟器安装了两个apk文件,分别是app-debug.apk和app-debug-androidTest.apk,instrumented测试相关的逻辑在app-debug-androidTest.apk中。简单介绍一下安装apk命令pm install:
255
+ ```
256
+ // 安装apk
257
+ //-t:允许安装测试 APK
258
+ //-r:重新安装现有应用,保留其数据,类似于替换安装
259
+ //更多请参考 https://developer.android.com/studio/command-line/adb?hl=zh-cn
260
+ adb shell pm install -t -r filePath
261
+ ```
196
262
197
- // 验证接收到的数据是否正确
198
- assertThat(createdFromParcelData.size(), is(1));
199
- assertThat(createdFromParcelData.get(0).first, is(TEST_STRING));
200
- assertThat(createdFromParcelData.get(0).second, is(TEST_LONG));
201
- }
202
- }
263
+ 安装完这两个apk后,通过``` am instrument ``` 命令运行instrumented测试用例,该命令的一般格式:
264
+ ```
265
+ am instrument [flags] <test_package>/<runner_class>
266
+ ```
267
+ 例如本例子中的实际执行命令:
268
+ ```
269
+ adb shell am instrument -w -r -e debug false -e class 'com.jdqm.androidunittest.SharedPreferenceDaoTest#sharedPreferenceDaoWriteRead' com.jdqm.androidunittest.test/android.support.test.runner.AndroidJUnitRunner
270
+ ```
203
271
```
272
+ -w: 强制 am instrument 命令等待仪器化测试结束才结束自己(wait),保证命令行窗口在测试期间不关闭,方便查看测试过程的log
273
+ -r: 以原始格式输出结果(raw format)
274
+ -e: 以键值对的形式提供测试选项,例如 -e debug false
275
+ 关于这个命令的更多信息请参考
276
+ https://developer.android.com/studio/test/command-line?hl=zh-cn
277
+ ```
278
+
279
+ 如果你实在没法忍受instrumented test的耗时问题,业界也提供了一个现成的方案[ Robolectric] [ 3 ] ,下一小节讲开源框库的时候会将这个例子改成本地本地测试。
280
+
281
+
204
282
# 六、常用单元测试开源库
205
283
#### 1. [ Mocktio] [ 1 ]
206
284
@@ -325,11 +403,32 @@ android {
325
403
```
326
404
327
405
- Example
328
-
406
+ 模拟打开MainActivity,点击界面上面的Button,读取TextView的文本信息。
407
+
408
+ MainActivity.java
409
+ ```
410
+ public class MainActivity extends AppCompatActivity {
411
+
412
+ @Override
413
+ protected void onCreate(Bundle savedInstanceState) {
414
+ super.onCreate(savedInstanceState);
415
+ setContentView(R.layout.activity_main);
416
+ final TextView tvResult = findViewById(R.id.tvResult);
417
+ Button button = findViewById(R.id.button);
418
+ button.setOnClickListener(new View.OnClickListener() {
419
+ @Override
420
+ public void onClick(View v) {
421
+ tvResult.setText("Robolectric Rocks!");
422
+ }
423
+ });
424
+ }
425
+ }
426
+ ```
427
+ 测试类(app/src/test/java/)
329
428
```
330
429
@RunWith(RobolectricTestRunner.class)
331
430
public class MyActivityTest {
332
-
431
+
333
432
@Test
334
433
public void clickingButton_shouldChangeResultsViewText() throws Exception {
335
434
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
@@ -341,16 +440,57 @@ public class MyActivityTest {
341
440
}
342
441
}
343
442
```
443
+ 测试结果
444
+ ![ Robolectric test passed] ( https://upload-images.jianshu.io/upload_images/3631399-3a0bba49cccc15a1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
445
+
446
+ 耗时917毫秒,是要比单纯的本地测试慢一些。这个例子非常类似于直接跑到真机或模拟器上,然而它只需要跑在本地JVM即可,这都是得益于Robolectric的Shadow。
447
+ > Note: 第一次跑需要下载一些依赖,可能时间会久一点,但后续的测试肯定比仪器化测试打包两个apk并安装的过程快。
448
+
449
+ 在第六小节介绍了通过仪器化测试的方式跑到真机上进行测试SharedPreferences操作,可能吐槽的点都在于耗时太长,现在通过[ Robolectric] [ 3 ] 改写为本地测试来尝试减少一些耗时。
450
+
451
+ 在实际的项目中,Application可能创建时可能会初始化一些其他的依赖库,不太方便单元测试,这里额外创建一个Application类,不需要在清单文件注册,直接写在本地测试目录即可。
452
+ ```
453
+ public class RoboApp extends Application {}
454
+ ```
455
+ 在编写测试类的时候需要通过``` @Config(application = RoboApp.class) ``` 来配置Application,当需要传入Context的时候调用``` RuntimeEnvironment.application ``` 来获取:
456
+ app/src/test/java/
457
+ ```
458
+ @RunWith(RobolectricTestRunner.class)
459
+ @Config(application = RoboApp.class)
460
+ public class SharedPreferenceDaoTest {
461
+
462
+ public static final String TEST_KEY = "instrumentedTest";
463
+ public static final String TEST_STRING = "玉刚说";
464
+
465
+ SharedPreferenceDao spDao;
466
+
467
+ @Before
468
+ public void setUp() {
469
+ //这里的Context采用RuntimeEnvironment.application来替代应用的Context
470
+ spDao = new SharedPreferenceDao(RuntimeEnvironment.application);
471
+ }
472
+
473
+ @Test
474
+ public void sharedPreferenceDaoWriteRead() {
475
+ spDao.put(TEST_KEY, TEST_STRING);
476
+ Assert.assertEquals(TEST_STRING, spDao.get(TEST_KEY));
477
+ }
478
+
479
+ }
480
+ ```
481
+ 像本地此时一样把它跑起来即可。
482
+
483
+
344
484
# 七、实践经验
345
485
346
486
#### 1. 代码中用到了TextUtil.isEmpty()的如何测试
347
487
```
348
- public static boolean isValidEmail(CharSequence email) {
349
- if (TextUtils.isEmpty(email)) {
350
- return false;
351
- }
352
- return EMAIL_PATTERN.matcher(email).matches();
488
+ public static boolean isValidEmail(CharSequence email) {
489
+ if (TextUtils.isEmpty(email)) {
490
+ return false;
353
491
}
492
+ return EMAIL_PATTERN.matcher(email).matches();
493
+ }
354
494
```
355
495
当你尝试本地测试这样的代码,就会收到一下的异常:
356
496
```
@@ -421,7 +561,7 @@ public class Presenter {
421
561
public class PresenterTest {
422
562
Model model;
423
563
Presenter presenter;
424
-
564
+
425
565
@Before
426
566
public void setUp() throws Exception {
427
567
// mock Model对象
@@ -457,7 +597,7 @@ public class Environment {
457
597
public class FileDaoTest {
458
598
459
599
public static final String TEST_STRING = "Hello Android Unit Test.";
460
-
600
+
461
601
FileDao fileDao;
462
602
463
603
@Before
@@ -488,16 +628,18 @@ public class FileDaoTest {
488
628
[ https://github.com/jdqm/AndroidUnitTest ] [ 6 ]
489
629
490
630
参考资料
491
-
492
631
https://developer.android.com/training/testing/unit-testing/
493
632
https://developer.android.com/training/testing/unit-testing/local-unit-tests
494
633
https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests
495
634
https://blog.dreamtobe.cn/2016/05/15/android_test/
496
635
https://www.jianshu.com/p/bc99678b1d6e
636
+ https://developer.android.com/studio/test/command-line?hl=zh-cn
637
+ https://developer.android.com/studio/command-line/adb?hl=zh-cn
497
638
498
639
[ 1 ] : https://github.com/mockito/mockito
499
640
[ 2 ] : https://github.com/cglib/cglib
500
641
[ 3 ] : http://robolectric.org/
501
642
[ 4 ] : https://github.com/powermock/powermock
502
643
[ 5 ] : https://upload-images.jianshu.io/upload_images/3631399-11da81c156bed56a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
503
644
[ 6 ] : https://github.com/jdqm/AndroidUnitTest
645
+ [ 7 ] : https://developer.android.com/studio/test/command-line?hl=zh-cn
0 commit comments