实验05 单元测试

知识点

  • 单元测试的定义

  • 单元测试(Unit Testing)是一种软件开发的验证过程,旨在隔离并检测软件组件(通常是函数、方法或类)的单个单元的功能是否按照预期执行。每个测试用例验证特定的条件或功能,确保代码的每个部分都能独立地按预期工作。
  • 单元测试与集成测试、系统测试的区别。
    • 集成测试(Integration Testing):

      • 目的:验证多个单元或组件在一起工作时的交互和协作是否正确。
      • 范围:通常在完成单元测试后进行,关注组件间的接口和交互。
      • 特点:可能需要模拟外部系统或使用实际的外部系统。
    • 系统测试(System Testing):

      • 目的:验证整个系统的功能是否满足用户需求和系统规格。
      • 范围:在所有集成测试完成后进行,涵盖整个系统。
      • 特点:测试用例基于用户需求和系统规格,可能包括性能测试、安全性测试等。
    • 接受测试/验收测试(Acceptance Testing):

      • 目的:确保软件满足业务需求和用户验收标准。
      • 范围:通常在系统测试之后,由用户或用户代表执行。
      • 特点:测试用例通常由用户编写,关注用户的工作流程和业务规则。
    • 性能测试(Performance Testing):

      • 目的:评估软件应用的速度、响应时间、稳定性、资源消耗等性能指标。
      • 范围:可以是单个组件也可以是整个系统。
      • 特点:关注在高负载或特定条件下系统的行为。
    • 回归测试(Regression Testing):

      • 目的:确保软件变更没有引入新的错误。
      • 范围:可以是单个单元、多个组件或整个系统。
      • 特点:通常在代码修改或添加新功能后进行。
  • 单元测试的目的

  • 目的
    • 早期发现错误:在开发周期的早期阶段捕捉和修复缺陷。
    • 提供文档:测试用例可以作为代码行为的文档。
    • 支持重构:确保在修改代码后原有功能仍然正常工作。
  • 单元测试的基本原则

  • 关键特点

    • 独立性

      • 每个单元测试应该独立于其他测试运行,不依赖于系统的其他部分或外部环境的状态。
      • 测试不应依赖于数据库、文件系统、网络或任何全局状态。
    • 原子性
      • 每个单元测试应该只测试一个具体的功能点或逻辑分支。
      • 避免在一个测试中验证多个功能,以确保测试的明确性和易于理解。
    • 可重复性
      • 无论何时何地执行,测试都应该产生相同的结果。
      • 这意味着测试不应该依赖于系统状态或时间敏感的操作。
  • 测试框架的选择和介绍

    • JUnit (Java)

      • 特点:JUnit是Java语言中最流行的单元测试框架之一,广泛用于Java和Android应用开发。JUnit 4主要使用注解来标识测试方法,而JUnit 5(Jupiter)则引入了更多的功能和改进。
      • 适用场景:适用于Java和Kotlin项目,特别是需要大量自动化测试的企业级应用。
    • TestNG

      • 特点:TestNG是JUnit的一个增强版,提供了更多的功能,如参数化测试、并行测试执行等。它同样使用注解来定义测试方法。
      • 适用场景:适用于需要复杂测试配置和并行测试的大型Java应用。
    • pytest (Python)

      • 特点:pytest是一个简单而强大的Python测试框架,支持简单的断言和参数化测试,并且可以很容易地与持续集成系统结合。
      • 适用场景:适用于Python项目,特别是科学计算和数据分析领域。
    • NUnit (.NET)

      • 特点:NUnit是.NET平台上的一个单元测试框架,受到JUnit的启发。它支持属性、断言和测试运行程序。
      • 适用场景:适用于C#、F#和VB.NET项目,特别是需要集成.NET特定功能和特性的应用程序。
    • Mocha (JavaScript/Node.js)

      • 特点:Mocha是一个灵活的JavaScript测试框架,适用于Node.js和浏览器。它支持异步测试,并且可以与其他断言库(如Chai)结合使用。
      • 适用场景:适用于JavaScript项目,特别是在需要处理异步操作和Promise的场景。
    • RSpec (Ruby)

      • 特点:RSpec是一个用于Ruby语言的行为驱动开发(BDD)框架,提供了一种表达性强的语法来编写测试。
      • 适用场景:适用于Ruby on Rails项目,特别是那些采用BDD方法论的团队。
    • Google Test (C++)

      • 特点:Google Test是一个用于C++的测试框架,提供了丰富的断言和测试组织功能。
      • 适用场景:适用于C++项目,特别是需要高性能和复杂测试结构的应用程序。
    • Karma (JavaScript)

      • 特点:Karma是一个测试运行器,可以为JavaScript应用提供实时的测试反馈。它通常与Mocha和Chai等断言库一起使用。
      • 适用场景:适用于需要在不同浏览器上进行测试的JavaScript前端应用。
    • Selenium

      • 特点:虽然Selenium主要用于自动化Web浏览器交互,但它也可以用来执行Web应用的单元测试。
      • 适用场景:适用于Web应用的自动化测试,特别是需要模拟用户交互的场景。
    • 选择测试框架时的考虑因素

      • 语言和平台支持:选择与你的编程语言和开发平台兼容的框架。
      • 社区和文档:选择有良好文档和活跃社区支持的框架,以便在遇到问题时能够快速找到解决方案。
      • 功能需求:根据项目需求选择提供所需功能的框架,例如参数化测试、并行测试等。
      • 集成需求:考虑框架与现有开发工具和持续集成系统的集成能力。
      • 团队熟悉度:选择团队成员熟悉或愿意学习的框架,以减少学习曲线。
  • 编写测试用例

    • 正向测试(Positive Testing)

      ​​​​​​
      • 定义:验证程序在正常或预期条件下的行为。
      • 方法
        • 确定代码的正常使用场景。
        • 设计测试用例以验证主要功能和流程。
        • 确保测试覆盖了最常见的使用情况。
      • 边界条件测试(Boundary Value Analysis)

      • 定义:验证程序在边界或极端条件下的行为。
      • 方法
        • 确定输入或循环的边界值,如数组的最小长度、最大长度、空数组等。
        • 测试循环的开始和结束条件。
        • 检查输入值的上限和下限。
      •  等价类划分(Equivalence Partitioning)

      • 定义:将输入数据划分为若干等价类,每个类的行为预期是相同的。
      • 方法
        • 识别有效和无效的输入值集合。
        • 从每个等价类中选择至少一个测试用例。
      • 错误猜测(Error Guessing)

      • 定义:基于经验和直觉,猜测可能存在缺陷的代码区域。
      • 方法
        • 识别代码中可能出错的逻辑。
        • 设计测试用例来验证这些猜测。
      • 测试用例的结构

      • 标题:明确测试用例的目的。
      • 前提条件:测试执行前必须满足的条件。
      • 测试步骤:详细描述执行测试的每个步骤。
      • 预期结果:定义测试执行后的预期行为或输出。
      • 实际结果:记录测试执行后的实际行为或输出。
      • 测试状态:标记测试通过或失败的状态。
  • 断言的使用

    • 断言的作用:

      • 验证预期结果: 断言允许测试者明确地指出代码执行后应该产生的结果。

      • 提供明确的错误信息: 当测试失败时,断言可以提供关于期望值和实际值差异的具体信息,这有助于快速定位问题。

      • 简化测试逻辑: 通过使用断言,测试代码可以更加简洁和直观,因为断言通常集成在测试框架中。

      • 自动化测试验证: 断言使得测试结果的验证自动化,无需手动检查输出。

      • 提高测试覆盖率: 断言有助于确保测试覆盖了所有重要的执行路径和边界条件。

    • 相等性断言

      • 验证两个值是否相等。
      • assertEquals(expectedValue, actualValue);
    • 不等性断言

      • 验证两个值是否不相等。
      • assertNotEquals(notExpectedValue, actualValue);

    • 真值断言

      • 验证一个条件是否为真。
      • assertTrue(condition);

    • 假值断言

      • 验证一个条件是否为假。
      • assertFalse(condition);

    • 异常断言

      • 验证代码执行时是否抛出了特定的异常。
      • assertThrows(ExpectedException.class, () -> {
            // 调用可能抛出异常的方法
        });

    • 范围断言

      • 验证一个值是否在特定的范围内。
      • assertGreater(expectedMin, actualValue);
        assertLess(expectedMax, actualValue);

    • 正则表达式断言

      • 验证一个字符串是否匹配特定的正则表达式。
      • assertMatchesRegex("expectedPattern", actualString);

    • 同一度断言

      • 验证两个引用是否指向同一个对象。
      • assertSame(expectedObject, actualObject);

  • 测试的组织和管理

    • 测试代码的组织结构

    • 按功能组织

      • 测试代码通常按照被测试的功能模块组织,每个模块或类有自己的测试类。
    • 目录结构

      • 在项目中创建一个专门的测试目录,如testtests,在这个目录下进一步按模块划分子目录。
    • 测试类和方法

      • 测试类通常以被测试类的名字命名,后跟Test作为后缀。
      • 测试方法的命名应清晰表达测试的意图,如testAdditionPositiveNumbers
    • 使用命名空间(针对某些语言):

      • 在支持命名空间的语言中,使用命名空间来组织测试代码,避免命名冲突。
    • 模块化测试代码

      • 将测试代码分解为模块或包,每个模块包含相关的测试类和辅助类。
    • 命名约定:

    • 测试类命名

      • 遵循ClassNameTestTestClassName的命名模式。
    • 测试方法命名

      • 使用test_前缀,后跟测试的场景或行为描述,如test_addition_with_negative_numbers
    • 常量和变量

      • 测试中使用的常量和变量应有明确和一致的命名规则。
    • 避免使用缩写

      • 在命名测试用例和变量时,为了提高可读性,避免使用缩写。
    • 测试的管理和执行策略:

    • 自动化测试执行

      • 使用持续集成(CI)工具自动执行测试,确保代码提交后立即验证。
    • 测试依赖管理

      • 确保测试代码不依赖于特定的运行顺序,每个测试都能独立运行。
    • 测试数据管理

      • 使用工厂模式或测试数据构建器模式来管理测试数据,确保数据的一致性和可复用性。
    • 测试环境隔离

      • 为测试提供一个隔离的环境,确保测试不会受到外部因素的干扰。
    • 测试覆盖率目标

      • 设定代码覆盖率目标,使用工具持续监控测试覆盖率。
    • 测试分层

      • 根据测试的范围和目的,将测试分层,如单元测试、集成测试、系统测试等。
    • 测试报告

      • 生成详细的测试报告,包括通过率、失败的测试用例、测试覆盖率等信息。
    • 测试维护

      • 定期审查和更新测试用例,确保它们与代码的当前状态保持一致。
    • 测试代码审查

      • 将测试代码纳入代码审查过程,确保测试的质量。
    • 测试优先级

      • 根据风险和重要性为测试用例设置优先级,优先执行关键功能的测试。
    • 测试版本控制

      • 将测试代码纳入版本控制系统,与应用代码同步演进。
  • Mocking和Stubs

    • Mock对象

      • Mock对象是一个模拟的、假的实现对象,用于在单元测试中代替实际的依赖对象。
      • 它主要用于验证被测试对象的行为,而不是依赖对象的行为。
    • Stubs

      • Stubs(存根)是提供预定响应的简单对象,通常用于模拟函数或方法的返回值。
      • 它们用于设置测试环境,以便在测试中模拟外部依赖的特定行为。
    • 应用场景:

    • 当单元测试需要隔离外部依赖时,使用Mock对象和Stubs来模拟这些依赖的行为。
    • 在测试中验证对象之间的交互,而不是它们的内部逻辑。
    • 使用Mocking工具隔离外部依赖:

      假设有一个UserService类,它依赖于一个UserRepository来获取用户数据。在单元测试中,我们不想与数据库交互,而是想模拟UserRepository的行为:

      @Test
      public void testGetUser() {
          // 创建Mock对象
          UserRepository mockRepository = Mockito.mock(UserRepository.class);
          
          // 设置Mock行为
          Mockito.when(mockRepository.findById(1)).thenReturn(new User(1, "TestUser"));
          
          // 创建被测试对象,并注入Mock的依赖
          UserService userService = new UserService(mockRepository);
          
          // 调用方法并验证结果
          User user = userService.getUser(1);
          assertEquals("TestUser", user.getName());
          
          // 验证交互
          Mockito.verify(mockRepository).findById(1);
      }

    • 展示如何使用Mocking工具来隔离外部依赖。
  • 测试覆盖率

    • 重要性:

    • 衡量测试的完整性

      • 测试覆盖率提供了一个量化的指标,用于衡量测试用例覆盖代码的程度。
    • 发现未测试的代码

      • 高覆盖率可以减少未被测试的代码部分,从而降低引入缺陷的风险。
    • 提高代码质量

      • 通过关注未被测试覆盖的代码区域,开发者可以提高代码的质量和健壮性。
    • 使用工具衡量和提高测试覆盖率:

    • 集成覆盖率工具

      • 在构建过程或持续集成流程中集成覆盖率工具,如JaCoCo、Cobertura或Istanbul。
    • 分析覆盖率报告

      • 运行测试后,生成覆盖率报告,分析哪些代码区域没有被测试覆盖。
    • 改进测试用例

      • 根据覆盖率报告,添加或改进测试用例,以覆盖未测试的代码。
    • 设置覆盖率目标

      • 为项目设定合理的覆盖率目标,并持续跟踪达成情况。
    • 持续改进

      • 将覆盖率作为代码审查和质量控制的一部分,持续改进测试覆盖率。

