从零开始学Java之单元测试简介

作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦

千锋教育高级教研员、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编写的测试用例,通常要包括以下内容:

  1. 创建测试类:每个测试类都会与一个被测试的类相关联,一个测试类中可以包含一个或多个测试方法。
  2. 定义测试方法:测试方法必须是public修饰的,且用@Test注解进行标记,并返回void类型的结果。
  3. 编写测试代码:测试方法应该包含我们要测试的代码,及检查测试结果是否正确的代码。

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程序进行单元测试。当然,实际上测试的内容还有很多,比如集成测试、性能测试等,但这些内容并不是初级阶段适合学习的。请大家持续关注壹哥 ,后面我会输出更多内容回馈大家。另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

相关推荐
Java探秘者13 分钟前
Maven下载、安装与环境配置详解:从零开始搭建高效Java开发环境
java·开发语言·数据库·spring boot·spring cloud·maven·idea
攸攸太上14 分钟前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
2301_7869643619 分钟前
3、练习常用的HBase Shell命令+HBase 常用的Java API 及应用实例
java·大数据·数据库·分布式·hbase
2303_8120444622 分钟前
Bean,看到P188没看了与maven
java·开发语言
苹果醋323 分钟前
大模型实战--FastChat一行代码实现部署和各个组件详解
java·运维·spring boot·mysql·nginx
秋夫人25 分钟前
idea 同一个项目不同模块如何设置不同的jdk版本
java·开发语言·intellij-idea
m0_6640470230 分钟前
数字化采购管理革新:全过程数字化采购管理平台的架构与实施
java·招投标系统源码
罗曼蒂克在消亡30 分钟前
graphql--快速了解graphql特点
后端·graphql
潘多编程32 分钟前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql
aqua35357423581 小时前
蓝桥杯-财务管理
java·c语言·数据结构·算法