转载自: 单元测试之Junit4详解
代码测试
- 按是否查看程序内部结构分为
- 黑盒测试(black-box testing):只关心输入和输出的结果,黑盒测试分为功能测试和性能测试
- 白盒测试(white-box testing):去研究里面的源代码和程序结构
- 按是否运行程序分为:静态测试、动态测试
- 静态测试(static testing):是指不实际运行被测软件,而只是静态地检查程序代码、界面或文档可能存在的错误的过程。包括:
- 对于代码测试,主要是测试代码是否符合相应的标准和规范。
- 对于界面测试,主要测试软件的实际界面与需求中的说明是否相符。
- 对于文档测试,主要测试用户手册和需求说明是否真正符合用户的实际需求。
- 动态测试(dynamic testing):是指实际运行被测程序,输入相应的测试数据,检查输出结果和预期结果是否相符的过程
- 按阶段划分:
- 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
桩模块(stud)是指模拟被测模块所调用的模块,驱动模块(driver)是指模拟被测模块的上级模块,驱动模块用来接收测试数据,启动被测模块并输出结果。 - 集成测试(integration testing),是单元测试的下一阶段,是指将通过测试的单元模块组装成系统或子系统,再进行测试,重点测试不同模块的接口部门。
集成测试就是用来检查各个单元模块结合到一起能否协同配合,正常运行。 - 系统测试(system testing),指的是将整个软件系统看做一个整体进行测试,包括对功能、性能,以及软件所运行的软硬件环境进行测试。
系统测试的主要依据是《系统需求规格说明书》文档 - 验收测试(acceptance testing),指的是在系统测试的后期,以用户测试为主,或有测试人员等质量保障人员共同参与的测试,它也是软件正式交给用户使用的最后一道工序
验收测试又分为a测试和beta测试,其中a测试指的是由用户、 测试人员、开发人员等共同参与的内部测试,而beta测试指的是内测后的公测,即完全交给最终用户测试
什么是单元测试
- 维基百科定义简化版如下:
- 单元测试 是针对 程序的最小单元 来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是单个程序、类、对象、方法等。 ——维基百科
- 单元测试的分类
- 本地测试(Local tests): 只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的[Mockito
- 仪器化测试(Instrumented tests): 在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式
- 单元测试要测试什么
- 列出想要测试覆盖的正常、异常情况,进行测试验证
- 性能测试,例如某个算法的耗时等等
为什么要使用单元测试
- 减少bug
- 快速定位bug、减少调试时间
- 提高代码质量
- 放心重构
- 主要是:领导要求、大牛都写单元测试、保住面子、心虚
Android中的测试
- 单元测试(Junit4、Mockito、PowerMockito、Robolectric)
- UI测试(Espresso、UI Automator)
- 压力测试(Monkey)
- 使用单元测试的时候注意进行依赖隔离
- Anddroid中的单元测试框架有很多,Junit,Mockito,Robolectric,PowerMockito
注意
- 这里提一句,测试用例用来达到也想要的结果,但对于逻辑错误无能为力
- 两种错误:断言方法报错,逻辑代码问题
Junit介绍
- 引用官网上的介绍:
A programmer-oriented testing framework for Java.- 它是一个Java的面向程序员的测试框架
- 了解 xUnit
- xUnit是一套基于测试驱动开发的测试框架,包括了 PythonUint、CppUnit、Junit,分别是 Python、C++、Java的测试框架
使用
- Junit 的功能十分的强大,知识点有很多,我这里讲一些常见的,剩下的大家自己下去了解
导入依赖
-
app/build 下导包
testImplementation 'junit:junit:4.12' //前面是 testImplementation 1
基本使用,断言方法
-
先准备好 原类(被测试类)
- 注意,被测试的方法是要用 public 修饰,不然调用不到
- 如下,下面就是几个简单的计算方法
public class Calculator {
public int add(int a,int b) { return a + b; } public int subtract(int a,int b) { return a - b; } public int multiply(int a,int b) { return a * b; } public int divide(int a,int b) { return a / b; }}
-
鼠标光标移至 被测试类名 处,快捷键:Alt + Enter -> 选择 create Test (或者 右击 -> go to -> Test),然后进行选择一些类的方法、存放路径等信息,完成即可生成 测试类。测试类名是以 Test 作为后缀的,接下来就可以进行一些测试操作了
- 关于 测试类 存放的位置
- AS已经自动帮我们创建好了,如下:
app/src ├── androidTestjava (仪器化单元测试、UI测试) ├── main/java (业务代码) └── test/java (本地单元测试)
- 关于 测试类 存放的位置
-
然后编写测试方法,使用
@Test进行注解,都是 public void 且 无参数 的方法,这里编写测试方法有快捷键:Alt + Insert 即可选择自动生成测试方法等- 方法名与原类中的方法名一致,或以 test 为前缀进行命名
- 使用 Assert 类中的静态方法
assertXxx()进行测试,又称为断言方法- 下面代码中使用的是
assertEquals()方法,比较实际值(调用 被测试方法的结果)与 期望值(你认为的值)是否相同。相同,Run 通过;不同,控制台提示错误信息(很明显的提示你)
- 下面代码中使用的是
- 跑程序的时候可以选择是跑一个类(即全部的测试方法),还是指定的方法
public class CalculatorTest {
private static final String TAG = "TAG"; private Calculator calculator = new Calculator(); @Before public void setUp() throws Exception {// Log.d(TAG,"执行:setUp()"); }
@After public void tearDown() throws Exception {// Log.d(TAG,"执行:tearDown()"); }
@Test public void add() { assertEquals(6,calculator.add(3,3)); } ··· @Test public void divide() { assertEquals(3,calculator.divide(9,3)); }}
测试套件
- 测试套件就是组织测试类一起运行
-
写一个空类作为测试套件的入口类
-
使用
@RunWith注解入口类,指明测试运行器为 Suite 类- Suite 是一个标准运行程序,允许您手动构建包含来自多个类的测试的套件
- 作用就是改变测试运行器
-
使用
@Suite.SuiteClasses注解入口类,并以数组的形式指明测试类- 这里,数组的先后顺序会影响进行测试的先后顺序
- 按顺序依次执行方法,一个测试类的方法执行完之后才会执行下一个测试类的方法
//测试套件:入口类 @RunWith(Suite.class) @Suite.SuiteClasses({Test1Test.class,Test2Test.class}) public class Test {
}
//测试套件:测试类一 public class Test1Test {
@Test public void add() { assertEquals(5,new Test1().add(2,3)); System.out.println("测试类Test1Test执行add()方法"); } @Test public void subtract() { assertEquals(5,new Test1().subtract(5,0)); System.out.println("测试类Test1Test执行subtract()方法"); }}
//测试套件:测试类二 public class Test2Test {
@Test public void add() { assertEquals(5,new Test2().add(2,3)); System.out.println("测试类Test2Test执行add()方法"); } @Test public void subtract() { assertEquals(5,new Test2().subtract(6,1)); System.out.println("测试类Test2Test执行subtract()方法"); }}
- 这里,数组的先后顺序会影响进行测试的先后顺序
参数化测试
-
参数化测试允许开发人员使用 不同的值 反复运行 同一个测试
- 和方法传值类似,一个方法可以调用多次,每次传递不同的值
- 但是测试方法并没有参数,所以提供了这样一个参数化测试来实现
- 使用如下
-
使用
@RunWith注解测试类,并指定属性值为 Parameterized.class- Parameterized是实现参数化测试的标准运行程序。运行参数化的测试类时,将为测试方法和测试数据元素的叉积创建实例。
-
然后就是定义你的测试方法所需要的参数值,(期望值 + 断言方法里调用的方法所需的参数值)
-
再编写一个构造方法,使用
@Parameterized.Parameters进行注解,在构造方法中对定义的变量进行赋值 -
再定义一个 public static 修饰的方法,返回值为 Collection<>,可以指定类型参数(推荐),也可以不指定
- 在这个方法中定义一个二维数组(一维数组不够),然后使用
Arrays.asList()方法将数组转换为 List集合,然后返回即可- 这里二维数组的元素就是你要进行测试的值
- 在这个方法中定义一个二维数组(一维数组不够),然后使用
-
至此,就可以正常的进行测试了
@RunWith(Parameterized.class) public class ParamTestTest {
private int expected = 0; private int input1 = 0; private int input2 = 0; @Parameterized.Parameters public static Collection<Object[]> t() { return Arrays.asList(new Object[][]{ {5,2,3}, {11,5,6} }); } public ParamTestTest(int expected, int input1, int input2) { this.expected = expected; this.input1 = input1; this.input2 = input2; } @Test public void add() { assertEquals(expected,new ParamTest().add(input1,input2)); }}
分类测试
-
所谓分类测试,就是将测试代码中的方法进行分类,然后根据类别来选择该跑什么类别的方法,当然,这个时候同一类别的方法肯定是都会执行的
-
主要是
@Category注解的使用,还包含:@RunWith、@SuiteClasses、@IncludeCategory这几个注解一起使用 -
一共需要的类数量:原类 + 测试类 + 作标识符的类 + 最后运行的类
- 原类:提供方法进行测试
- 测试类:方法使用
@Category注解,指明属性值(为类别的标识符),别忘了@Test - 作标识符的类:就是几个空类
- 最后运行的类:空类 + 注解
- 使用
@RunWith注解,并指定属性值为 Categories 类 - 使用
@Categories.IncludeCategory注解,并指明属性值为你要进行测试的 标识符的类 - 使用
@Suite.SuiteClasses注解,并指明属性值为测试类
- 使用
- 最后就可以运行了
-
如下:
//原类 public class CategoryTest {
public int add(int a,int b) { return a + b; } public int subtract(int a,int b) { return a - b; } public int secondPower(int a) { return a * a; } public int theThirdPower(int a) { return a * a * a; }}
//测试类 public class CategoryTestTest {
@Category(BaseOperations.class) @Test public void add() { assertEquals(5,new CategoryTest().add(2,3)); } @Category(BaseOperations.class) @Test public void subtract() { assertEquals(5,new CategoryTest().subtract(6,1)); } @Category(PowerOperations.class) @Test public void secondPower() { assertEquals(4,new CategoryTest().secondPower(2)); } @Category(PowerOperations.class) @Test public void theThirdPower() { assertEquals(8,new CategoryTest().theThirdPower(2)); }}
//作标识符进行判断种类的类(这里就是两种,幂运算,基本运算) public class PowerOperations { //这个代表幂运算类的方法标识符 }
public class BaseOperations { //这个代表基本运算的方法标识符 }
//最后运行的类 @RunWith(Categories.class) @Categories.IncludeCategory(BaseOperations.class) @Suite.SuiteClasses(CategoryTestTest.class) public class Operations { }
假设测试
-
使用 assumeXxx(假设方法)进行判断,假设的条件是否成立,不成立则终止测试
-
Assume类中有很多的假设方法,可以自行查看,都有注释的
@Test public void testAssumptions() { //假设进入testAssumptions时,变量i的值为10,如果该假设不满足,程序不会执行assumeThat后面的语句 assumeThat( i, is(10) ); //如果之前的假设成立,会打印"assumption is true!"到控制台,否则直接调出,执行下一个测试用例函数 System.out.println( "assumption is true!" );
}
断言方法
- 断言方法都有很多的重载形式,这里讲讲含义就行了,具体可以自行查看
- 断言方法都来自 Assert 类,都是 static public 的
- 不满足断言方法,则会抛出:AssertionError
assertArrayEquals
- 断言两个对象数组相等
assertEquals
- 断言两个对象相等
assertNotEquals()
- 断言两个对象不相等
assertNull()
- 断言一个对象为空
assertNotNull()
- 断言一个对象不为空
assertSame()
- 断言两个对象引用相同的对象
assertNotSame()
- 断言两个对象没有引用同一对象
assertThat()
- 断言实际值是否满足指定的条件,与 Matcher 一起使用,Matcher 指定条件
- 详细看后面的讲解
assumeThat
- 假设满足指定的条件,如果不满足,测试停止
- Assume类中有很多的假设方法
assertTrue()
- 断言条件为真
assertFalse()
- 断言条件为假
assertThat与CoreMatchers
-
断言实际值是否满足指定的条件,与 Matcher 一起使用
-
这种断言方法一共两种重载形式:
//第一个参数:reason 为断言失败时的输出信息 //第二个参数:actual 为断言的值或对象 //第三个参数:matcher 为断言的匹配器,里面的逻辑决定了 给定的 actual对象满不满足断言 public static void assertThat(String reason, T actual, Matcher<? super T> matcher) { MatcherAssert.assertThat(reason, actual, matcher); }
public static void assertThat(T actual, Matcher<? super T> matcher) { assertThat("", actual, matcher); }
-
在 CoreMatchers类中组织了所有JUnit内置的Matcher(匹配的方法),调用其任意一个方法都会创建一个与方法名字相关的Matcher
-
下面的匹配并没有全部讲解完,具体请自行进入
org.hamcrest.CoreMatchers类中查看
一般匹配符
-
allOf 匹配符表明如果接下来的所有条件必须都成立测试才通过,相当于“与”(&&)
assertThat(testedNumber, allOf(is(8), not(16)));
-
anyOf 匹配符表明如果接下来的所有条件只要有一个成立则测试通过,相当于“或”(||)
assertThat(testedNumber, anyOf(is(8), not(16)));
-
anything 匹配符表明无论什么条件,永远为true
assertThat(testedNumber, anything());
-
is 匹配符表明如果前面待测的object等于后面给出的object,则测试通过
assertThat(testedString, is("developerWorks"));
-
not 匹配符和 is 匹配符正好相反,表明如果前面待测的object不等于后面给出的object,则测试通过
assertThat(testedString, not("developerWorks"));
-
both 匹配符表明当两个指定的匹配器都与检查的对象匹配时,该匹配器将匹配
- 一般与 and 匹配符一起使用
assertThat("两个并没有全部匹配",5,both(is(5)).and(not(4)));
字符串相关匹配符
-
containsString 匹配符表明如果测试的字符串testedString包含子字符串"developerWorks"则测试通过
assertThat(testedString, containsString("dveloperWorks"));
-
endsWith 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"结尾则测试通过
assertThat(testedString, endsWith("developerWorks"));
-
startsWith 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"开始则测试通过
assertThat(testedString, startsWith("developerWorks"));
-
equalTo 匹配符表明如果测试的testedValue等于expectedValue则测试通过,equalTo可以测试数值之间,字符串之间和对象之间是否相等,相当于Object的equals方法
assertThat(testedValue, equalTo(expectedValue));
collection相关匹配符
-
hasItem 匹配符表明如果测试的迭代对象 iterableObject 含有元素 “element” 项则测试通过
assertThat(iterableObject, hasItem("element"));
自定义 Matcher
-
首先,大家要去看看 Matcher 这个接口与 BaseMatcher 这个实现类,就几个方法,比较简单,都写着有注释
-
看完就知道我们自定义的 Matcher 应该去继承的是 BaseMatcher 这个类,而不是 Matcher 这个类
-
使用:
- 创建一个类继承 BaseMatcher 类,然后重写两个方法:
matche()、describeTo(),根据自己的需求去实现 - 然后就可以在测试方法中使用了
//IsRichardMatcher这个类是我实现的Matcher类 assertThat(user,new IsRichardMatcher());
- 创建一个类继承 BaseMatcher 类,然后重写两个方法:
注解
@Test
-
将一个普通方法修饰为测试方法
- 限时测试,异常捕获
-
指定异常,以使测试方法在 当且仅当方法抛出 指定类的异常 时才成功,没有抛异常则会失败
//在 @Test 中指定了 断言异常,运行时不会出现错误 @Test(expected = AssertionError.class) public void add() { assertEquals(6,calculator.add(3,1)); System.out.println("测试方法:add() 执行"); }
-
以毫秒为单位 指定超时时间,超时则失败
- 超过指定的时间会中断方法,并抛出异常
@Test(timeout = 3000) public void subtract() { assertEquals(20,calculator.subtract(30,10)); System.out.println("测试方法:subtract() 执行"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }
@Ignore
-
在测试中测试运行器会忽略被 @Ignore 注解的方法,可以指定属性值(比如说为什么被忽略)
-
一般用于测试方法还没有准备好,或者方法太耗时之类
@Ignore("暂时不需要测试这个方法,所以进行忽略") @Test public void multiply() { assertEquals(10,calculator.multiply(2,5)); System.out.println("测试方法:multiply() 执行"); }
@RunWith
- 可以更改运行测试器
- JUnit将调用其引用的类,以在该类中运行测试,而不是JUnit内置的运行器。
- JUnit 4中的套件是使用RunWith和一个名为Suite的自定义运行程序构建的
- 一般在测试套件的时候使用
@BeforeClass
-
在所有测试开始之前执行一次,必须为 static 修饰
-
用于做一些耗时的初始化工作(如: 连接数据库)
@BeforeClass public static void setUpBeforeClass() throws Exception { System.out.println("BeforeClass执行"); }
@AfterClass
-
在所有测试结束之后执行一次,必须为 static 修饰
-
用于清理数据(如: 断开数据连接)
@AfterClass public static void tearDownAfterClass() throws Exception { System.out.println("AfterClass执行"); }
@Before
-
在每个测试方法运行前执行一次
-
用于准备测试环境(如: 初始化类,读输入流等),在一个测试类中,每个@Test方法的执行都会触发一次调用
@Before public void setUp() throws Exception { System.out.println("Before执行"); }
@After
-
在每个测试方法运行结束后执行一次
-
这个方法在每个测试之后执行,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用
@After public void tearDown() throws Exception { System.out.println("After执行"); }
@FixMethodOrder
-
修饰类,使得该测试类中的所有测试方法都按照方法名的字母顺序执行,属性值有三个
-
指定形式:
@FixMethodOrder(MethodSorters.NAME_ASCENDING),MethodSorters是一个枚举类,下面的三个值都是它的枚举常量- DEFAULT
- 以确定但不可预测的顺序对测试方法进行排序,默认值
- JVM
- 将测试方法按JVM返回的顺序保留,请注意,JVM的运行顺序可能会有所不同
- NAME_ASCENDING
- 根据 方法名称 按字典名称 对测试方法进行排序,升序
@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class CalculatorTest {
··· @Test public void divide() { assertEquals(3,calculator.divide(9,3)); System.out.println("测试方法:divide() 执行"); }}
- DEFAULT
Junit 运行流程
-
下面是打印出来的信息,代码很简单就贴出来了
- 在开发中,BeforeClass、AfterClass注解的方法是 static 的
BeforeClass执行(类加载) Before执行 测试方法:subtract() 执行 After执行 Before执行 测试方法:divide() 执行 After执行 Before执行 测试方法:add() 执行 After执行 Before执行 测试方法:multiply() 执行 After执行 AfterClass执行
-
可以很明显的看出执行顺序:
BeforeClass -> Before -> 测试方法 -> After -> AfterClass,如果有多个测试方法,那么重复: Before -> 测试方法 -> After 这段流程
使用中可能出现的 Bug
-
在测试方法中打 Log,出现错误:
java.lang.RuntimeException: Method d in android.util.Log not mocked.
a. 先仔细想一想,文章的一开始说了什么呀
b. 就是 Junit 可以隔离 Android 框架呀@Before public void setUp() throws Exception { Log.d(TAG,"执行:setUp()"); //在此使用 Log }
-
原因
- Log类是android sdk的api,用Junit做单元测试,只能用纯java API,否则会报错
- 这里就如果要使用 Log 就得自己创建一个 Log 类了,然后编写方法,如下:
public class Log { public static int d(String tag, String msg) { System.out.println("DEBUG: " + tag + ": " + msg); return 0; }
public static int i(String tag, String msg) { System.out.println("INFO: " + tag + ": " + msg); return 0; } public static int w(String tag, String msg) { System.out.println("WARN: " + tag + ": " + msg); return 0; } public static int e(String tag, String msg) { System.out.println("ERROR: " + tag + ": " + msg); return 0; } // add other methods if required...}