实验 

一 实验目的:

1、了解什么是单元测试,单元测试的级别、单元测试的内容。

2、掌握单元测试框架JUnit的使用。

3、掌握参数化测试方法的运用及测试脚本的编写。

二 实验环境

1、JDK8.0或以上;

2、Intellij IDEA集成开发环境;

3、Maven构建工具。

三 实验准备

1、掌握JUnit测试框架的基本使用;

2、具备Java编程基础;

3、安装及配置好测试环境。

四 实验内容

(一)网上蛋糕购物系统中,针对蛋糕商品查询业务的持久类GoodsDao中的getGoodsById、getCountOfGoodsByTypeID方法编写单元测试类(一般情况下,单元测试要对每个方法进行测试)。

(1)创建GoodsDaoTest测试类,编写对应的测试方法。请提供GoodsDaoTest测试类代码,要求代码中要对每个方法进行注释说明。

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

public class GoodsDaoTest {

    @Mock
    private GoodsDataAccess goodsDataAccess; // 假设这是访问数据库的接口

    @InjectMocks
    private GoodsDao goodsDao; // 被测试的持久类

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.initMocks(this); // 初始化Mock对象
    }

    // 测试getGoodsById方法
    @Test
    public void testGetGoodsById_WithValidId_ShouldReturnCorrectGoods() {
        // 准备
        int validId = 1;
        Goods expectedGoods = new Goods(validId, "Chocolate Cake", 20.0);
        Mockito.when(goodsDataAccess.getGoodsById(validId)).thenReturn(expectedGoods);

        // 执行
        Goods result = goodsDao.getGoodsById(validId);

        // 验证
        assertNotNull(result, "返回的对象不应为空");
        assertEquals(expectedGoods, result, "返回的蛋糕应与预期相符");
    }

    @Test
    public void testGetGoodsById_WithInvalidId_ShouldReturnNull() {
        // 准备
        int invalidId = -1;
        Mockito.when(goodsDataAccess.getGoodsById(invalidId)).thenReturn(null);

        // 执行
        Goods result = goodsDao.getGoodsById(invalidId);

        // 验证
        assertNull(result, "使用无效ID查询时,应返回null");
    }

    // 测试getCountOfGoodsByTypeID方法
    @Test
    public void testGetCountOfGoodsByTypeID_WithValidTypeId_ShouldReturnCorrectCount() {
        // 准备
        int validTypeId = 1;
        int expectedCount = 10;
        Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(validTypeId)).thenReturn(expectedCount);

        // 执行
        int result = goodsDao.getCountOfGoodsByTypeID(validTypeId);

        // 验证
        assertEquals(expectedCount, result, "返回的数量应与预期相符");
    }

    @Test
    public void testGetCountOfGoodsByTypeID_WithInvalidTypeId_ShouldReturnZero() {
        // 准备
        int invalidTypeId = -1;
        Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(invalidTypeId)).thenReturn(0);

        // 执行
        int result = goodsDao.getCountOfGoodsByTypeID(invalidTypeId);

        // 验证
        assertEquals(0, result, "使用无效类型ID查询时,应返回0");
    }
}

