Android单元测试编写原则及相关三方库详解
概念
单元测试只是测试一个方法单元,它不是测试一个整个流程。举一个🌰
一个Login页面,上面有两个EditText和一个Button。两个EditText分别用于输入用户名和密码。点击Button以后,有一个UserManager会去执行performlogin操作,然后将结果返回,更新页面。
那么我们给这个东西做单元测试的时候,不是测这个login流程。这种整个流程的测试:给两个输入框设置正确的用户名和密码,点击login button,最后页面得到更新,叫做 集成测试,而不是单元测试。当然,集成测试是有必要的,但这不是程序员应该花精力的地方。
Test Pyramid
Test Pyramid理论基本大意是,单元测试是基础,是我们应该花绝大多数时间去写的部分,而集成测试等应该是冰山上面能看见的那一小部分。
为什么是这样呢?因为集成测试设置起来很麻烦,运行起来很慢,发现的bug少,在保证代码质量、改善代码设计方面更起不到任何作用,因此它的重要程度并不是那么高,也无法将它纳入我们正常的工作流程中。
而单元测试则刚好相反,它运行速度超快,能发现的bug更多,在开发时能引导更好的代码设计,在重构时能保证重构的正确性,因此它能保证我们的代码在一个比较高的质量水平上。同时因为运行速度快,我们很容易把它纳入到我们正常的开发流程中。
至于为什么集成测试发现的bug少,而单元测试发现的bug多,这里也稍作解释,因为集成测试不能测试到其中每个环节的每个方面,某一个集成测试运行正确了,不代表另一个集成测试也能运行正确。而单元测试会比较完整的测试每个单元的各种不同的状况、临界条件等等。一般来说,如果每一个环节是对的,那么在很大的概率上,整个流程就是对的。虽然不能保证整个流程100%一定是对的。所以,集成测试需要有,但应该是少量,单元测试是我们应该花重点去做的事情。
为什么
如何你在编写单元测试的时候发现当前类不好测,说明该类设计有问题
多种工具
JUnit
JUnit4是Java界用的最广泛的一个基础框架。
一个测试方法包括三个部分:
举一个🌰:
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(...); Mockito.verify(mockUserManager, Mockito.atLeast(3)).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; if (mPasswordValidator.verifyPassword(password)) return;
mUserManager.performLogin(null, password); }
|
这里我们需要PasswordValidator来验证密码的有效性,但是这个类的verifyPassword()方法需要联网,其实我们只需要给一些阈值判断即可,因为我们要测的是login(),跟PasswordValidator内部逻辑没有关系,这才是单元测试真正该有的颗粒度,比如可以这么写:
PasswordValidator mockValidator = Mockito.mock(PasswordValidator.class);
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
Mockito.when(validator.verifyPassword("xiaochuang_is_not_handsome")).thenReturn(false);
|
又比如有如下逻辑:
public void loginCallbackVersion(String username, String password) { if (username == null || username.length() == 0) return; if (mPasswordValidator.verifyPassword(password)) return; mUserManager.performLogin(username, password, new NetworkCallback() { @Override public void onSuccess(Object data) { } @Override public void onFailure(int code, String 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 { Object[] arguments = invocation.getArguments();
NetworkCallback callback = (NetworkCallback) arguments[2]; callback.onFailure(500, "Server error"); return 500; } }).when(mockUserManager).performLogin(anyString(), anyString(), any(NetworkCallback.class));
|