一.JUnit
Java自帶的單元測試工具,用于m跟p層的單元測試,需要了解一些注解
@Before@After@Test等
集成方式
testImplementation 'junit:junit:4.12'
- 關(guān)于JUnit的斷言。
assertTrue 判斷是否為true。
assertFalse 判斷是否為false。
assertSame 判斷引用地址是否相等。
assertNotSame 判斷引用地址是否不相等。
assertNull 判斷是否為null
assertNotNull 判斷是否不為null
assertEquals 判斷是否相等
assertNotEquals 判斷是否不相等
assertThat 條件判斷斷言
上邊說的assertThat,下邊詳細(xì)介紹下
/**數(shù)值匹配**/
//測試變量是否大于指定值
assertThat(test1.getShares(), greaterThan(50));
//測試變量是否小于指定值
assertThat(test1.getShares(), lessThan(100));
//測試變量是否大于等于指定值
assertThat(test1.getShares(), greaterThanOrEqualTo(50));
//測試變量是否小于等于指定值
assertThat(test1.getShares(), lessThanOrEqualTo(100));
//測試所有條件必須成立
assertThat(test1.getShares(), allOf(greaterThan(50),lessThan(100)));
//測試只要有一個(gè)條件成立
assertThat(test1.getShares(), anyOf(greaterThanOrEqualTo(50), lessThanOrEqualTo(100)));
//測試無論什么條件成立(還沒明白這個(gè)到底是什么意思)
assertThat(test1.getShares(), anything());
//測試變量值等于指定值
assertThat(test1.getShares(), is(100));
//測試變量不等于指定值
assertThat(test1.getShares(), not(50));
/**字符串匹配**/
String url = "http://www.taobao.com";
//測試變量是否包含指定字符
assertThat(url, containsString("taobao"));
//測試變量是否已指定字符串開頭
assertThat(url, startsWith("http://"));
//測試變量是否以指定字符串結(jié)尾
assertThat(url, endsWith(".com"));
//測試變量是否等于指定字符串
assertThat(url, equalTo("http://www.taobao.com"));
//測試變量再忽略大小寫的情況下是否等于指定字符串
assertThat(url, equalToIgnoringCase("http://www.taobao.com"));
//測試變量再忽略頭尾任意空格的情況下是否等于指定字符串
assertThat(url, equalToIgnoringWhiteSpace("http://www.taobao.com"));
/**集合匹配**/
List<User> user = new ArrayList<User>();
user.add(test1);
user.add(test2);
//測試集合中是否還有指定元素
assertThat(user, hasItem(test1));
assertThat(user, hasItem(test2));
/**Map匹配**/
Map<String,User> userMap = new HashMap<String,User>();
userMap.put(test1.getUsername(), test1);
userMap.put(test2.getUsername(), test2);
//測試map中是否還有指定鍵值對(duì)
assertThat(userMap, hasEntry(test1.getUsername(), test1));
//測試map中是否還有指定鍵
assertThat(userMap, hasKey(test2.getUsername()));
//測試map中是否還有指定值
assertThat(userMap, hasValue(test2));
關(guān)于匹配的字符串詳情點(diǎn)擊
二.Mockito
所謂的mock就是創(chuàng)建一個(gè)類的虛假的對(duì)象,在測試環(huán)境中,用來替換掉真實(shí)的對(duì)象,以達(dá)到兩大目的:
1.驗(yàn)證這個(gè)對(duì)象的某些方法的調(diào)用情況,調(diào)用了多少次,參數(shù)是什么等等
2.指定這個(gè)對(duì)象的某些方法的行為,返回特定的值,或者是執(zhí)行特定的動(dòng)作
集成方式
testImplementation 'org.mockito:mockito-core:2.23.0'
使用
- 驗(yàn)證方法調(diào)用次數(shù)
User user = Mockito.mock(User.class);
UserManager manager = new UserManager(user);
manager.login("xmq","123456");
Mockito.verify(user,Mockito.times(1)).login(Mockito.anyString(),Mockito.anyString()); //驗(yàn)證User中的login調(diào)用了多少次
private class User {
public void login(String user, String pass) {
System.out.print(user+pass);
}
}
private class UserManager {
private User mUser;
public UserManager(User user) {//這里注意下,對(duì)象是以一種注入的方式
mUser = user;
}
public void login(String user, String pass) {
mUser.login(user,pass);
}
}
- 指定mock對(duì)象的某些方法的行,或者是執(zhí)行特定的動(dòng)作
User user = Mockito.mock(User.class);
Mockito.when(user.isMaster("xmq")).thenReturn(true);
Assert.assertTrue(user.isMaster("xmq"));
class User {
public void login(String user, String pass) {
System.out.print(user+pass);
}
public boolean isMaster(String user) {
return "xmq".equals(user);
}
}
注意:這里有個(gè)問題,若刪除
Mockito.when(user.isMaster("xmq")).thenReturn(true);這一行的話isMaster方法本身傳入?yún)?shù)為xmq時(shí)正常邏輯返回true,可是實(shí)際上是false。這是因?yàn)閙ock如果不指定返回值的話,一個(gè)mock對(duì)象的所有非void方法都將返回默認(rèn)值:int、long類型方法將返回0,boolean方法將返回false,對(duì)象方法將返回null等等;而void方法將什么都不做。
替代方案,使用Spy,spy對(duì)象的方法默認(rèn)調(diào)用真實(shí)的邏輯,mock對(duì)象的方法默認(rèn)什么都不做,或直接返回默認(rèn)值.
User user = Mockito.spy(User.class);
User user = Mockito.spy(new User());
List list = new LinkedList();
List spy = spy(list);
//下邊兩種處理是不一樣的
doReturn("foo").when(spy).get(0); //返回的是 foo
when(spy.get(0)).thenReturn("foo"); //將會(huì)拋出 IndexOutOfBoundsException 的異常,因?yàn)?List 為空
三.Robolectric
用于View層的單元測試,可直接運(yùn)行于JVM上,其實(shí)內(nèi)部是使用了一個(gè)android.jar包,具體原理有時(shí)間再理
- 集成方式
build.gradle 中
android{
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
testImplementation "org.robolectric:robolectric:3.8" //這里4.0以上的需要AndroidStudio 3.2以上才可以
testImplementation "org.robolectric:robolectric-annotations:3.4-rc2"
注意:若出現(xiàn)AndroidManifest.xml找不到的時(shí)候在Edit configurations 中配置Working directory 配置為$MODULE_DIR$
edit configurations -> defaults -> android junit -> working directory選擇$MODULE_DIRS

- 使用方式:這里就不去詳細(xì)介紹每一個(gè)控件的測試方法,就以一個(gè)實(shí)際例子介紹下
public class LoginActivity extends BaseActivity<LoginContract.LoginPresenter> implements LoginContract.LoginView {
@BindView(R.id.tv_login_user_id)
EditText etUserId;
@BindView(R.id.tv_login_user_pass)
EditText etUserPass;
@BindView(R.id.tv_user_name)
TextView tvUserName;
@Override
protected int getLayoutId() {
return R.layout.login_activity;
}
@Override
protected void init() {
}
@OnClick(R.id.btn_login)
public void login() {
//view 可以進(jìn)行一些簡單的邏輯處理,比如盼空校驗(yàn)等,就沒必要交給presenter了
if (TextUtils.isEmpty(etUserId.getText())) {
showToast(getString(R.string.login_user_empt));
return;
}
if (TextUtils.isEmpty(etUserPass.getText())) {
showToast(getString(R.string.login_pass_empy));
return;
}
presenter.login(etUserId.getText().toString(), etUserPass.getText().toString());
}
@Override
protected LoginContract.LoginPresenter createPresenter() {
return new LoginPresenter(new LoginSource());
}
@Override
public void loginSuccess() {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
@Override
public void loginFail() {
//登錄失敗后,可以清空賬號(hào) 密碼 之類的UI操作
etUserPass.setText("登錄失??!");
}
}
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 26)
public class LoginActivityTest {
private EditText etUserId;
private EditText etUserPass;
private Button btnLogin;
private LoginActivity mLoginActivity;
@Rule
public RxJavaTestSchedulerRule mRxJavaTestSchedulerRule = new RxJavaTestSchedulerRule(); //增加Rxjava規(guī)則
@Before
public void setUp() throws Exception {
mLoginActivity = Robolectric.buildActivity(LoginActivity.class).setup().get(); //創(chuàng)建Activity
etUserId = mLoginActivity.findViewById(R.id.tv_login_user_id); //獲取其中的控件
etUserPass = mLoginActivity.findViewById(R.id.tv_login_user_pass);
btnLogin = mLoginActivity.findViewById(R.id.btn_login);
}
@After
public void tearDown() throws Exception {
}
@Test
public void login() {
etUserId.setText("xmq");
etUserPass.setText("123456");
btnLogin.performClick(); //Button的點(diǎn)擊事件
assertEquals("登錄失敗!", ShadowToast.getTextOfLatestToast()); //斷言是否彈出“ 登錄失??!”toast
etUserId.setText("xuser");
etUserPass.setText("Zc123456");
btnLogin.performClick();
assertEquals("登錄成功!", ShadowToast.getTextOfLatestToast());
}
@Test
public void loginSuccess() {
mLoginActivity.loginSuccess();
Intent expectedIntent = new Intent(mLoginActivity, MainActivity.class);
Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
assertEquals(expectedIntent.getComponent(), actualIntent.getComponent()); //斷言Activity跳轉(zhuǎn)是否正確
}
@Test
public void loginFail() {
mLoginActivity.loginFail();
assertEquals("登錄失?。?,etUserPass.getText().toString());
}
}
關(guān)于RxJavaTestSchedulerRule 規(guī)則是將Rxjava異步轉(zhuǎn)為同步
public class RxJavaTestSchedulerRule implements TestRule {
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// ShadowLog.stream = System.out;
LcHttpClientWrapper.getInstance().sync(true);
RxJavaPlugins.reset();
final Scheduler immediate = new Scheduler() {
@Override
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
return super.scheduleDirect(run, 0, unit);
}
@Override
public Worker createWorker() {
return new ExecutorScheduler.ExecutorWorker(new Executor() {
@Override
public void execute(@android.support.annotation.NonNull Runnable runnable) {
runnable.run();
}
});
}
};
RxJavaPlugins.setInitIoSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxJavaPlugins.setInitComputationSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxJavaPlugins.setInitNewThreadSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxJavaPlugins.setInitSingleSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
RxAndroidPlugins.reset();
RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return immediate;
}
});
RxAndroidPlugins.setInitMainThreadSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
@Override
public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
return immediate;
}
});
base.evaluate();
}
};
}
}
自定義shadow:
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
}
@Implements(User.class) //增加關(guān)聯(lián)注解
public class ShadowUser {
@Implementation //重寫的方法
public String getName() {
return "shadowXmq";
}
}
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class , shadows = ShadowUser.class,sdk= 26) //這里需要使用shadow關(guān)聯(lián)shadow對(duì)象
public class ShadowTest {
@Test
public void name() {
User user =new User("xmq");
assertEquals("xmq",user.toString());
assertNotEquals("xmq",user.getName());
}
}
Roboletric詳情點(diǎn)擊
- 生成報(bào)告
./gradlew clean testDebugUnitTest