代码解释

  • @Mock注解用于创建模拟对象。
  • @InjectMocks注解用于创建被测试类的实例,并将模拟对象注入到它的依赖中。
  • @BeforeEach注解的方法在每个测试方法执行之前都会运行,用于设置测试环境。
  • @Test注解的方法是实际的测试用例。
  • Mockito.when(...).thenReturn(...)用于定义模拟对象的行为。
  • assertNotNullassertEqualsassertNull是JUnit提供的断言方法,用于验证测试结果是否符合预期。

 

(2)请设计3条测试用例测试getCountOfGoodsByTypeID方法,执行测试。

测试用例:

序号

测试用例编号

测试用例名称

输入数据

预期结果

测试结果

测试用例1: 有效商品类型ID

序号 测试用例编号 测试用例名称 输入数据 预期结果 测试结果
1 TC001 正常商品类型查询 1 >0 [待测试]

说明:此测试用例验证当提供有效的商品类型ID时,方法应返回该类型下商品的正数数量。

测试用例2: 无效商品类型ID(负数)

序号 测试用例编号 测试用例名称 输入数据 预期结果 测试结果
2 TC002 负数商品类型查询 -1 0 [待测试]

说明:此测试用例验证当商品类型ID为负数时,方法应返回0,表示没有商品匹配。

