Android单元测试

213 阅读5分钟

概念

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

一个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));