本文仅为学习笔记;不是原创文章;
一:单元测试基本概念
概念:单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码。
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:
单元测试是基础,是我们应该花绝大多数时间去写的部分,而集成测试等应该是冰山上面能看见的那一小部分
单元测试:
对软件质量的提升
方便重构
节约时间
提升代码设计
二:单元测试框架: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的使用
- 验证方法调用
验证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());
- 指定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() {
}
}
网友评论