测试用例3: 边界商品类型ID(例如最大的正整数)

序号 测试用例编号 测试用例名称 输入数据 预期结果 测试结果
3 TC003 最大正整数商品类型查询 Integer.MAX_VALUE 0或实际数量 [待测试]

说明:此测试用例验证当商品类型ID为整数最大值时,方法的表现,可能是返回0或者实际的商品数量,取决于数据库中的数据。

 

(3)针对以上测试方法的不足(多条测试用例需要多次执行),根据参数化测试的规则,使用以上测试用例数据,修改测试方法并执行测试。请提供修改后的测试方法代码

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

public class GoodsDaoTest {

    @Mock
    private GoodsDataAccess goodsDataAccess;

    @InjectMocks
    private GoodsDao goodsDao;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    // 参数化测试用例
    @ParameterizedTest
    @CsvSource({
        "1, 10, 应返回正数的商品数量",
        "-1, 0, 应返回0表示没有商品",
        "2147483647, 5, 应返回实际的商品数量或0"
    })
    public void testGetCountOfGoodsByTypeID(int typeId, int expectedResult, String caseDescription) {
        // 准备
        Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(typeId)).thenReturn(expectedResult);

        // 执行
        int result = goodsDao.getCountOfGoodsByTypeID(typeId);

        // 验证
        assertEquals(expectedResult, result, caseDescription);
    }
}

  1. 新建一个Foo类,使用Mockito框架对该类进行测试

