作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦
千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者
前言
经过前面100多篇文章的讲解,壹哥 带着大家实现了从零开始学Java,我相信只要大家认真地看过壹哥 的文章,肯定就已经得窥Java门径,算是成功入门了。在前面的这些文章中,我们从编程的概念和Java行业讲起,搭建了Java开发环境,详细讲解了开发工具、数据类型、分支结构、面向对象、集合、常用类、泛型、数据结构、算法、异常处理、IO流、多线程、网络通信、注解、反射、枚举、JSON与XML解析等技术,这些都属于Java SE的核心内容。掌握了这些核心内容,我们就有了继续往下学习的牢固基础。而在继续往下学习之前,壹哥再带大家学习一点测试相关的技能,以便我们之后学习其他的内容。
在上一篇文章中,壹哥 给大家讲解了反射的基本使用,但受限于篇幅,我们并没有把反射的知识全都学完。在今天的这篇文章中,壹哥会继续往下讲解反射的高级玩法,希望大家集中精力,继续认真学习哦。
------------------------------前戏已做完,精彩即开始----------------------------
全文大约【6800】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......
配套开源项目资料
Github: github.com/SunLtd/Lear...
Gitee: gitee.com/sunyiyi/Lea...
一. 测试简介
1. 概述
我们知道Java是一种面向对象的编程语言,广泛应用于企业级的项目开发,而为了确保Java程序的质量和稳定性,测试是必不可少的环节。
所谓的测试,其实是对软件质量进行评估的过程,让我们识别和修复程序中的错误和缺陷,从而提高软件质量,减少开发成本,提高软件可靠性、可维护性、安全性等。
无论我们做什么类型的项目开发,在项目开发过程中和上线前,甚至包括上线后,都要进行大量的测试。比如我们都熟悉的游戏开发,更是要做大量的测试,公测、内测这样的词汇相信很多人都听过。公测是把游戏向广大玩家开放,让他们免费试玩游戏,并收集他们的反馈和建议。内测是指将游戏向特定的用户群体开放,如公司员工、游戏爱好者、专业测试人员等,以收集他们的反馈和建议。公测和内测可以帮助开发人员发现和修复游戏中的问题和缺陷,并改进游戏的品质和用户体验。
如果游戏没有进行公测和内测,可能会导致以下问题:
- 游戏存在严重的问题和缺陷,导致游戏体验差,玩家流失;
- 游戏没有得到足够的反馈和建议,无法及时改进游戏;
- 游戏无法吸引足够的玩家参与,导致游戏的品质和稳定性得不到保证。
因此,一个游戏在整个的开发过程中,以及上线前都需要进行多种测试,以确保游戏的质量和稳定性。比如我们要对游戏进行单元测试,来测试游戏项目中各个函数、类等是否有异常;可以对游戏中多个模块之间的交互进行集成测试;通过功能测试检查游戏的各种功能是否正常工作,是否符合需求;通过性能测试来考核游戏在各种极端负载下的性能表现,如响应时间、内存消耗、CPU利用率等;还要测试游戏的安全性和防御外部攻击的能力,看看项目中是否存在XSS、SQL注入、CSRF等各种安全漏洞。
当然,很多时候,这些测试并不是我们程序员要担负的任务,而是由专业的测试人员来负责。对我们程序员来说,我们更关心的是对代码本身的测试,那么在Java开发中到底有哪些测试呢?我们往下看。
2. 分类
实际上,测试作为一个独立的技术方向,是有很多种类型的,一般情况下,主要包括以下几种大类:
- 功能测试 :测试程序的功能是否符合需求,包括黑盒测试、白盒测试、灰盒测试等。
- 非功能测试 :测试程序的非功能特征是否符合要求,包括性能测试、安全测试、可靠性测试、兼容性测试、可用性测试等。
- 回归测试:针对修改后的程序重新进行测试,以确保没有引入新的问题。
- 用户验收测试:由用户或客户进行的测试,以确保软件符合用户的需求和要求。
以上是测试的几大分类,如果我们再进行细分,又可以细分为以下几种情况。其中,功能测试包括以下子类:
- 黑盒测试:测试人员不需要了解程序的内部结构和实现方式,只需测试程序是否按照用户需求和规格说明书的要求进行工作。
- 白盒测试:测试人员需要了解程序的内部结构和实现方式,以测试程序的各个组件是否按照设计要求进行工作。
- 灰盒测试:介于白盒测试和黑盒测试之间,测试人员需要部分了解程序的内部结构来进行测试。
- 单元测试:测试程序的最小可测试单元,如某个方法、类等。
- 集成测试:测试多个单元组合而成的功能,一般是多个方法或类共同实现的某项大功能。
- 系统测试:测试整个系统是否按照了用户的需求和规格说明书进行工作。
- 冒烟测试:测试系统的主要功能是否正常工作,一般是进行比较暴力的测试,比如进行快速高频次的按钮点击。
非功能测试包括以下子类:
- 性能测试:测试程序的性能是否达到了预期要求。
- 安全测试:测试程序的安全性以及对外部攻击的抵抗能力。
- 可靠性测试:测试程序是否稳定可靠,是否容易出现故障。
- 兼容性测试:测试程序在不同的硬件和软件环境下是否正常工作。
- 可用性测试:测试程序的易用性和用户体验。
回归测试包括以下子类:
- 回归测试:针对修改后的程序重新进行测试,以确保没有引入新的问题。
- 退化测试:测试程序的性能是否随着时间的推移而降低。
- 逆向测试:测试程序的异常处理能力。
- 安全测试:测试程序的安全性以及对外部攻击的抵抗能力。
- 兼容性测试:测试程序在不同的硬件和软件环境下是否正常工作。
用户验收测试包括以下子类:
- 系统验收测试:由用户或客户进行的测试,以确保软件符合用户的需求和要求。
- Alpha测试:由软件开发人员在开发过程中进行的测试,以发现和修复问题。
- Beta测试:由广大用户进行的测试,以收集用户的反馈和建议,以及发现和修复问题。
- 验证测试:测试程序是否按照用户需求和规格说明书的要求工作。
- 接口测试:测试程序的接口是否按照设计要求工作。
- 安全测试:测试程序的安全性以及对外部攻击的抵抗能力。
- 兼容性测试:测试程序在不同的硬件和软件环境下是否正常工作。
- 可用性测试:测试程序的易用性和用户体验。
像上文中,壹哥给大家说的游戏开发时的公测、内测,主要是为了发现和修复问题,以及收集用户的反馈和建议而进行的测试,所以都属于是功能测试的一种。而从另一个角度来说,公测也属于用户验收测试中的Beta测试。实际上,这些不同的测试分类,它们的归属可能是重合的,很多时候从不同的角度进行考察,可能会归于不同的类型。这就好比一个人,从性别上来说是男人,根据年龄来分属于年轻人,根据职业来分又是工程师等。
3. 测试工具
针对以上这些测试分类,我们不可能靠自己手动进行操作,目前各种测试类型其实都有对应的测试工具或框架,接下来壹哥再跟大家说几种常见的测试工具:
- 单元测试框架 :JUnit 、TestNG、Mockito、PowerMock等;
- 集成测试框架 :Selenium、Cucumber、Fitnesse、SoapUI等;
- 性能测试工具 :JMeter、Gatling、LoadRunner等;
- 安全测试工具:Burp Suite、WebInspect、AppScan等;
- 可靠性测试工具:Fault Injection、Chaos Monkey等;
- 兼容性测试工具:BrowserStack、Sauce Labs等;
- 用户体验测试工具:UserTesting、UsabilityHub、Optimizely等。
以上这些测试工具可以帮助测试人员实现自动化测试,提高测试效率和测试质量。不同的测试种类需要使用不同的测试工具,我们要根据具体的情况选择合适的测试工具进行测试。
但是大家要注意,以上这些测试分类和测试工具,大部分都是专业的测试人员需要掌握的。测试作为一个独立的技术岗位,其实也是需要进行专门的学习的,千锋教育也有很专业的测试课程,如果大家对测试刚兴趣,可以来学习哦。本文不会讲解其他太多的测试内容,接下来我们主要是学习如何在Java中实现单元测试和集成测试。
二. 单元测试
1. 概念
单元测试是编程过程中很常见的一种测试方法,属于是功能测试,它是把程序分解成最小的可测试单元进行测试,一般是测试某个方法或类的功能是否符合预期。
2. 常用框架
在Java中,JUnit是最流行的单元测试框架,该框架可以帮助我们编写出自动化的单元测试用例,并给我们提供测试结果的详细报告。除了JUnit框架之外,还有TestNG、Mockito、PowerMock等也可以实现单元测试。我们来看看这几种测试框架的区别:
单元测试框架 | 特点 | 优点 | 缺点 |
---|---|---|---|
JUnit | 注释驱动,灵活性高,易于集成 | 简单易用,学习成本低;易于集成到开发环境中;提供了多种断言方法,可以根据需要选择;提供了详细的测试报告,方便查看测试结果 | 不支持并发测试;不能很好地处理测试用例之间的依赖关系 |
TestNG | 注释驱动,支持并发测试 | 支持多线程测试,可以更快地运行测试用例;提供了丰富的配置和扩展选项 | 学习成本相对较高;集成不如JUnit广泛 |
Mockito | 可以模拟对象和行为 | 简化测试用例编写;可以模拟不易构造或访问的对象 | 对于没有接口或虚方法的类,无法使用Mockito |
综合对比下来,JUnit相对来说易学易用且性能优良,所以是目前Java中最流行的单元测试框架。
三. JUnit框架
1. 简介
壹哥在上面说过,JUnit凭借自身的良好特性,是目前Java中最流行的单元测试框架。该框架可以帮助我们编写出自动化的单元测试用例,并给我们提供测试结果的详细报告。
JUnit框架具有如下特性:
- 注解驱动:使用注解标记测试方法,而不是继承特定的类或实现特定的接口。
- 灵活性高:提供了多种断言方法,可以根据需要选择使用,同时也提供了很多用于设置和清理测试环境的方法。
- 易于集成:可以与Eclipse、IntelliJ IDEA等Java IDE集成,也可以与Maven、Gradle等构建工具集成。
2. 特点
JUnit框架作为一个流行的单元测试框架,既有优点,也存在不足,总的来说具有如下优缺点。
2.1 优点
- 简单易用,学习成本低;
- 易于集成到开发环境中;
- 提供了多种断言方法,可以根据需要选择;
- 提供了详细的测试报告,方便查看测试结果。
2.2 缺点
- 不支持并发测试;
- 不能很好地处理测试用例之间的依赖关系。
3. 使用场景
基于以上特点,我们通常是在以下场景中使用JUnit:
- 单元测试
- 集成测试
- 回归测试
后面壹哥会利用JUnit框架,给大家讲解如何进行单元测试。
4. 常用注解
壹哥 在前面说过,JUnit是基于注解驱动的单元测试框架,主要是使用注解来标记测试方法,而不是继承特定的类或实现特定的接口。所以接下来壹哥要先给大家讲解JUnit框架中,常用的注解及其作用和用法:
- @Test
- 作用:标记一个测试方法。这是最常用的一个注解。
- 用法:将@Test注解标记在测试方法的上面。注意:测试方法必须是public修饰,返回值类型必须是void,参数列表必须为空。
- @Before
- 作用:标记在测试方法之前要执行的方法。
- 用法:将@Before注解标记在测试方法之前需要执行的方法上方。
- @After
- 作用:标记在测试方法之后执行的方法。
- 用法:将@After注解标记在测试方法之后需要执行的方法上方。
- @BeforeClass
- 作用:标记在测试类中所有的测试方法执行之前只执行一次的方法。
- 用法:将@BeforeClass注解标记在需要在所有测试方法执行之前执行的方法上方。该方法必须是public static类型的。
- @AfterClass
- 作用:标记在测试类中所有的测试方法执行之后只执行一次的方法。
- 用法:将@AfterClass注解标记在需要在所有测试方法执行之后执行的方法上方。该方法必须是public static类型的。
- @Ignore
- 作用:标记一个测试方法被忽略。
- 用法:将@Ignore注解标记在需要被忽略的测试方法上方,被忽略的测试方法不会被执行。
- @RunWith
- 作用:指定测试运行器,该方法也比较常用。
- 用法:将@RunWith注解标记在测试类上方,并指定运行器类。JUnit提供了多个运行器,包括BlockJUnit4ClassRunner、Parameterized、Suite等。
除了上述注解,JUnit还给我们提供了一些其他的注解,如@Parameters、@Category、@Rule等,用于进行参数化测试、测试分类、测试规则等。
5. 测试用例
了解了JUnit之后,接下来我们就要看看如何利用JUnit来编写一个测试用例了。
5.1 概念
所谓的测试用例,是指测试人员通过代码编写的一组测试步骤,用于验证软件是否在按照用户的需求和规格说明书进行工作。测试用例通常要包括输入数据、预期结果和实际结果等信息,以及测试用例的执行者、执行时间、执行结果等元数据。一个好的测试用例可以帮助测试人员检测出程序中存在的问题,并提供详细的测试报告,以便开发人员及时修复问题。而利用JUnit编写的测试用例,通常要包括以下内容:
- 创建测试类:每个测试类都会与一个被测试的类相关联,一个测试类中可以包含一个或多个测试方法。
- 定义测试方法:测试方法必须是public修饰的,且用@Test注解进行标记,并返回void类型的结果。
- 编写测试代码:测试方法应该包含我们要测试的代码,及检查测试结果是否正确的代码。
5.2 开发规范
我们在编写JUnit单元测试用例时,并不是随心所欲的,应该遵循以下规范要求:
- 测试类名应与被测试类名相同,但要在测试类名后添加"Test"后缀;
- 测试方法名应以"test"开头,以表明该方法是一个测试方法;
- 测试方法应该使用@Test注解进行标记;
- 测试方法必须用public修饰,返回值是void类型,且方法中不能带有任何参数;
- 测试方法应尽可能地独立于其他测试方法,以便在测试失败时更容易地确定问题所在;
- 测试方法应该使用Assert类中的断言方法来验证测试结果是否正确;
- 测试方法应该使用@Before和@After注解来设置和清理测试环境;
- 测试方法应该使用@BeforeClass和@AfterClass注解来设置和清理测试环境,这些方法只会在测试类的所有测试方法执行之前或之后执行一次;
- 测试方法应该使用@Ignore注解来忽略某些测试方法,这些测试方法不会被执行。
5.3 实现案例
说了这么多理论内容,还是让我们来看一个简单的测试用例吧,代码如下:
java
//静态导包,引入Assert类
import static org.junit.Assert.assertEquals;
import org.junit.jupiter.api.Test;
import com.yyg.work.Calculator;
/**
* @author 一一哥Sun
* @company 千锋教育
*/
public class CalculatorTest {
//测试方法以public修饰,返回值void,不带任何参数。方法名以test开头
@Test
public void testAddition() {
//创建Calculator对象
Calculator calculator = new Calculator();
//assertEquals是Assert静态类中的方法,我们可以通过静态导包引入该方法,然后直接调用
//assert-->断言,判断2+2的结果是否等于4,如果等于则执行通过,否则报错
assertEquals(4, calculator.add(2, 2));
}
}
/**
* @author 一一哥Sun
* @company 千锋教育
*/
public class Calculator {
//定义一个add加法
public int add(int x, int y) {
return x + y;
}
}
在上面的示例中,壹哥使用JUnit编写了一个名为CalculatorTest的测试类,并使用@Test注解标记了一个名为testAddition的测试方法。在测试方法中,我们创建了一个Calculator对象,并使用assertEquals断言方法来验证add方法的结果是否正确。如果测试失败,则JUnit将在报告中提供错误信息和堆栈跟踪。在测试类中运行测试方法,可以把鼠标的光标定位在测试方法的名称上,或者双击选中方法名也行,然后右键选择Run As即可,如下图所示:
然后执行过程及结果如下图所示:
此时如果我们把断言的参数修改一下,断言2+2=3,就会出现如下图所示的结果:
6. 异常处理
在单元测试中,我们也需要测试程序在异常情况下的行为。JUnit给我们提供了多种处理异常的方法,包括使用@Test标注的expected属性、使用@Test标注的timeout属性、使用@Rule标注的ExpectedException、使用@Test(expected=Exception.class)等。其中,使用@Rule注解的ExpectedException是最常用的处理异常的方法之一。下面是一个使用ExpectedException的示例:
java
//静态导包,引入Assert类
import static org.junit.Assert.assertEquals;
import org.junit.Rule;
import org.junit.jupiter.api.Test;
import org.junit.rules.ExpectedException;
import com.yyg.work.Calculator;
/**
* @author 一一哥Sun
* @company 千锋教育
*/
public class CalculatorTest {
//处理异常
//制定异常的处理规则
@Rule
public ExpectedException exceptionRule = ExpectedException.none();
@Test
public void testDivideByZero() {
//期待该异常由算术异常来处理
exceptionRule.expect(ArithmeticException.class);
//期待预料会产生除零错误
exceptionRule.expectMessage("/ by zero");
Calculator calculator = new Calculator();
//调用方法
calculator.divide(1, 0);
}
}
/**
* @author 一一哥Sun
* @company 千锋教育
*/
public class Calculator {
//除法
public int divide(int x, int y) {
return x / y;
}
}
在上面的示例中,我们使用了ExpectedException来测试程序在除零的情况下,是否会抛出ArithmeticException异常,且异常信息是否为/ by zero。如果测试失败,JUnit就会在报告中提供错误信息和堆栈跟踪报告,如下图所示:
7. 处理条件
在单元测试中,我们还可以测试一个程序在不同参数情况下的行为。JUnit给我们提供了多种处理条件的方法,包括使用@RunWith注释的Parameterized、使用@Test注解的Assume、使用@Rule注解的ExternalResource等。其中,使用Parameterized处理条件是最常用的方法之一。但是因为JUnit框架目前流行的版本有JUnit4和JUnit5两大版本,且两个版本中的API稍有不同,所以接下来壹哥针对这两个版本分别讲解。
我们在执行时,需要选择合适的Runner版本。执行参数化测试时,要选择合适的JUnit版本,否则可能会产生异常。运行测试时的版本选择可以按下图所示:
7.1 JUnit4处理方案
下面是JUnit4版本中使用Parameterized的示例:
java
//静态导包,引入Assert类
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import com.yyg.work.Calculator;
/**
* @author 一一哥Sun
* @company 千锋教育
*/
//@RunWith(JUnitParamsRunner.class)
@RunWith(Parameterized.class)
public class CalculatorTest {
private int x;
private int y;
private int expected;
public CalculatorTest(int x, int y, int expected) {
this.x = x;
this.y = y;
this.expected = expected;
}
//参数化处理,定义方法中要使用的参数
@Parameterized.Parameters
public static Collection<Object[]> data() {
//定义几个待测试的参数
return Arrays.asList(new Object[][]{
{2, 2, 4},
{1, 1, 3},
{0, 0, 0},
{-1, -1, -2},
{-1, 1, 0},
{1, -1, 0}
});
}
@Test
public void testAddition() {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(x, y));
}
}
在上面的示例中,我们使用了Parameterized来测试程序在不同情况下的行为。在这个案例中,壹哥给大家定义了一个名为data的静态方法来提供测试数据,然后使用@Parameterized.Parameters注解将其与测试方法关联。在测试方法中,我们创建了一个Calculator对象,并使用assertEquals断言方法验证add方法的结果是否正确。另外大家还有在类上添加一个@RunWith(Parameterized.class)注解,指明该类要进行参数化测试。
如果我们的参数都没问题,则会执行如下结果:
如果我们的参数有问题,则会产生如下结果:
7.2 JUnit5处理方案
JUnit 5中进行参数化处理,使用了与JUnit 4不同的注解来处理参数化测试。具体地说,我们需要在测试类上使用@RunWith(JUnitParamsRunner.class)注解,并将测试方法标记为@ParameterizedTest,而不是@Test,然后还需要使用@ValueSource注解提供测试数据。以下是一个使用JUnit 5的示例:
java
//静态导包,引入Assert类
import static org.junit.Assert.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import com.yyg.work.Calculator;
/**
* @author 一一哥Sun
* @company 千锋教育
*
* 适用于JUnit5版本
*/
public class CalculatorTest2 {
/**
* @ParameterizedTest、@ValueSource:JUnit5中的API
*/
@ParameterizedTest
@ValueSource(ints = {2, 3, 4})
public void testAddition(int i) {
Calculator calculator = new Calculator();
assertEquals(i + 2, calculator.add(2, i));
}
}
在上面的示例中,我们使用了JUnit 5的@ParameterizedTest注解来测试程序在不同情况下的行为。我们使用@ValueSource注解提供了一个整数数组,然后在测试方法中,我们创建了一个Calculator对象,并使用assertEquals断言方法来验证add方法的结果是否正确。如果测试失败,则JUnit会在报告中提供错误信息和堆栈跟踪。
------------------------------正片已结束,来根事后烟----------------------------
四. 结语
至此,壹哥 就带大家学习了该如何对Java程序进行单元测试。当然,实际上测试的内容还有很多,比如集成测试、性能测试等,但这些内容并不是初级阶段适合学习的。请大家持续关注壹哥 ,后面我会输出更多内容回馈大家。另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。