一、项目单元测试环境配置
gradle配置:
dependencies {
// junit4
testImplementation 'junit:junit:4.12'
// mokito
testImplementation "org.mockito:mockito-core:2.8.9"
testImplementation "org.mockito:mockito-android:2.8.9”
// powermokito
testImplementation "org.powermock:powermock-module-junit4:1.7.1"
testImplementation "org.powermock:powermock-api-mockito2:1.7.1"
testImplementation 'org.powermock:powermock-core:1.7.1'
testImplementation "org.powermock:powermock-module-junit4-rule:1.7.1"
testImplementation "org.powermock:powermock-classloading-xstream:1.7.1"
//robolectric
testImplementation "org.robolectric:robolectric:3.3.1"
}
抽取单测基类:
1)纯java单测基类
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "AndroidManifest.xml", sdk = 21, application = ApplicationStub.class)
public abstract class BaseJavaTest {
private int androidSdkVersion;
protected static void log(String msg) {
System.out.println(msg);
}
@Before
public void init() {
// 输出日志
MockitoAnnotations.initMocks(this);
androidSdkVersion = VERSION.SDK_INT;
}
/**
* hook执行前,测试log
*/
@Before
public void startLog() {
log("======= start =======");
}
/**
* hook执行后,测试log
*/
@After
public void endLog() throws NoSuchFieldException, IllegalAccessException {
log("======= end =======");
mockVersionSdkIntReturn(androidSdkVersion);
}
//基类可以封装一些通用的单测方法
/**
* mock sdk 版本
*/
protected void mockVersionSdkIntReturn(int apiVersion) throws NoSuchFieldException, IllegalAccessException {
Field sdkInt = VERSION.class.getField("SDK_INT");
sdkInt.setAccessible(true);
sdkInt.set(null, apiVersion);
}
...
}
2)PowerMock基类
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "AndroidManifest.xml", sdk = 21, application = ApplicationStub.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.powermock.*"})
public abstract class BasePowerMockTest {
private int androidSdkVersion;
//解决powermock和robolectric兼容性
@Rule
public PowerMockRule rule = new PowerMockRule();
protected static void log(String msg) {
System.out.println(msg);
}
@Before
public void init() {
// 输出日志
MockitoAnnotations.initMocks(this);
androidSdkVersion = VERSION.SDK_INT;
}
/**
* hook执行前,测试log
*/
@Before
public void startLog() {
log("======= start =======");
}
/**
* hook执行后,测试log
*/
@After
public void endLog() {
log("======= end =======");
try {
mockVersionSdkIntReturn(androidSdkVersion);
} catch (Exception e) {
e.printStackTrace();
}
}
protected void mockVersionSdkIntReturn(int apiVersion) throws NoSuchFieldException, IllegalAccessException {
Field sdkInt = VERSION.class.getField("SDK_INT");
sdkInt.setAccessible(true);
sdkInt.set(null, apiVersion);
}
...
}
二、可测代码设计原则
- 充分利用mvp模式;
- 一个函数只做一件事情,不要把几件事揉在一起;
- 通过将查询和行为分离,可以方便对查询做测试;
- 将纯逻辑部分拆分为独立函数,方便测试;
- 函数的依赖尽量通过参数来传递;
- 尽量不要把复杂结构当做参数传递给函数。
好的代码,能极大的避免mock,降低单测书写难度,因此单测某种程度上也能反过来倒逼程序员写出更优秀的代码。
三、Android单测实战场景
mock场景的单测主要包括三大步:构建对象 + 打桩 + 验证行为
3.1 构建对象
-
new
常规初始化对象 -
mock
构建空实现对象 -
spy
构建具体实现对象
注:
类没有依赖且对象好构建,那可以选择new来初始化对象,否则使用mock/spy。前者类对外部依赖较多,只关新少数函数的具体实现;后者类对外依赖较少,关心大部分函数的具体实现。
3.2 打桩
mock对象之后的后续函数操作,doCallRealMethod()、doReturn()、thenReturn()、doNothing()等是比较常用的打桩方法。
1)public/protected/default方法:
mock类执行真实方法
AppManagerModule appManagerModule = Mockito.mock(AppManagerModule.class);
Mockito.doCallRealMethod().when(appManagerModule).isIgnoreApp("22");
boolean res = appManagerModule.isIgnoreApp("22");
Assert.assertFalse(res);
公有方法返回修改值
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Context context = Mockito.mock(Context.class);
ActivityManager activityManager = Mockito.mock(ActivityManager.class);
Mockito.doReturn(activityManager).when(context).getSystemService(Mockito.eq(Context.ACTIVITY_SERVICE));
2)private/static/final方法
mock类执行真实方法
AppManagerModule mockAppManagerModule = PowerMockito.mock(AppManagerModule.class);
doCallRealMethod().when(mockAppManagerModule).getDownloadingApkCount();
mockAppManagerModule.getDownloadingApkCount();
静态方法返回修改值
List downloadInfoList = DownloadProxy.getInstance().getDownloadInfoList(DownloadType.APK, true);
DownloadProxy downloadProxy = PowerMockito.mock(DownloadProxy.class);
//类的@PrepareForTest需要添加DownloadProxy.class
PowerMockito.mockStatic(DownloadProxy.class);
//DownloadProxy.getInstance()默认返回mock的downloadProxy实例
PowerMockito.when(DownloadProxy.getInstance()).thenReturn(downloadProxy);
//构建List
ArrayList downloadInfos = new ArrayList<>();
DownloadInfo downloadInfo = new DownloadInfo();
downloadInfos.add(downloadInfo);
//返回设置为构建的list
PowerMockito.when(downloadProxy.getDownloadInfoList(Mockito.any(SimpleDownloadInfo.DownloadType.class),Mockito.anyBoolean())).thenReturn(downloadInfos);
私有方法返回修改值
public class MockPrivateObjectClass {
public String stepName;
public MockPrivateObjectClass() {
}
private void setStepName() {
System.out.print("enter setStepName");
stepName = "has set name";
}
public void testStepName() {
setStepName();
System.out.print(stepName);
}
}
@Test
public void replacePrivateMethodTest() {
MockPrivateObjectClass objectClass = new MockPrivateObjectClass();
PowerMockito.replace(PowerMockito.method(MockPrivateObjectClass.class, "setStepName")).with(new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
Whitebox.setInternalState(o, "stepName", "modify step name");
return null;
}
});
objectClass.testStepName();
}
invoke对象私有方法
ObjectWhiteBoxClass objectWhiteBoxClass = new ObjectWhiteBoxClass();
Whitebox.invokeMethod(objectWhiteBoxClass, “addObject", "test1");
Assert.assertEquals(1, objectWhiteBoxClass.getObjectList().size());
3.3 验证行为
验证返回值:
Assert.assertFalse、Assert.assertEquals等
Assert.assertEquals("0", mockAppManagerModule.getPkgScanStatus());
Assert.assertFalse(appManagerModule.isIgnoreApp("22"););
验证方法被调用及其频率:(要求对象是mock对象)
Mockito.verify、PowerMotiko.verifyStatic、Mockito.verifyPrivate等
public/protected/default方法:
NormalClassB b = new MockNormalClassB();
NormalClassB mockB = Mockito.spy(b);
Mockito.verify(mockB).getName();
Mockito.verify(triggerManager, Mockito.times(1)).showDesktopWindowLocked(
Mockito.any(Context.class), Mockito.any(DesktopWinTrigger.class),
Mockito.any(DesktopWinCardInfo.class),
Mockito.anyLong(), Mockito.anyInt());
static方法:
PowerMockito.verifyStatic(Mockito.times(1));
ToastUtils.show(null, 0x7632343, Toast.LENGTH_SHORT);
private方法:
NormalClassA classA = new NormalClassA();
NormalClassA mockClassA = PowerMockito.spy(classA);
PowerMockito.verifyPrivate(mockClassA, times(1)).invoke("privateAdd", Mockito.anyInt(), Mockito.anyInt());
四、常见报错问题处理
持续更新中…..