申明
- 本片文章大主要就是手动写一遍用来加深下印象,真想学技术还是移步下面的资料链接;
- 参考资料:
https://blog.csdn.net/kk_lzvvkpj/article/details/124759719 Android 单元测试只看
//www.greatytc.com/p/7d602a9f85e3 Mockito的使用
这一篇就够了
单元测试的目的以及测试内容
- 目的当然是提前找出问题,提高程序健壮性
- 单元测试要测什么?
- 列出想要测试覆盖的正常、异常情况,进行测试验证;
- 性能测试,例如某个算法的耗时等等。
- 单元测试的分类
- 本地测试(Local tests): 只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的[Mockito][1];
- 仪器化测试(Instrumented tests): 在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式。
JUnit 注解
-
了解一些JUnit注解,有助于更好理解后续的内容。
本地测试
- 根据单元有没有外部依赖(如Android依赖、其他单元的依赖),将本地测试分为两类,首先看看没有依赖的情况
添加依赖
//单元测试的依赖库
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
// Optional -- Mockito framework(可选,用于模拟一些依赖对象,以达到隔离依赖的效果)
androidTestImplementation 'org.mockito:mockito-core:2.19.0'
单元测试代码位置
- java.package目录下是仪器化单元测试,UI测试
-
test.java.package目录下是本地单元测试
创建测试类
- 手动创建也可以自动生成
-
在类名上ALT+ENTER弹出选择选项
- 其中勾选setUp/tearDown是会自动创建一个setUp()/ tearDown() 的空方法
public class TimeUtilsTest extends TestCase {
public void setUp() throws Exception {
super.setUp();
}
public void tearDown() throws Exception {
}
}
简单例子
- 判断谁否是收集号码的方法
public class PhoneUtils {
/**
* 判断是否是手机号
*/
public static boolean assertPhone(String phoneNum){
return phoneNum.matches("^1[3-9][0-9]{9}");
}
public static boolean assertPhone2(String phoneNum){
return phoneNum.matches("^1[3-9]\\d{9}");
}
}
- 单元测试用例和效果
- 正确输入和反馈
public void testAssertPhone() {
// assertEquals(PhoneUtils.assertPhone("12871650177"),true);
assertEquals(PhoneUtils.assertPhone("13871650177"),true);
}
- 错误输入和反馈
public void testAssertPhone() {
assertEquals(PhoneUtils.assertPhone("12871650177"),true);
// assertEquals(PhoneUtils.assertPhone("13871650177"),true);
}
- 可以很直观的告诉你测试是否通过,即你运行的结果和你的预期是否符合
- 其中使用中出现的一个问题:
Method d in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.
java.lang.RuntimeException: Method d in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.util.Log.d(Log.java)
at com.southwind.module_common.SouthWindLog.d(SouthWindLog.java:51)
at com.southwind.module_common.SouthWindLogTest.testTestD1(SouthWindLogTest.java:39)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
...
//上述错误的原因是方法里面调用了android.util.Log包里面的静态方法
//解决办法就是在你自动生成的before方法里面mock一下这个静态类
@Override
protected void setUp() throws Exception {
super.setUp();
mockStatic(Log.class);
}
//再次运行就能正常了,这个就是接下来要说的隔离依赖;
通过模拟框架模拟依赖,隔离依赖
- 前面验证邮件格式的例子,本地JVM虚拟机就能提供足够的运行环境,但如果要测试的单元依赖了Android框架,比如用到了Android中的Context类的一些方法,又比如上述问题中的Log类的调用,本地JVM将无法提供这样的环境,这时候模拟框架[Mockito][1]就派上用场了。
Mockito的使用
- 首先是添加依赖
// required if you want to use Mockito for unit tests
testCompile 'org.mockito:mockito-core:2.7.22'
// required if you want to use Mockito for Android tests
androidTestCompile 'org.mockito:mockito-android:2.7.22'
- 然后是使用Mockito创建mock对象
- 使用静态方法mock()
- 使用注解@Mock
使用注解
@Mock
Activity activityMock;
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Test
public void testD() {
SouthWindLog.d(activityMock,"测试");
}
- 啊,使用失败了哈哈哈....这里的activityMock总是null
使用静态方法哈
@Mock
Activity activityMock;
@Test
public void testD() {
activityMock = Mockito.mock(Activity.class);
when(activityMock.getString(R.string.log_d)).thenReturn("单元测试日志");
SouthWindLog.d(activityMock);
//verify是判断方法是否被调用过
verify(activityMock).getString(R.string.log_d);
}
- 这个倒是成功了,但是说实话这个测试其实测试了个寂寞,因为它没有具体返回,其实不好判断,道理运行正确与否;
配置模拟对象
- Mockito可以通过自然的API来实现模拟对象的返回值.没有指定的方法调用返回空值
"when thenReturn"和"when thenThrow"
- 模拟对象可以根据传入方法中的参数来返回不同的值, when(….).thenReturn(….)方法是用来根据特定的参数来返回特定的值.
//强行指定1和2是相等的。
public void testMockObject(){
PhoneUtils phoneUtils = mock(PhoneUtils.class);
when(phoneUtils.isEqules(1,2)).thenReturn(true);
assertEquals(phoneUtils.isEqules(1,2),false);
}
//结果就是不通过哈哈
//返回多个值
Iterator<Integer> i = mock(Iterator.class);
when(i.next()).thenReturn(11).thenReturn(12);
String res = i.next() + "/" + i.next();
assertEquals(res,"11/12");
- when() thenThrow()可以用来抛出异常;当然运行就抛出自己定义的异常了;
PhoneUtils phoneUtils = mock(PhoneUtils.class);
when(phoneUtils.divideInput(0)).thenThrow(new Exception("0不能做除数"));
try {
phoneUtils.divideInput(0);
fail("0做除数啦,夭寿啦");
}catch (Exception e){
}
"doReturn when" 和 "doThrow when"
- 这个和上面的差不多;
使用Spy包装实例
- 可以使用@Spy注解 或者 spy() 方法来包装一个真实的对象. 除非有特殊的指定,否则每次调用都会委托给该对象
public void testSpy(){
ArrayList<String> list = new ArrayList<>();
ArrayList<String> spy = Mockito.spy(list);
// when(spy.get(0)).thenReturn("第一个元素");
// assertEquals("第一个元素",spy.get(0));
doReturn("第一个元素").when(spy).get(0);
assertEquals("第一个元素",spy.get(0));
}
- 注意:在使用Spy包装真实对象时使用when(….).thenReturn(….)将无效,必须使用 doReturn(…).when(…)来进行插桩.上面注释掉的部分使用直接报错了;
验证模拟对象的调用
- Mockito将会追踪所有方法的调用和传入模拟对象的参数.你可以在模拟对象上使用verify()方法验证指定的条件是否满足.例如,你可以验证是否使用某些参数调用了方法.这种测试称为行为测试.行为测试并不能检查方法调用的结果,但是它可以验证一个方法是否使用正确的参数被调用
public void testVerify(){
PhoneUtils phoneUtils = mock(PhoneUtils.class);
when(phoneUtils.divideInput(5)).thenReturn(2);
phoneUtils.divideInput(10);
phoneUtils.isEqules(1,2);
phoneUtils.isEqules(2,2);
phoneUtils.isEqules(2,2);
verify(phoneUtils).divideInput(ArgumentMatchers.eq(10));//期望入参是10的调用
//verify(phoneUtils).divideInput(ArgumentMatchers.eq(5));//报错了,没有调用入参是5的情况
//判断方法跑了多少次
verify(phoneUtils,times(2)).isEqules(2,2);
//verify(phoneUtils,times(3)).isEqules(2,2);//这里报错,说跑了两次,但是期望的是三次
//判断是否从没出现后面的调用情况
verify(phoneUtils,never()).isEqules(0,1);
verify(phoneUtils, atLeastOnce()).isEqules(1,2);
verify(phoneUtils, atLeast(1)).isEqules(1,2);//至少被调用1次
verify(phoneUtils, atMost(3)).isEqules(1,2);//至多调用了3次
//下面的方法用来检查是否所有的用例都涵盖了,如果没有将测试失败
//放在所有的测试后面
verifyNoMoreInteractions(phoneUtils);
}
Answer
- 在写测试用例时针对复杂的方法结果往往会使用Answer.虽然使用thenReturn可以每次返回一个预定义的值,但是通过answers可以让你的插桩方法(stubbed method)根据参数计算出结果.
- 其实原作者的没怎么看懂,看起来和doReturen..when差不多。可能是我还没有理解吧;
- 没问题了,只要跑一次就知道了使用doReturn..when是会报错的,但是answer能通过测试
- 还发现了一点,所有的设定是会被最新的覆盖的,下面的输出是first,所以前面不论设置了多少answer只有最后一个有效哈;
public void testAnswer(){
TestAnswer testAnswer = mock(TestAnswer.class);
doAnswer(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
// doReturn(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
when(testAnswer.add(anyString(),anyString())).thenAnswer(returnsSecondArg());
when(testAnswer.add(anyString(),anyString())).then(returnsFirstArg());
System.out.println(testAnswer.add("first","second"));
}
- 有时候会传入回调方法作为参数
public void testAnswer() {
TestAnswer testAnswer = mock(TestAnswer.class);
// doAnswer(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
//// doReturn(returnsFirstArg()).when(testAnswer).add(anyString(),anyString());
// when(testAnswer.add(anyString(),anyString())).thenAnswer(returnsSecondArg());
// when(testAnswer.add(anyString(),anyString())).then(returnsFirstArg());
// System.out.println(testAnswer.add("first","second"));
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
//invocation这个是存放你参数的容易,我第三个参数是回调函数,所以下面下标要写2
CallBack callBack = invocation.getArgument(2);
callBack.back("callback");
return "return";
}
}).when(testAnswer).add(anyString(), anyString(), any(CallBack.class));
String result = testAnswer.add("1", "2", new CallBack() {
@Override
public void back(String back) {
System.out.println(back);
}
});
System.out.println(result);
}
class TestAnswer {
public String add(String first, String second, CallBack callBack) {
callBack.back("");
return "";
}
public String add(String first, String second) {
return "";
}
}
interface CallBack {
void back(String back);
}
//最后打印的是 符合我再模拟中提供的数据
...
...
> Task :module_common:testDebugUnitTest
callback
return
BUILD SUCCESSFUL in 2s
16 actionable tasks: 3 executed, 13 up-to-date
- 上面注意的是when...xxx方法都是建立再原方法是有返回的基础上,没有返回就不能这么写;
总结
- 写起来还挺费劲的。
- 考虑可读性:对于方法名使用表达能力强的方法名,对于测试范式可以考虑使用一种规范, 如 RSpec-style。方法名可以采用一种格式,如: [测试的方法][测试的条件][符合预期的结果]。
- 不要使用逻辑流关键字:比如(If/else、for、do/while、switch/case),在一个测试方法中,如果需要有这些,拆分到单独的每个测试方法里。
- 测试真正需要测试的内容:需要覆盖的情况,一般情况只考虑验证输出(如某操作后,显示什么,值是什么)。
- 不需要考虑测试private的方法:将private方法当做黑盒内部组件,测试对其引用的public方法即可;不考虑测试琐碎的代码,如getter或者setter。
- 每个单元测试方法,应没有先后顺序:尽可能的解耦对于不同的测试方法,不应该存在Test A与Test B存在时序性的情况。
- 感觉单元测是麻烦的很,其实普通开发哪有写这东西,都是丢给测试直接真机测试。不过对于数据准确性要求很高的这个还是很有用的,比如零售相关;还有那种计算多,数据复杂的也很好;