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

单元测试&依赖注入

作者: 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() {
    }
}

相关文章

  • 单元测试&依赖注入

    本文仅为学习笔记;不是原创文章; 参考资料1参考资料2 一:单元测试基本概念 概念:单元测试,是为了测试某一个类的...

  • SpringBoot 单元测试

    SpringBoot 单元测试 依赖注入 引入依赖: 这里引入了一个spring-boot-starter-tes...

  • Spring Boot 单元测试

    单元测试service层代码 如果想在单元测试中使用Spring boot的依赖注入功能,需要在单元测试的clas...

  • 依赖注入与单元测试

    不可测的代码 假设我们需要记录一些日志,而日志的容器有一定的容量,如果日志满了就无法记录了,我们可能会写出如下的代...

  • Mockito框架使用

    Mockito框架是单元测试处理bean依赖注入的一大利器 省略UserService ,此时正常编写测试类的话会...

  • 开源项目的依赖注入

    开源项目的依赖注入 依赖注入概念 依赖注入(DI:Dependency Injection): 依赖注入方式: B...

  • 资料收集

    依赖注入 AngularJs依赖注入的研究 (已读) 依赖注入(已读)

  • Dagger2常用注解诠释

    依赖注入 控制反转(IoC)与依赖注入(DI)浅谈依赖注入理解依赖注入(IOC)和学习Unity Gradle配置...

  • Day62 Spring 依赖注入源码解析

    手动注入自动注入 依赖注入,set注入,构造注入 依赖注入: Key依据 byType byName constr...

  • Dagger2 源码分析

    Dagger简单介绍 Dagger2涉及到依赖注入,有关依赖注入的请到理解依赖注入 通过注解方式实现依赖注入分为两...

网友评论

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

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