@TOC
手机用户请
横屏
获取最佳阅读体验,REFERENCES
中是本文参考的链接,如需要链接和更多资源,可以关注其他博客发布地址。
平台 | 地址 |
---|---|
CSDN | https://blog.csdn.net/sinat_28690417 |
简书 | https://www.jianshu.com/u/3032cc862300 |
个人博客 | https://yiyuery.club |
基于JUnit
从零开始认识单元测试
JUnit预备知识
什么是软件测试?
- 软件测试是检查实际结果与预期结果是否匹配并确保软件系统无缺陷的活动。
- 软件测试还有助于识别产品与实际需求不符或是缺失项。
- 测试活动既可以手动完成,也可以使用自动化工具完成。
- 有些人更喜欢将软件测试称为白盒和黑盒测试
什么是软件测试目标?
- 在给定的产品中尽可能多地发现错误(或bug)。
- 演示一个给定的软件产品与它的需求规格匹配。
- 使用最小的成本和努力来验证软件的质量。
- 生成高质量的测试用例,执行有效的测试,并发布正确和有用的问题报告。
什么是软件测试过程?
软件测试通常分为两个主要过程——验证 & 认证。
验证是当您的团队只需要检查软件、系统或框架是否符合文档要求时的过程。
认证是您的团队需要验证系统正确性的过程。在这个过程中,您将回顾产品、系统,并考虑用户真正想要什么和已经做了什么。
在软件测试中,缺陷和错误之间有区别,我们应该清楚地区分,以避免误解问题。
软件测试分类
单元测试
这是在开发人员级别使用的最基本的测试,测试人员专注于单元代码的单个部分,而它已经从任何外部交互或依赖于任何模块之前被隔离。这个测试要求开发人员检查他们编写的最小代码单元,并证明单元可以独立工作。
如果你听说过测试驱动开发
(TDD:Test-Driven Development),单元测试就不陌生。单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。比如对函数abs()
,我们可以编写出以下几个测试用例:
- 输入正数,比如1、1.2、0.99,期待返回值与输入相同;
- 输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
- 输入0,期待返回0;
- 输入非数值类型,比如None、[]、{},期待抛出TypeError。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。单元测试通过后有什么意义呢?
如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件
输入不正确,总之,需要修复使单元测试能够通过。
如果我们对abs()
函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()
函数原有
的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的
时候,可以极大程度地保证该模块行为仍然是正确的。
From:
廖雪峰
单元测试方式
单元测试可以由两种方式完成:
-
人工测试
- 手动执行测试用例并不借助任何工具的测试被称为人工测试。�消耗时间并单调:由于测试用例是由人力资源执行,所以非常缓慢并乏味。
- 人力资源上投资巨大:由于测试用例需要人工执行,所以在人工测试上需要更多的试验员。
- 可信度较低:人工测试可信度较低是可能由于人工错误导致测试运行时不够精确。
- 非程式化:编写复杂并可以获取隐藏的信息的测试的话,这样的程序无法编写。
-
自动测试(借助工具支持并且利用自动工具执行用例被称为自动测试。)
- 快速自动化运行测试用例时明显比人力资源快。
- 人力资源投资较少:测试用例由自动工具执行,所以在自动测试中需要较少的试验员。
- 可信度更高:自动化测试每次运行时精确地执行相同的操作。
- 程式化:试验员可以编写复杂的测试来显示隐藏信息。
JUnit 简介
JUnit
是一个 Java 编程语言的单元测试框架。JUnit 在测试驱动的开发方面有很重要的发展,是起源于 JUnit 的一个统称为 xUnit 的单元测试框架之一。
JUnit 促进了先测试后编码
的理念,强调建立测试数据的一段代码,可以先测试,然后再应用。这个方法就好比“测试一点,编码一点,测试一点,编码一点……”,增加了程序员的产量和程序的稳定性,可以减少程序员的压力和花费在排错上的时间。
特点:
- JUnit 是一个开放的资源框架,用于编写和运行测试。
- 提供注释来识别测试方法。
- 提供断言来测试预期结果。
- 提供测试运行来运行测试。
- JUnit 测试允许你编写代码更快,并能提高质量。
- JUnit 优雅简洁。没那么复杂,花费时间较少。
- JUnit 测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。
- JUnit 测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。
- JUnit 在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。
从零开始搭建JUnit测试环境
测试场景
JUnit是一款优秀的开源Java单元测试框架,也是目前使用率最高最流行的测试框架,开发工具Eclipse和IDEA对JUnit都有很好的支持,JUnit主要用于以下测试场景。
- 白盒测试:把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人�员是公开的;
- 回归测试:软件或环境修复或更正后的再测试;
- 单元测试:最小粒度的测试,以测试某个功能或代码块。一般由程序员来做,因为它需要知道内部程序设计和编码的细节;
JUnit GitHub地址:https://github.com/junit-team
环境搭建
- Spring Boot 2.1.0 RELEASE
- JUnit 4.12
- Maven 3.0.0+
- IDEA 2019.2
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Spring 框架
Spring Boot 框架
核心API
JUnit API TestCase
测试样例定义了运行多重测试的固定格式
- int countTestCases() 为被run(TestResult result) 执行的测试案例计数
- TestResult createResult() 创建一个默认的 TestResult 对象
- String getName() 获取 TestCase 的名称
- TestResult run() 一个运行这个测试的方便的方法,收集由TestResult 对象产生的结果
- void run(TestResult result) 在 TestResult 中运行测试案例并收集结果
- void setName(String name) 设置 TestCase 的名称
- void setUp()
创建固定装置,例如,打开一个网络连接 - void tearDown() 拆除固定装置,例如,关闭一个网络连接
- String toString() 返回测试案例的一个字符串表示
/*测试入口*/
public class Ex3Test {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(JunitEx3Test.class,JunitEx3_2Test.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}
public class JunitEx3_2Test extends TestCase {
@Test
public void testXX2() {
System.out.println("No of Test2 Case:" + this.countTestCases());
System.out.println("Original Test Name:" + this.getName());
this.setName("JunitEx3_2Test Name");
System.out.println("Update Test Name:" + this.getName());
}
}
public class JunitEx3Test extends TestCase {
/**
* 为被run(TestResult result) 执行的测试案例计数
* 注意前缀必须为test***
*/
@Test
public void testX1() {
System.out.println("No of Test Case:" + this.countTestCases());
System.out.println("Original Test Name:" + this.getName());
this.setName("testX1 Name");
System.out.println("Update Test Name:" + this.getName());
}
@Test
public void testX2() {
System.out.println("No of Test Case:" + this.countTestCases());
System.out.println("Original Test Name:" + this.getName());
this.setName("testX2 Name");
System.out.println("Update Test Name:" + this.getName());
}
}
JUnit API TestResult
TestResult 类收集所有执行测试案例的结果。它是收集参数层面的一个实例。这个实验框架区分失败和错误。失败是可以预料的并且可以通过假设来检查。错误是不可预料的问题就像 ArrayIndexOutOfBoundsException。
TestResult
类的一些重要方法列式如下:
- void addError(Test test, Throwable t) 在错误列表中加入一个错误
- void addFailure(Test test, AssertionFailedError t) 在失败列表中加入一个失败
- void endTest(Test test) 显示测试被编译的这个结果
- int errorCount() 获取被检测出错误的数量
- Enumeration errors() 返回错误的详细信息
- int failureCount() 获取被检测出的失败的数量
- void run(TestCase test) 运行 TestCase
- int int runCount() 获得运行测试的数量
- void startTest(Test test) 声明一个测试即将开始
- void stop()标明测试必须停止
/*测试入口*/
public class Ex4Test {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(JEx4_1Test.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}
public class JEx4_1Test extends TestResult {
@Test
public void testX1() throws IllegalArgumentException {
throw new IllegalArgumentException("testX1 failed");
}
@Test
public void testX2() throws IllegalArgumentException {
throw new IllegalArgumentException("testX2 failed");
}
@Test
public void testX3() {
System.out.println("testX3 success...");
}
}
JUnit API TestSuite
TestSuite 类是测试的组成部分。它运行了很多的测试案例
- void addTest(Test test) 在套中加入测试。
- void addTestSuite(Class<? extends TestCase> testClass) 将已经给定的类中的测试加到套中。
- int countTestCases() 对这个测试即将运行的测试案例进行计数。
- String getName() 返回套的名称。
- void run(TestResult result) 在 TestResult 中运行测试并收集结果。
- void setName(String name) 设置套的名称。
- Test testAt(int index) 在给定的目录中返回测试。
- int testCount() 返回套中测试的数量。
- static Test warning(String message) 返回会失败的测试并且记录警告信息。
/*测试入口*/
public class Ex5Test {
public static void main(String[] args) {
TestSuite suite = new TestSuite(TestJunit1.class, TestJunit2.class);
TestResult result = new TestResult();
suite.run(result);
System.out.println("Number of test cases = " + result.runCount());
}
}
public class TestJunit1 extends TestCase {
String message = "Robert";
MessageUtil messageUtil = new MessageUtil(message);
@Test
public void testMessage() {
System.out.println("TestJunit1 testMessage()");
assertEquals(message, messageUtil.printMessage());
}
}
public class TestJunit2 extends TestCase {
String message = "Robert";
MessageUtil messageUtil = new MessageUtil(message);
@Test
public void testMessage() {
System.out.println("TestJunit2 testMessage()");
message = "Hi!" + "Robert";
assertEquals(message,messageUtil.printMessage());
}
}
JUnit单元测试如何开展
IDEA支持
IntellJ IDEA 支持快速生成测试用例
Ctrl + Shift + T
断言测试
测试结果的表达式
断言测试也就是期望值测试,是单元测试的核心,也就是决定测试结果的表达式
Assert对象中的断言方法:
Assert.assertEquals 对比两个值相等
Assert.assertNotEquals 对比两个值不相等
Assert.assertSame 对比两个对象的引用相等
Assert.assertArrayEquals 对比两个数组相等
Assert.assertTrue 验证返回是否为真
Assert.assertFlase 验证返回是否为假
Assert.assertNull 验证null
Assert.assertNotNull 验证非null
public class Ex1Test {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(Ex1Test.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
/**
* 简单输出测试 Person(name=xxx)
*/
@Test
public void test1() {
Person person = Person.builder().name("xxx").build();
System.out.println(person.toString());
}
/**
* 利用 Assert 断言输出结果
* org.junit.Assert
*/
@Test
public void test2() {
Person person = Person.builder().name("xxx").build();
System.out.println(person.toString());
Assert.assertEquals(person.getName(), "xxx");
Assert.assertSame(person.getName(), "xxx");
Assert.assertNotSame(person.getName(), "xx2");
Assert.assertFalse(person.getName().endsWith("xx2"));
Assert.assertTrue(person.getName().endsWith("xx"));
Assert.assertNull(null);
Assert.assertNotNull(person);
}
/**
* 测试 @Before
* Person(name=before....1)
* Person(name=xxx3)
*/
@Test
public void test3() {
Person person = Person.builder().name("xxx3").build();
System.out.println(person.toString());
}
@Before
public void before1() {
Person person = Person.builder().name("before....1").build();
System.out.println(person.toString());
}
/**
* 测试 @After
* Person(name=before....1)
* Person(name=xxx4)
* Person(name=after...1)
*/
@Test
public void test4() {
Person person = Person.builder().name("xxx4").build();
System.out.println(person.toString());
}
@After
public void after1() {
Person person = Person.builder().name("after...1").build();
System.out.println(person.toString());
}
/**
* 测试@AfterClass
* Person(name=before....1)
* Person(name=xxx5)
* Person(name=after...1)
* Person(name=after...2)
*/
@Test
public void test5() {
Person person = Person.builder().name("xxx5").build();
System.out.println(person.toString());
}
/**
* java.lang.Exception: Method after2() should be static
* 在所有方法执行之后执行
*/
@AfterClass
public static void after2() {
Person person = Person.builder().name("after...2").build();
System.out.println(person.toString());
}
/**
* 测试 @BeforeClass
* Person(name=before...2)
* Person(name=before....1)
* Person(name=xxx6)
* Person(name=after...1)
* Person(name=after...2)
*/
@Test
public void test6() {
Person person = Person.builder().name("xxx6").build();
System.out.println(person.toString());
}
/**
* java.lang.Exception: Method before2() should be static
* 在所有方法执行之前执行
*/
@BeforeClass
public static void before2() {
Person person = Person.builder().name("before...2").build();
System.out.println(person.toString());
}
/**
* hamcrest-core-1.3.jar
* Hamcrest是一款用以编写matcher对象的框架,以类库的形式发布。一个matcher对象就是一个明确定义的匹配规则.
* > 匹配规则:Matchers
*/
@Test
@Ignore
public void test7() {
MatcherAssert.assertThat(Long.valueOf(1), instanceOf(Integer.class));
//字符串匹配符
String n = "Magci";
//containsString:字符串变量中包含指定字符串时,测试通过
MatcherAssert.assertThat(n, Matchers.containsString("ci"));
//startsWith:字符串变量以指定字符串开头时,测试通过
MatcherAssert.assertThat(n, Matchers.startsWith("Ma"));
//endsWith:字符串变量以指定字符串结尾时,测试通过
MatcherAssert.assertThat(n, Matchers.endsWith("i"));
//euqalTo:字符串变量等于指定字符串时,测试通过
MatcherAssert.assertThat(n, Matchers.equalTo("Magci"));
//equalToIgnoringCase:字符串变量在忽略大小写的情况下等于指定字符串时,测试通过
MatcherAssert.assertThat(n, Matchers.equalToIgnoringCase("magci"));
//equalToIgnoringWhiteSpace:字符串变量在忽略头尾任意空格的情况下等于指定字符串时,测试通过
MatcherAssert.assertThat(n, Matchers.equalToIgnoringWhiteSpace(" Magci "));
//...so on
}
@Test
public void testX() {
}
/**
* fail({{message}}) 直接中止方法运行
*/
@Test
@Ignore
public void test8() {
Assert.fail("directlly stop and output fail log!");
}
}
套件测试
测试套件意味着捆绑几个单元测试用例并且一起执行他们。在 JUnit 中,@RunWith 和 @Suite 注释用来运行套件测试。这个教程将向您展示一个例子,其中含有两个测试样例 TestJunit1 & TestJunit2 类,我们将使用测试套件一起运行他们。
/*测试入口*/
public class Ex2Test{
public static void main(String[] args) {
Result result = JUnitCore.runClasses(TestSuite.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}
public class MessageUtil {
String message;
public String printMessage() {
return message;
}
public MessageUtil(String message) {
this.message = message;
}
}
public class TestJunit1 {
String message = "Robert";
MessageUtil messageUtil = new MessageUtil(message);
@Test
public void testMessage() {
System.out.println("TestJunit1 testMessage()");
assertEquals(message, messageUtil.printMessage());
}
}
public class TestJunit2 {
String message = "Robert";
MessageUtil messageUtil = new MessageUtil(message);
@Test
public void testMessage() {
System.out.println("TestJunit2 testMessage()");
message = "Hi!" + "Robert";
assertEquals(message,messageUtil.printMessage());
}
}
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestJunit1.class ,TestJunit2.class
})
public class TestSuite {
}
时间测试
Junit 提供了一个暂停的方便选项。如果一个测试用例比起指定的毫秒数花费了更多的时间,那么 Junit 将自动将它标记为失败。timeout 参数和 @Test 注释一起使用。现在让我们看看活动中的 @test(timeout)。
public class Ex6Test {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(JEx6_1Test.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}
public class JEx6_1Test {
@Test
public void testX1(){
System.out.println("x1");
}
@Test(timeout = 10)
public void testX2(){
for(int i =0;i<100000000;i++){
Math.random();
}
System.out.println("x2");
}
@Test
public void testX3(){
System.out.println("x3");
}
}
异常测试
Junit 用代码处理提供了一个追踪异常的选项。你可以测试代码是否它抛出了想要得到的异常。expected 参数和 @Test 注释一起使用。现在让我们看看活动中的 @Test(expected)。
/*测试入口*/
public class Ex7Test {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(JEx7_1Test.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}
public class JEx7_1Test {
String message = "Robert";
MessageUtil messageUtil = new MessageUtil(message);
@Test
public void testX1(){
System.out.println("x1");
}
//@Test(expected = ArithmeticException.class)
@Test(expected = IllegalArgumentException.class)
public void testX2(){
messageUtil.printMessage();
}
@Test
public void testX3(){
System.out.println("x3");
}
}
class MessageUtil {
private String message;
//Constructor
//@param message to be printed
public MessageUtil(String message){
this.message = message;
}
// prints the message
public void printMessage(){
System.out.println(message);
int a =0;
int b = 1/a;
}
// add "Hi!" to the message
public String salutationMessage(){
message = "Hi!" + message;
System.out.println(message);
return message;
}
}
参数化测试
Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同一个测试。你将遵循 5 个步骤来创建参数化测试。
- 用 @RunWith(Parameterized.class) 来注释 test 类。
- 创建一个由 @Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合。
- 创建一个公共的构造函数,它接受和一行测试数据相等同的东西。
- 为每一列测试数据创建一个实例变量。
- 用实例变量作为测试数据的来源来创建你的测试用例。
一旦每一行数据出现测试用例将被调用。
/*测试入口*/
public class Ex8Test {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(JEx8_1Test.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}
@RunWith(Parameterized.class)
public class JEx8_1Test {
private Integer inputNumber;
private Boolean expectedResult;
private PrimeNumberChecker primeNumberChecker;
@Before
public void initialize() {
primeNumberChecker = new PrimeNumberChecker();
}
public JEx8_1Test(Integer inputNumber,
Boolean expectedResult) {
this.inputNumber = inputNumber;
this.expectedResult = expectedResult;
}
@Parameterized.Parameters
public static Collection primeNumbers() {
return Arrays.asList(new Object[][] {
{ 2, true },
{ 6, false },
{ 19, true },
{ 22, false },
{ 23, true }
});
}
@Test
public void testPrimeNumberChecker() {
System.out.println("Parameterized Number is : " + inputNumber);
assertEquals(expectedResult,
primeNumberChecker.validate(inputNumber));
}
}
/**
* 质数
*/
class PrimeNumberChecker {
public Boolean validate(final Integer primeNumber) {
for (int i = 2; i < (primeNumber / 2); i++) {
if (primeNumber % i == 0) {
return false;
}
}
return true;
}
}
事务控制
public class Ex1Test extends BaseBootJunitTest {
@Resource
private IPersonService personService;
/**
* 初始数据插入
*/
@Test
public void testX1() {
personService.saveWithName("p_test_1111");
}
/**
* 方法内部事务间隔离
*/
@Test
public void testX2() {
//第一行数据插入成功
personService.saveWithName(CapsuleStringUtil.randomStr("p_test_"));
//第二行数据插入失败
personService.saveNoTransactional("p_test_1111");
}
/**
* 针对Test方法加入事务控制
* 避免测试数据污染数据库
*/
@Test
@Transactional(rollbackFor = Exception.class)
public void testX3() {
//第一行数据插入失败
personService.saveWithName(CapsuleStringUtil.randomStr("p_test_"));
//第二行数据插入失败
personService.saveNoTransactional("p_test_1111");
}
}
接口测试
public class Ex2Test extends BaseBootJunitTest {
@Resource
private TestRestTemplate restTemplate;
@Test
public void testHello() {
String resp = restTemplate.getForObject("/hello", String.class);
System.out.println(resp);
Assert.assertEquals("Hello Junit!", resp);
}
}
忽略测试
框架扩展
Hamcrest匹配器的用法
官网地址 http://hamcrest.org/JavaHamcrest/tutorial
自动化测试
/*测试入口*/
public class Ex3Test {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(JEx3_1Test.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class JEx3_1Test extends BaseBootJunitTest {
@Resource
private IPersonService personService;
private String pName;
/**
* 每个测试方法都是个测试用例,独立执行 @beforeInit 注解标注的方法
*/
@Before
public void before() {
System.out.println("[Before]-----------------------------------");
pName = "p_test_auto_1";
}
@After
public void after() {
System.out.println("[After]-----------------------------------");
}
/**
* 正常过程:人员添加
*/
@Test
public void testX() {
Person person = personService.saveWithName(pName);
log("添加后人员信息返回结果", person);
Person dbPerson = personService.findByPersonName(pName,true);
Assert.assertNotNull(dbPerson);
}
/**
* 正常过程:人员修改
*/
@Test
public void testX2() {
Person oldPerson = personService.findByPersonName(pName,true);
oldPerson.setName(pName + "_modify");
Person newPerson = personService.saveWithResult(oldPerson);
log("修改后人员信息", newPerson);
Assert.assertNotNull(newPerson);
Assert.assertEquals(oldPerson.getName(), newPerson.getName());
}
/**
* 正常过程:人员删除
*/
@Test
public void testX3() {
Person dbPerson = personService.findByPersonName(pName+"_modify",true);
personService.delete(dbPerson.getId());
log("删除前人员信息", dbPerson);
Person personDel = personService.findByPersonName(dbPerson.getName(), false);
log("删除后搜索结果", personDel);
}
}
总结
本文从软件测试为讨论的切入点,介绍了单元测试在软件测试中的重要性和对应角色。接下来,以JUnit测试框架展开,就环境搭建、测试类型、JUnit 核心API、JUnit各种测试方法分别进行了介绍,并提供了代码示例。最后,结合人员的增删改操作,编写了对应的自动化测试用例。
REFRENCES
更多
扫码关注“架构探险之道”,回复本文标题或关键词,获取本文源码
知识星球(扫码加入获取历史源码和文章资源链接)