Skip to content

Commit b760820

Browse files
committed
update README
1 parent 7f39642 commit b760820

File tree

1 file changed

+179
-37
lines changed

1 file changed

+179
-37
lines changed

README.md

Lines changed: 179 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# AndroidUnitTest
22

33

4+
45
单元测试是应用程序测试策略中的基本测试,通过对代码进行单元测试,可以轻松地验证单个单元的逻辑是否正确,在每次构建之后运行单元测试,可以帮助您快速捕获和修复因代码更改(重构、优化等)带来的回归问题。本文主要聊聊Android中的单元测试,主要内容如下:
56

67
1. 单元测试的目的以及测试内容
@@ -84,7 +85,7 @@ import static org.hamcrest.core.Is.is;
8485
import static org.junit.Assert.assertThat;
8586
8687
public class EmailValidatorTest {
87-
88+
8889
@Test
8990
public void isValidEmail() {
9091
assertThat(EmailValidator.isValidEmail("name@email.com"), is(true));
@@ -98,6 +99,25 @@ public class EmailValidatorTest {
9899
2. 运行一个测试类中的所有测试方法:打开类文件,在类的范围内右键选择**Run**,或者直接选择类文件直接右键**Run**
99100
3. 运行一个目录下的所有测试类:选择这个目录,右键**Run**
100101

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+
101121
也可以通过命令 ```gradlew test``` 来运行所有的测试用例,这种方式可以添加如下配置,输出单元测试过程中各类测试信息:
102122

103123
```
@@ -112,8 +132,15 @@ android {
112132
}
113133
}
114134
```
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+
115142
- 通过模拟框架模拟依赖,隔离依赖
116-
前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,本地JVM将无法提供这样的环境,这时候模拟框架[Mockito][1]就该上场了
143+
前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,本地JVM将无法提供这样的环境,这时候模拟框架[Mockito][1]就派上用场了
117144

118145
- 一个Context#getString(int)的测试用例
119146
```
@@ -133,16 +160,23 @@ public class MockUnitTest {
133160
//模拟方法调用的返回值,隔离对Android系统的依赖
134161
when(mMockContext.getString(R.string.app_name)).thenReturn(FAKE_STRING);
135162
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());
136166
}
137167
}
138168
```
139169

170+
![read string from context](https://upload-images.jianshu.io/upload_images/3631399-6562a503401d7f03.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
171+
140172
通过模拟框架[Mockito][1],指定调用context.getString(int)方法的返回值,达到了隔离依赖的目的,其中[Mockito][1]使用的是[cglib][2]动态代理技术。
141173

142174

143175
# 五、仪器化测试
144176

145-
通过模拟的手段来隔离Android依赖有时候代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量,但由于要跑到真机或模拟器上,所以会慢一些。
177+
在某些情况下,虽然可以通过模拟的手段来隔离Android依赖,但代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的工作量。
178+
179+
仪器化测试是在真机或模拟器上运行的测试,它们可以利用Android framework APIs 和 supporting APIs。如果测试用例需要访问仪器(instrumentation)信息(如应用程序的Context),或者需要Android框架组件的真正实现(如Parcelable或SharedPreferences对象),那么应该创建仪器化单元测试,由于要跑到真机或模拟器上,所以会慢一些。
146180

147181
- 配置
148182

@@ -162,45 +196,89 @@ android {
162196
}
163197
}
164198
```
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+
```
166226

227+
创建仪器化测试类(app/src/androidTest/java)
167228
```
168229
// @RunWith 只在混合使用 JUnit3 和 JUnit4 需要,若只使用JUnit4,可省略
169230
@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 = "玉刚说";
172235
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;
176237
177238
@Before
178239
public void setUp() {
179-
mLogHistory = new LogHistory();
240+
spDao = new SharedPreferenceDao(App.getContext());
180241
}
181242
182243
@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窗口展示,如下图:
189251

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)
192253

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+
```
196262

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+
```
203271
```
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+
204282
# 六、常用单元测试开源库
205283
#### 1. [Mocktio][1]
206284

@@ -325,11 +403,32 @@ android {
325403
```
326404

327405
- 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/)
329428
```
330429
@RunWith(RobolectricTestRunner.class)
331430
public class MyActivityTest {
332-
431+
333432
@Test
334433
public void clickingButton_shouldChangeResultsViewText() throws Exception {
335434
MainActivity activity = Robolectric.setupActivity(MainActivity.class);
@@ -341,16 +440,57 @@ public class MyActivityTest {
341440
}
342441
}
343442
```
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+
344484
# 七、实践经验
345485

346486
#### 1. 代码中用到了TextUtil.isEmpty()的如何测试
347487
```
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;
353491
}
492+
return EMAIL_PATTERN.matcher(email).matches();
493+
}
354494
```
355495
当你尝试本地测试这样的代码,就会收到一下的异常:
356496
```
@@ -421,7 +561,7 @@ public class Presenter {
421561
public class PresenterTest {
422562
Model model;
423563
Presenter presenter;
424-
564+
425565
@Before
426566
public void setUp() throws Exception {
427567
// mock Model对象
@@ -457,7 +597,7 @@ public class Environment {
457597
public class FileDaoTest {
458598
459599
public static final String TEST_STRING = "Hello Android Unit Test.";
460-
600+
461601
FileDao fileDao;
462602
463603
@Before
@@ -488,16 +628,18 @@ public class FileDaoTest {
488628
[https://github.com/jdqm/AndroidUnitTest][6]
489629

490630
参考资料
491-
492631
https://developer.android.com/training/testing/unit-testing/
493632
https://developer.android.com/training/testing/unit-testing/local-unit-tests
494633
https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests
495634
https://blog.dreamtobe.cn/2016/05/15/android_test/
496635
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
497638

498639
[1]: https://github.com/mockito/mockito
499640
[2]: https://github.com/cglib/cglib
500641
[3]: http://robolectric.org/
501642
[4]: https://github.com/powermock/powermock
502643
[5]: https://upload-images.jianshu.io/upload_images/3631399-11da81c156bed56a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
503644
[6]: https://github.com/jdqm/AndroidUnitTest
645+
[7]: https://developer.android.com/studio/test/command-line?hl=zh-cn

0 commit comments

Comments
 (0)