单元测试说明

什么是单元测试

单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误。

单元测试的价值

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

相关框架:

  1. 测试框架:测试框架是一种工具或库,用于编写、组织和运行测试用例。它提供了一组规则和结构,帮助开发人员有效地进行单元测试、集成测试等各种测试。

  2. 断言框架:断言框架用于编写测试用例中的断言,即验证代码执行结果是否符合预期。它提供了丰富的断言方法,用于比较、判断和验证预期结果。

  3. MOCK框架:用于模拟系统中的组件,定义模拟对象的行为,并验证代码与模拟对象的交互。

相关依赖:

1.Spring Boot Starter Test – JUnit5:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.9.1</version>
    <scope>test</scope>
</dependency>
  • Spring Boot Starter Test 包含了一些用于测试 Spring Boot 应用程序的依赖项,包括 TestNG 和 JUnit 的支持。
  • 已经引入Spring Boot Starter Test时,无需单独引入Junit5依赖

2.AssertJ :

<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.24.2</version>
    <scope>test</scope>
</dependency>

  • AssertJ 是一个流式断言库,用于编写更具可读性和表达性的断言。
  • AssertJ相对JUnit本身的语法更加清晰、流畅,使断言更易读。
  • AssertJ 提供了更多的断言选项,覆盖了更多的测试场景。
  • AssertJ 支持链式调用,可以在一个断言中完成多个验证。

3.Mockito:

        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.14.5</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy-agent -->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>1.14.5</version>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>4.11.0</version>
            <scope>test</scope>
        </dependency>
  • Mockito是一个用于Java的开源Mocking(模拟)框架。它允许你在测试中创建和使用模拟对象,从而轻松模拟和控制对象的行为。
  • Mockito旨在提供简洁、直观的API,使得编写测试更加容易。

###4.Testable:

<dependency>
	<groupId>com.alibaba.testable</groupId>
	<artifactId>testable-all</artifactId>
	<version>${testable.version}</version>
	<scope>test</scope>
</dependency>
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<configuration>
		<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.7.9/testable-agent-0.7.9.jar</argLine>
	</configuration>
</plugin>
  • 单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。
  • 无需初始化,不挑服务框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好Mock定义,加个@MockInvoke注解,一切统统搞定。

JUnit5:

import org.junit.jupiter.api.*;

/**
 * 功能描述: JUnit5测试类;
 *
 * @Author: wangxu
 * @Date: 2023/12/15 13:18
 */
public class JUnit5Test {

    @BeforeAll
    static void before() {
        System.out.println("=====BeforeAll=====");
    }


    @AfterAll
    static void after() {
        System.out.println("=====AfterAll=====");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("=====BeforeEach=====");
    }

    @AfterEach
    void afterEach() {
        System.out.println("=====AfterEach=====");
    }


    @org.junit.jupiter.api.Test
    @DisplayName("简单测试")
    void testSimple() {
        System.out.println("第一个测试方法");
    }


    @Disabled
    @org.junit.jupiter.api.Test
    @DisplayName("禁用测试")
    public void disabledTest() {
        //这个测试不会运行
        System.out.println("这个测试不会执行");
    }


    @DisplayName("重复测试")
    @RepeatedTest(value = 3)
    public void i_am_a_repeated_test() {
        System.out.println("执行测试");
    }


    @Nested
    @DisplayName("第一个内嵌测试类")
    class FirstNestTest {
        @org.junit.jupiter.api.Test
        void test() {
            System.out.println("第一个内嵌测试类执行测试");
        }
    }

    @Nested
    @DisplayName("第二个内嵌测试类")
    class SecondNestTest {
        @org.junit.jupiter.api.Test
        void test() {
            System.out.println("第二个内嵌测试类执行测试");
        }
    }
}

AssertJ:

编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言

import org.junit.jupiter.api.Test;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
// 使用 AssertJ的断言
public class AssertJSarnpleTest {
    @Test
    public void testUsingAssertJ() {

        // 子符串判断
        String s = "abcde";
        assertThat(s).as("字符串判断,判断首尾及长度").startsWith("ab").endsWith("de").hasSize(5);
        // 数字判断
        Integer i = 50;
        assertThat(i).as("数字判断,数字大小比较").isGreaterThan(10).isLessThan(100);
        // 日期判断
        Date date1 = new Date();
        Date date2 = new Date(date1.getTime() + 100);
        Date date3 = new Date(date1.getTime() - 100);
        assertThat(date1).as("日期判断:日期大小比较").isBefore(date2).isAfter(date3);
        // list比较
        List<String> list = Arrays.asList("a", "b", "c", "d");
        assertThat(list).as("list的首尾元素及长度").startsWith("a").endsWith("d").hasSize(4);
        // Map判断
        Map<String, Object> map = new HashMap<>();
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);
        assertThat(map).as("Map的长度及键值测试").hasSize(3).containsKeys("A", "B", "C");
    }
}

