Android Unit Case

Android单元测试编写原则及相关三方库详解

概念

单元测试只是测试一个方法单元,它不是测试一个整个流程。举一个🌰

一个Login页面,上面有两个EditText和一个Button。两个EditText分别用于输入用户名和密码。点击Button以后,有一个UserManager会去执行performlogin操作,然后将结果返回,更新页面。
那么我们给这个东西做单元测试的时候,不是测这个login流程。这种整个流程的测试:给两个输入框设置正确的用户名和密码,点击login button,最后页面得到更新,叫做 集成测试,而不是单元测试。当然,集成测试是有必要的,但这不是程序员应该花精力的地方。

Test Pyramid

Test Pyramid理论基本大意是,单元测试是基础,是我们应该花绝大多数时间去写的部分,而集成测试等应该是冰山上面能看见的那一小部分

为什么是这样呢?因为集成测试设置起来很麻烦,运行起来很慢,发现的bug少,在保证代码质量、改善代码设计方面更起不到任何作用,因此它的重要程度并不是那么高,也无法将它纳入我们正常的工作流程中。
而单元测试则刚好相反,它运行速度超快,能发现的bug更多,在开发时能引导更好的代码设计,在重构时能保证重构的正确性,因此它能保证我们的代码在一个比较高的质量水平上。同时因为运行速度快,我们很容易把它纳入到我们正常的开发流程中。
至于为什么集成测试发现的bug少,而单元测试发现的bug多,这里也稍作解释,因为集成测试不能测试到其中每个环节的每个方面,某一个集成测试运行正确了,不代表另一个集成测试也能运行正确。而单元测试会比较完整的测试每个单元的各种不同的状况、临界条件等等。一般来说,如果每一个环节是对的,那么在很大的概率上,整个流程就是对的。虽然不能保证整个流程100%一定是对的。所以,集成测试需要有,但应该是少量,单元测试是我们应该花重点去做的事情。

为什么

如何你在编写单元测试的时候发现当前类不好测,说明该类设计有问题

  • 提升软件质量
  • 方便重构
  • 节约时间
  • 提升代码设计

多种工具

JUnit

JUnit4是Java界用的最广泛的一个基础框架。

一个测试方法包括三个部分:

  • setup
  • 执行操作
  • 验证结果

举一个🌰:

public class LoginTest {
Calculator mCalculator;

@Before
public void setup(){
mCalculator = new Calculator();
}

@Test
public void addTest() throws Exception {
int sum = mCalculator.add(1,2);
assertEquals(3,sum);
}

@Test
@Ignore("not implemented yet")
public void multiplyTest() throws Exception {
int product = mCalculator.multiply(2,4);
assertEquals(8,product);
}
}

@Before : 每个测试函数在调用之前都会先调用@Before注解的函数,比如 addTest 运行前会执行 setup , multiplyTest 运行前会执行 setup,类似逻辑的还有@After、@BeforeClass、@AfterClass。即在跑一个测试类的所有测试方法之前,会执行一个被@BeforeClass修饰的函数。

@Ignore : 如果需要忽略某些方法可以使用该注解,例如正式代码还没有实现

@Test(expected = IllegalArgumentException.class) : 表示验证这个测试方法将抛出异常,如果没有抛出的话,则测试失败。

public class Calculator {
public double divide(double divident,double dividor) {
if (dividor == 0) throw new IllegalArgumentException("Dividor can't be 0");
return divident / dividor;
}
}

@Test(expected = IllegalArgumentException.class)
public void test () {
mCalculator.dividor(4,0);
}

Mock/Mockito

Mock是创建一个类的虚假对象,在测试环境中,用来替换真实对象,以达到两个目的:

  • 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么
  • 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

Mockito是最Java界使用最广泛的Mock框架

1.验证方法调用

@Test
public void testLogin() throws Exception {
UserManager mockUserManager = Mockito.mock(UserManager.class);
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.setUserManager(mockUserManager); //<==

loginPresenter.login("xiaochuang", "xiaochuang password");

Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
}

如果需要验证mockUserManager的performLogin()得到了调用,同时参数是”username”,”password”

Mockito.verify(mockUserManager).performLogin("username","password");

当然也可以验证该函数调用次数

Mockito.verify(mockUserManager, Mockito.times(3)).performLogin(...); //验证mockUserManager的performLogin得到了三次调用。
Mockito.verify(mockUserManager, Mockito.atLeast(3)).performLogin(...); //验证mockUserManager的performLogin最少得到了三次调用。
Mockito.verify(mockUserManager).performLogin(Mockito.anyString(),Mockito.anyString()); //并不关心参数,任意参数皆可

2.指定mock对象的某些方法的行为
举一个🌰:

public void login(String username, String password) {
if (username == null || username.length() == 0) return;
//假设我们对密码强度有一定要求,使用一个专门的validator来验证密码的有效性
if (mPasswordValidator.verifyPassword(password)) return; //<==

mUserManager.performLogin(null, password);
}

这里我们需要PasswordValidator来验证密码的有效性,但是这个类的verifyPassword()方法需要联网,其实我们只需要给一些阈值判断即可,因为我们要测的是login(),跟PasswordValidator内部逻辑没有关系,这才是单元测试真正该有的颗粒度,比如可以这么写:

//先创建一个mock对象
PasswordValidator mockValidator = Mockito.mock(PasswordValidator.class);
//当调用mockValidator的verifyPassword方法,同时传入"xiaochuang_is_handsome"时,返回true
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
//当调用mockValidator的verifyPassword方法,同时传入"xiaochuang_is_not_handsome"时,返回false
Mockito.when(validator.verifyPassword("xiaochuang_is_not_handsome")).thenReturn(false);

又比如有如下逻辑:

public void loginCallbackVersion(String username, String password) {
if (username == null || username.length() == 0) return;
//假设我们对密码强度有一定要求,使用一个专门的validator来验证密码的有效性
if (mPasswordValidator.verifyPassword(password)) return;
//login的结果将通过callback传递回来。
mUserManager.performLogin(username, password, new NetworkCallback() { //<==
@Override
public void onSuccess(Object data) {
//update view with data
}
@Override
public void onFailure(int code, String msg) {
//show error msg
}
});
}

想进一步测试传给mUserManager.performLogin的NetworkCallback里面的代码,验证view得到了更新,测试环境里,我们并不想依赖mUserManager.performLogin的真实逻辑,而是让mUserManager直接调用传入的NetworkCallback的onSuccess或onFailure方法,这种指定mock对象执行特定的动作的写法如下:Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args);举一个🌰:

Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
//这里可以获得传给performLogin的参数
Object[] arguments = invocation.getArguments();

//callback是第三个参数
NetworkCallback callback = (NetworkCallback) arguments[2];

callback.onFailure(500, "Server error");
return 500;
}
}).when(mockUserManager).performLogin(anyString(), anyString(), any(NetworkCallback.class));

作者

8MilesRD

发布于

2019-08-06

更新于

2020-01-16

许可协议

评论