美文网首页
单元测试&依赖注入

单元测试&依赖注入

作者: shuixingge | 来源:发表于2016-05-19 14:39 被阅读1348次

    本文仅为学习笔记;不是原创文章

    参考资料1
    参考资料2

    一:单元测试基本概念

    概念:单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码。

    
    
    public class Calculator {
        public int add(int one, int another) {
            //为了简单起见,暂不考虑溢出等情况。
            return one + another;
        }
    }
    

    测试Calculator类的add()方法所编写的单元测试代码

    public class CalculatorTest {
        public void testAdd() throws Exception {
            Calculator calculator = new Calculator();
            int sum = calculator.add(1, 2);
            Assert.assertEquals(3, sum);
        }
    }
    

    一个方法对应的测试方法主要分为3部分
    setup:一般是new出要测试的类,以及其他一些前提条件的设置:Calculator calculator = new Calculator();
    执行操作:一般是调用要测试的方法,获得运行结果:int sum = calculator.add(1, 2);
    验证结果: 验证得到的结果跟预期中是一样的:Assert.assertEquals(3, sum);

    单元测试和集成测试:
    单元测试只是测试一个方法单元,它不是测试一整个流程。
    集成测试是整个流程的测试

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

    Test Pyramid理论:

    单元测试:
    对软件质量的提升
    方便重构
    节约时间
    提升代码设计

    二:单元测试框架:Junit使用

    public class Calculator {
        public int add(int one, int another) {
            // 为了简单起见,暂不考虑溢出等情况。
            return one + another;
        }
     
        public int multiply(int one, int another) {
            // 为了简单起见,暂不考虑溢出等情况。
            return one * another;
        }
    }
    

    不使用单元测试框架的代码的测试代码;

    public class CalculatorTest {
        public static void main(String[] args) {
            Calculator calculator = new Calculator();
            int sum = calculator.add(1, 2);
            if(sum == 3) {
                System.out.println("add() works!")
            } else {
                System.out.println("add() does not works!")
            }
     
            int product = calculator.multiply(2, 4);
            if (product == 8) {
                System.out.println("multiply() works!")
            } else {
                System.out.println("multiply() does not works!")
            }
        }
    }
    

    缺点:造成代码臃肿;

    使用Junit进行单元测试;通过@Test注解来标志的需要测试的方法;
    下面的代码的缺陷:需要两次 Calculator calculator = new Calculator();,造成重复性的对象生成;

    public class CalculatorTest {
     
            public void testAdd()  throws Exception {
            Calculator calculator = new Calculator();
            int sum = calculator.add(1, 2);
            Assert.assertEquals(3, sum);
        }
     
        public void testMultiply() throws Exception {
            Calculator calculator = new Calculator();
            int product = calculator.multiply(2, 4);
            Assert.assertEquals(8, product);
        }
     
    }
    

    解决上面代码的问题: @Before
    @Before: 被@Before修饰过了,那么在每个测试方法调用之前,这个方法都会得到调用。
    @After: 就是每个测试方法运行结束之后,会得到运行的方法。

    public class CalculatorTest {
        Calculator mCalculator;
    
        @Before
        public void setup() {
            mCalculator = new Calculator();
        }
    
       @Test
        public void testAdd() throws Exception {
            int sum = mCalculator.add(1, 2);
            assertEquals(3, sum);  //为了简洁,往往会static import Assert里面的所有方法。
        }
    
        @Test
        public void testMultiply() throws Exception {
            int product = mCalculator.multiply(2, 4);
            assertEquals(8, product);
        }
    
    }
    

    @BeforeClass和@AfterClass:
    @BeforeClass的作用是,在跑一个测试类的所有测试方法之前,会执行一次被@BeforeClass修饰的方法,执行完所有测试方法之后,会执行一遍被@AfterClass修饰的方法。这两个方法可以用来setup和release一些公共的资源,需要注意的是,被这两个annotation修饰的方法必须是静态的。

    JUnit的其他功能:
    1 Ignore一些测试方法:
    很多时候,因为某些原因(比如正式代码还没有实现等),我们可能想让JUnit忽略某些方法,让它在跑所有测试方法的时候不要跑这个测试方法。

    public class CalculatorTest {
        Calculator mCalculator;
    
        @Before
        public void setup() {
            mCalculator = new Calculator();
        }
    
        // Omit testAdd() and testMultiply() for brevity
    
     @Test
        @Ignore("not implemented yet")
        public void testFactorial() {
        }
    }
    

    2 验证方法会抛出某些异常
    示例代码

    public class Calculator {
     
        // Omit testAdd() and testMultiply() for brevity
     
        public double divide(double divident, double dividor) {
            if (dividor == 0) throw new IllegalArgumentException("Dividor cannot be 0");
     
            return divident / dividor;
        }
    }
    

    测试当传入的除数是0的时候,这个方法应该抛出IllegalArgumentException异常
    在Junit中,可以通过给**@Test **annotation传入一个expected参数来达到这个目的

    public class CalculatorTest {
        Calculator mCalculator;
     
        @Before
        public void setup() {
            mCalculator = new Calculator();
        }
     
        // Omit testAdd() and testMultiply() for brevity
     
        @Test(expected = IllegalArgumentException.class)
        public void test() {
            mCalculator.divide(4, 0);
        }
     
    }
    
    

    三: 单元测试框架Mock和Mockito

    Mock的引出:怎么测试login方法?

    public class LoginPresenter {
        private UserManager mUserManager = new UserManager();
     
        public void login(String username, String password) {
            if (username == null || username.length() == 0) return;
            if (password == null || password.length()  6) return;
     
            mUserManager.performLogin(username, password);
        }
     
    }
    

    使用JUnit进程测试,编写的测试方法
    问题: login()没有返回值,无法通过assertEquals来测试方法;

    public class LoginPresenterTest {
     
        @ Test
        public void testLogin() throws Exception {
            LoginPresenter loginPresenter = new LoginPresenter();
            loginPresenter.login("xiaochuang", "xiaochuang password");
     
            //验证LoginPresenter里面的mUserManager的performLogin()方法得到了调用,同时参数分别是“xiaochuang”、“xiaochuang password”
            ...
        }
    }
    

    Mock的概念:
    所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的;
    1 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
    2 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作;
    Mockito框架的使用

    public class LoginPresenterTest {
     
        @ Test
        public void testLogin() throws Exception {
            Mockito.mock(UserManager.class);
            LoginPresenter loginPresenter = new LoginPresenter();
            loginPresenter.login("xiaochuang", "xiaochuang password");
     
            UserManager userManager = loginPresenter.getUserManager();
            Mockito.verify(userManager).performLogin("xiaochuang", "xiaochuang password");  //
        }
    }
    
    

    上面代码会报错
    报错原因:传给Mockito.verify()的参数必须是一个mock对象,而我们传进去的不是一个mock对象,所以出错了

    报错
    修正:
    public class LoginPresenterTest {
    
        @ Test
        public void testLogin() throws Exception {
            UserManager mockUserManager = Mockito.mock(UserManager.class);  //
            LoginPresenter loginPresenter = new LoginPresenter();
    
            loginPresenter.login("xiaochuang", "xiaochuang password");
    
            Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");  //
        }
    }
    

    还是会报错
    原因:UserManager mockUserManager = Mockito.mock(UserManager.class);
    的确给我们创建了一个mock对象,保存在mockUserManager
    里面。然而,当我们调用loginPresenter.login("xiaochuang", "xiaochuang password");
    的时候,用到的mUserManager依然是使用new UserManager()
    创建的正常的对象。而mockUserManager
    并没有得到任何的调用,因此,当我们验证它的performLogin()
    方法得到了调用时

    报错

    修正: 在login之前加入setter();

    public class LoginPresenter {
     
        private UserManager mUserManager = new UserManager();
     
        public void login(String username, String password) {
            if (username == null || username.length() == 0) return;
            if (password == null || password.length()  6) return;
     
            mUserManager.performLogin(username, password);
        }
     
        public void setUserManager(UserManager userManager) {  //
            this.mUserManager = userManager;
        }
     
    }
    
    @ 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");
    }
    

    总结:
    1 Mockito.mock()并不是mock一整个类,而是根据传进去的一个类,mock出属于这个类的一个对象,并且返回这个mock对象;而传进去的这个类本身并没有改变,用这个类new出来的对象也没有受到任何改变;

    2 mock出来的对象并不会自动替换掉正式代码里面的对象,你必须要有某种方式把mock对象应用到正式代码里面

    Mockito的使用

    1. 验证方法调用
      验证mockUserManager的performLogin()得到了调用
    Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
    

    等价于

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

    验证mockUserManager的performLogin得到了三次调用。

    Mockito.verify(mockUserManager, Mockito.times(3)).performLogin(...); 
    

    可以输入任何参数

    Mockito.verify(mockUserManager).performLogin(Mockito.anyString(), Mockito.anyString());
    
    1. 指定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);
    }
    

    在测试的环境下我们想简单处理,指定让它直接返回true或false。

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

    用any系列方法来指定

    Mockito.when(validator.verifyPassword(anyString())).thenReturn(true);
    

    执行特定的动作;下面代码LoginPresenter的login()方法

    
    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
            }
        });
    }
    
    
    

    执行callback.onFailure()方法;当调用mockUserManager
    的performLogin方法时,会执行answer里面的代码,我们上面的例子是直接调用传入的callback的onFailure方法,同时传给onFailure方法500和”Server error”

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

    四:依赖注入

    依赖:如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。

    public class Human {
        ...
        Father father;
        ...
        public Human() {
            father = new Father();
        }
    }
    
    

    上述代码的缺点:
    (1):如果现在要改变 father 生成方式,如需要用new Father(String name)
    初始化 father,需要修改 Human 代码;
    (2):如果想测试不同 Father 对象对 Human 的影响很困难,因为 father 的初始化被写死在了 Human 的构造函数中;
    (3):如果new Father()过程非常缓慢,单测时我们希望用已经初始化好的 father 对象 Mock 掉这个过程也很困难。

    public class Human {
        ...
        Father father;
        ...
        public Human(Father father) {
            this.father = father;
        }
    }
    

    依赖注入:像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入。
    优点:
    (1) : 解耦,将依赖之间解耦。
    (2) : 因为已经解耦,所以方便做单元测试,尤其是 Mock 测试。*

    Java 中的依赖注入:
    依赖注入的实现有多种途径,而在 Java 中,使用注解是最常用的。通过在 字段的声明前添加 @Inject 注解进行标记,来实现依赖对象的自动注入。需要Dagger依赖注入框架

    public class Human {
        ...
        @Inject Father father;
        ...
        public Human() {
        }
    }
    

    相关文章

      网友评论

          本文标题:单元测试&依赖注入

          本文链接:https://www.haomeiwen.com/subject/coqgrttx.html