Mockito:

import com.hzsun.core.common.exception.BusinessException;
import com.hzsun.uc.UserCenterStarter;
import com.hzsun.uc.pojo.response.UserInfoResponse;
import com.hzsun.uc.service.impl.UcUserServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;

import java.util.Random;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

/**
 * 功能描述: ;
 *
 * @Author: wangxu
 * @Date: 2023/12/18 9:38
 */
//指定springboot的启动入口,模拟一个服务器环境,不然会在初始化websocket时报错
@SpringBootTest(classes = UserCenterStarter.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MockitoTest {


    @SpyBean
    private UcUserServiceImpl userServiceSpy;

    @MockBean
    private UcUserServiceImpl userServiceMock;


    @Mock
    private Random randomMock;

    @Spy
    private Random randomSpy;

    @BeforeEach
    void each(){
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void mock_normal_class(){
        assertThat(randomMock.nextInt()).as("mock注解会默认模拟为这个类型的默认值,如int就模拟为0:").isEqualTo(0);
        assertThat(randomSpy.nextInt()).as("spy对象,它默认会调用原始方法,除非显式地对方法进行了打桩");
        when(randomSpy.nextInt()).thenReturn(10).thenReturn(20).thenThrow(new BusinessException("只能调用2次"));
        assertThat(randomSpy.nextInt()).as("spy注解打桩后会走mock的方法").isEqualTo(10);
        assertThat(randomSpy.nextInt()).as("spy注解打桩后会走mock的方法").isEqualTo(20);
//        assertThat(randomSpy.nextInt()).as("spy注解打桩后会走mock的方法").isEqualTo(30);
        verify(randomSpy,times(3)).nextInt();
    }

    @Test
    public void mock_spirng_bean() {

        UserInfoResponse oneById = userServiceMock.getOneById(1);
        assertThat(oneById).isNull();

        UserInfoResponse userInfoResponse = new UserInfoResponse();
        userInfoResponse.setId(1);
        userInfoResponse.setUserName("王旭");
        userInfoResponse.setSex(1);
        userInfoResponse.setBirthday("1998-10-04");
        userInfoResponse.setUserNumber("123456789");
        when(userServiceMock.getOneById(1)).thenReturn(userInfoResponse).thenReturn(userInfoResponse);

        assertThat(userServiceMock.getOneById(1)).extracting(UserInfoResponse::getUserName).isEqualTo("王旭");
        assertThat(userServiceMock.getOneById(1)).hasFieldOrPropertyWithValue("sex",1);
    }
}

#Testable

package com.hzsun.uc.test;

import cn.hutool.core.collection.CollUtil;
import com.hzsun.uc.service.IUcTnUserClassService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;


/**
 * 功能描述: 主要介绍如何mock私有方法和spring中bean的模拟;
 *
 * @Author: wangxu
 * @Date: 2023/12/18 16:37
 */

@Component
@Slf4j
public class Testable {

    @Autowired
    private IUcTnUserClassService tnUserClassService;

    public void replaceUserClasses(Integer userId, Integer userMainClassId, Set<Integer> userClassIds) {
        Set<Integer> classIdSet = new HashSet<>();
        classIdSet.add(userMainClassId);
        if (CollUtil.isNotEmpty(userClassIds)) {
            classIdSet.addAll(userClassIds);
        }
        log.info("replaceUserClasses被调用");
        tnUserClassService.replaceAll(userId, classIdSet);
    }
}
package com.hzsun.uc.test;

import com.alibaba.testable.core.annotation.MockInvoke;
import com.alibaba.testable.core.model.MockScope;
import com.hzsun.uc.UserCenterStarter;
import com.hzsun.uc.service.IUcTnUserClassService;
import com.hzsun.uc.service.impl.UcTnUserClassServiceImpl;
import com.hzsun.uc.test.Testable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked;
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;

/**
 * 功能描述: 主要介绍如何mock私有方法和spring中bean的模拟;
 *
 * @Author: wangxu
 * @Date: 2023/12/18 16:37
 */
public class TestableTest {

    private Testable testable = new Testable();

    public static class Mock{

        @MockInvoke(targetClass = IUcTnUserClassService.class)
        private void replaceAll(Integer userId, Collection<Integer> classIds){
            System.out.println("replaceAll被调用了");
        }
    }

    @Test
    public void mock_spirng_bean() {
        testable.replaceUserClasses(1,1,new HashSet<>());

        Set<Integer> userMainClassIdSet = new HashSet<>();
        userMainClassIdSet.add(1);

        testable.replaceUserClasses(1,1,userMainClassIdSet);
    }

}


规范:

遵循 F.I.R.S.T 原则:

  • Fast(快速): 单元测试应该非常迅速执行。
  • Independent(独立): 单个测试不应该依赖于其他测试的执行结果。对于外部依赖(例如数据库、网络调用),使用模拟(Mock)来隔离测试。
  • Repeatable(可重复): 在任何环境中,测试都应该产生相同的结果。
  • Self-Validating(自验证): 测试应该有一个布尔输出,测试通过则返回 true,否则返回 false。
  • Timely(及时): 最好在编写实际代码之前编写测试,而不是等到实现完成后再写。

单测包结构

1、test包结构包名必须同main一致

2、对应类的单元测试类命名:${className}Test,

eg: main:com.hellobike.ride.application.RideIfaceImpl ===> test:com.hellobike.ride.application.RideIfaceImplTest

为测试使用清晰和描述性的命名:

测试方法应该清晰地描述正在测试的场景或功能。使用命名约定,例如 replaceUserClasses方法的测试方法为replaceUserClasses_nonEmptyUserClassIds和replaceUserClasses_emptyUserClassIds

使用JaCoCo进行类覆盖率、方法覆盖率、行覆盖率的统计。行覆盖率需达到?

写可维护的测试:

单元测试是可维护性的一部分。当代码发生变化时,确保测试也相应地进行更新。

覆盖关键路径和边界情况、异常情况:

确保测试覆盖主要路径,并考虑在输入边界情况/异常情况下进行测试。

定期运行测试:

将测试集成到持续集成(CI)流程中,以确保每次代码更改时都运行测试。

单元测试报告 版本:V1.3 文 档 编 号 保 密 等 级 作 者 最后修改日期 审 核 人 最后审批日期 批 准 人 最后批准日期 修订记录 日期 版本 修订说明 修订人 目 录 1 简介 2 1.1 目的 2 1.2 背景 2 1.3 范围 2 2 测试用例清单 2 3 功能测试分析 2 4 边界测试分析 2 5 覆盖率分析 2 6 内存使用分析 2 7 典型缺陷记录 3 7.1 缺陷1 3 7.1.1 表现 3 7.1.2 原因 3 7.1.3 方案 3 8 测试数据分析 3 8.1 测试有效性分析 3 8.2 测试效率分析 3 9 产品质量分析 4 10 测试结论 4 简介 目的 【描述该单元测试报告的目的。】 背景 【描述单元测试报告的背景,单元测试活动目的。如无特殊背景信息,可裁剪。】 范围 【说明单元测试报告在整个项目周期的适用范围】 测试用例清单 模块 目标类 级别 用例类 用例描述 执行结果 备注 【被测的代码类】 【代码级别】 【Junit测试类1】 【意图描述】 【P/F】 【Junit测试类2】 功能测试分析 边界测试分析 覆盖率分析 目标类 级别 方法覆盖率 行覆盖率 备注 【被测的代码类】 【代码级别】 内存使用分析 典型缺陷记录 记录单元测试中所发现的典型缺陷或常见缺陷。供再次发现同类问题时,作为参考使用。 缺陷1 表现 【缺陷表现描述】 原因 【缺陷产生原因分析描述】 方案 【解决方案描述】 测试有效性分析 【统计实际发现的缺陷数据,分析与计划值产生偏差的原因,结合《项目量化管理计划》定义的阈值,确定是否采取相关措施】 计划发现缺陷数 致命 严重 一般 实际发现缺陷数 偏差分析 对策或调整措施 产品质量分析 【结合上述数据和信息,对本次测试的项目、产品的本身质量进行分析、评价和总结】 测试结论  【描述测试是否达到测试计划的目的,是否满足单元测试的结束条件。】
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值