public class Foo {

    private Bar bar;

    public void setBar(Bar bar) {

        this.bar = bar;

    }

    public String doSomething() {

        return "Foo::doSomething " + bar.doSomethingElse();

    }

}

public class Bar {

    public String doSomethingElse() {

        return "Bar::doSomethingElse";

}

}

测试脚本截图为:

import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;

public class FooTest {

    @Mock
    private Bar mockBar; // 创建Bar的Mock对象

    @InjectMocks
    private Foo foo; // 被测试的Foo对象

    @BeforeEach
    public void setUp() {
        // 初始化Mock对象
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testDoSomething() {
        // 准备
        when(mockBar.doSomethingElse()).thenReturn("Bar::doSomethingElse");

        // 执行
        String result = foo.doSomething();

        // 验证
        verify(mockBar).doSomethingElse(); // 验证Bar的doSomethingElse方法被调用
        assertEquals("Foo::doSomething Bar::doSomethingElse", result);
    }
}

五 实验总结

(1)什么叫桩程序?桩程序有什么作用?

桩程序(Stub): 桩程序是一种模拟对象,用于在单元测试中代替实际的依赖项或外部系统。它通常用于模拟那些在测试环境中不可用或不适合使用的组件,如数据库、网络服务或复杂计算。

作用

  1. 隔离测试:桩程序允许开发者在不依赖外部系统或复杂逻辑的情况下测试代码。
  2. 控制测试环境:通过返回预定的响应,桩程序可以控制测试环境,确保测试的一致性和可重复性。
  3. 提高测试速度:使用桩程序可以避免耗时的外部调用,从而加快测试执行速度。
  4. 简化测试逻辑:桩程序可以简化测试逻辑,使测试用例更专注于验证被测试代码的行为。
  5. 模拟错误情况:桩程序还可以模拟错误情况或异常响应,帮助测试代码的健壮性和错误处理能力。

(2)使用参数化测试有什么好处?

参数化测试是一种测试方法,它允许使用不同的输入参数多次执行同一个测试方法。以下是使用参数化测试的一些好处:

  1. 减少重复代码:参数化测试可以减少编写和维护多个相似测试用例的代码量。
  2. 提高测试效率:通过一次性执行多个测试用例,参数化测试可以提高测试的效率和覆盖率。
  3. 简化测试数据管理:集中管理测试数据,便于更新和维护。
  4. 增强测试的灵活性:可以轻松地添加、修改或删除测试参数,以适应不同的测试需求。
  5. 提高测试的可读性:通过清晰的参数化表达,测试用例的意图和行为更容易被理解。
  6. 支持边界值分析:参数化测试可以方便地测试边界值和异常值,提高代码的边界条件覆盖率。
  7. 自动化测试执行:参数化测试通常与自动化测试框架结合使用,实现测试的自动化执行。
  8. 易于维护和扩展:随着软件需求的变更,参数化测试可以更容易地进行更新和扩展。
  9. 提供一致的测试结果:确保每个测试用例在相同的测试逻辑下运行,提供一致的测试结果。

你可能感兴趣的:(软件测试,单元测试)