四.JaCoCo
使用JaCoCo生成測試報(bào)告,Android Instrument Test 中默認(rèn)已經(jīng)集成,但是在Android Unit Test并沒有集成,需要我們手動(dòng)配置gradle
- 使用方式
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.0" //指定jacoco的版本
reportsDir = file("$buildDir/JacocoReport") //指定jacoco生成報(bào)告的文件夾
}
android {
buildTypes {
debug {
//打開覆蓋率統(tǒng)計(jì)開關(guān)
testCoverageEnabled = true
}
}
}
//依賴于testDebugUnitTest任務(wù)
task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
group = "reporting" //指定task的分組
reports {
xml.enabled = true //開啟xml報(bào)告
html.enabled = true //開啟html報(bào)告
}
def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug/com/*/*/*", //指定類文件夾, 這里的路徑需要指定你的包名
includes: ["**/*.*"], //包含類的規(guī)則,這里我們生成所有Presenter類的測試報(bào)告
excludes: ['**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*']) //排除類的規(guī)則
def mainSrc = "${project.projectDir}/src/main/java" //指定源碼目錄
sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec") //指定報(bào)告數(shù)據(jù)的路徑
}
執(zhí)行代碼生成報(bào)告:
./gradlew clean jacocoTestReport

其他:
- 1.配置日志輸出
unitTests.all{
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
outputs.upToDateWhen { false }
showStandardStreams = true
}
}
總結(jié):
單元測試本身是對(duì)代碼質(zhì)量的一種把控,當(dāng)我們case越多,覆蓋的代碼率越高,出現(xiàn)異常的情況就會(huì)越少。以上中P層的代碼更加注重代碼的邏輯,所以驗(yàn)證時(shí)以View層是否被調(diào)用為準(zhǔn);View層以View的變化為準(zhǔn),比如是否彈出正確toast、某一個(gè)控件的String是否發(fā)生變化、Activity是否跳轉(zhuǎn)等等