Day03 Maven基础(核心)


什么是Maven
图1 Web标准组成
Maven的作用
图2 Maven的作用
图3 依赖管理
图4 项目构建
图5 统一项目结构
图6 Maven市场占有率

1. Maven概述

1.1 介绍

图7 Maven原理及其仓库
图8 Maven小结

1.2 安装

图9 Maven安装测试
图10 Maven安装小结

2. IDEA集成Maven

2.1 IDEA配置Maven环境(全局)

图11 IDEA配置Maven环境(全局)

全局配置需关闭Idea重启

2.2 创建Maven项目

① 创建本课程的源代码工作目录 G:\workspace\duSSM\web-ai-code 其中duSSM为个性化目录

② 创建Java空项目 web-ai-project01

③ 创建Maven模块 maven-project01

④ 创建 Helloworld.java并运行

图12 创建web-ai-project01和maven-project01
图13 创建web-ai-project01、maven-project01并运行

2.3 Maven坐标

什么是坐标?

图14 Maven坐标

Maven 坐标主要组成

图15 Maven坐标小结

2.4 导入Maven项目

先将要导入的maven项目复制到你的项目目录下

图16 Maven项目导入验证
图17 Maven项目导入小结

3. 依赖管理

3.1 依赖配置

图18 排除依赖
图19 添加依赖和排除依赖验证
图20 小结

3.2 生命周期

Maven的生命周期就是为了对所有的maven项目构建过程进行抽象和统一。

Maven中有3套相互独立的生命周期:

每套生命周期包含一些阶段(phase),阶段是有顺序的,后面的阶段依赖于前面的阶段。

图21 3套相互独立的生命周期
五个主要生命周期阶段:

在同一套生命周期中,当运行后面的阶段时,前面的阶段都会运行。

图22 5个主要生命周期阶段
执行指定生命周期的两种方式:
图23 编译、打包、安装本地仓库验证
图24 Maven生命周期阶段与插件
图25 Maven生命周期阶段小结

4. 单元测试

测试概述
单元测试 集成测试 系统测试 验收测试
介绍 对软件的基本组成单位进行测试,最小测试单位。 将已分别通过测试的单元,按设计要求组合成系统或子系统,再进行的测试 对已经集成好的软件系统进行彻底的测试。 交付测试,是针对用户需求、业务流程进行的正式的测试。
目的 检验软件基本组成单位的正确性。 检查单元之间的协作是否正确。 验证软件系统的正确性、性能是否满足指定的要求。 验证软件系统是否满足验收标准。
人员 开发人员 开发人员 测试人员 客户/需求方
图26 测试阶段划分
方法 描述
白盒 清楚软件内部结构、代码逻辑。用于验证代码、逻辑正确性。
黑盒 不清楚软件内部结构、代码逻辑。用于验证软件的功能、兼容性等方面。
灰盒 结合了白盒测试和黑盒测试的特点,既关注软件的内部结构又考虑外部表现(功能)。
图27 测试方法划分

4.1 快速入门

图28 Main单元测试方法与JUnit测试方法对比
图29 图示main测试方法
图30 图示Junit测试方法
【案例】使用JUnit,对UserService中业务方法进行单元测试。
<dependency>  
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.1</version>
</dependency>

cn/zjy/UserService.java

package cn.zjy;

import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;

public class UserService {

    /**
     * 给定一个身份证号, 计算出该用户的年龄
     * @param idCard 身份证号
     */
    public Integer getAge(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        String birthday = idCard.substring(6, 14);
        LocalDate parse = LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyyMMdd"));
        return Period.between(parse, LocalDate.now()).getYears();
    }

    /**
     * 给定一个身份证号, 计算出该用户的性别
     * @param idCard 身份证号
     */
    public String getGender(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        return Integer.parseInt(idCard.substring(16,17)) % 2 == 1 ? "男" : "女";
    }
}

cn/zjy/UserServiceTest.java

package cn.zjy;
import org.junit.jupiter.api.Test;
public class UserServiceTest {
    @Test
    public void testGetAge(){
        Integer age = new UserService().getAge("110002200505091218");
        System.out.println(age);
    }
}
图31 测试案例完成验证
图32 Junit测试小结

4.2 断言

JUnit提供了一些辅助方法,用来帮我们确定被测试的方法是否按照预期的效果正常工作,这种方式称为断言

断言方法 描述
Assertions.assertEquals(Object exp, Object act, String msg) 检查两个值是否相等,不相等就报错。
Assertions.assertNotEquals(Object unexp, Object act, String msg) 检查两个值是否不相等,相等就报错。
Assertions.assertNull(Object act, String msg) 检查对象是否为null,不为null,就报错。
Assertions.assertNotNull(Object act, String msg) 检查对象是否不为null,为null,就报错。
Assertions.assertTrue(boolean condition, String msg) 检查条件是否为true,不为true,就报错。
Assertions.assertFalse(boolean condition, String msg) 检查条件是否为false,不为false,就报错。
Assertions. assertThrows(Class expType, Executable exec, String msg) 检查程序运行抛出的异常,是否符合预期。

上述方法形参中的最后一个参数 msg,表示错误提示信息,可以不指定(有对应的重载方法)。

图33 断言的辅助方法
修改 cn/zjy/UserService.java 性别逻辑
    /**
     * 给定一个身份证号, 计算出该用户的性别
     * @param idCard 身份证号
     */
    public String getGender(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        //return Integer.parseInt(idCard.substring(16,17)) % 2 == 1 ? "男" : "女";
        return Integer.parseInt(idCard.substring(16,17)) % 2 == 0 ? "男" : "女";
    }
创建 cn/zjy/UserServiceTest.java 断言性别测试
    @Test
    public void testGenderWithAssert() {
        UserService userService = new UserService();
        String gender = userService.getGender("100000200010011011");
        //断言
        // Assertions.assertEquals("男", gender);
        Assertions.assertEquals("男", gender, "性别获取错误有问题");
    }
图34 性别断言测试验证
修改 cn/zjy/UserService.java 性别逻辑正确
    /**
     * 给定一个身份证号, 计算出该用户的性别
     * @param idCard 身份证号
     */
    public String getGender(String idCard){
        if (idCard == null || idCard.length() != 18) {
            throw new IllegalArgumentException("无效的身份证号码");
        }
        return Integer.parseInt(idCard.substring(16,17)) % 2 == 1 ? "男" : "女";
    }
创建 cn/zjy/UserServiceTest.java 断言性别测试2
    @Test
    public void testGenderWithAssert2() {
        UserService userService = new UserService();
        //断言
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getGender(null);
        });
    }
图35 断言异常捕获测试验证
图36 断言Null异常捕获测试验证
图37 断言小结

4.3 常见注解

在JUnit中还提供了一些注解,还增强其功能,常见的注解有以下几个:

注解 说明 备注
@Test 测试类中的方法用它修饰才能成为测试方法,才能启动执行 单元测试
@ParameterizedTest 参数化测试的注解 (可以让单个测试运行多次,每次运行时仅参数不同) 用了该注解,就不需要@Test注解了
@ValueSource 参数化测试的参数来源,赋予测试方法参数 与参数化测试注解配合使用
@DisplayName 指定测试类、测试方法显示的名称 (默认为类名、方法名)
@BeforeEach 用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。 初始化资源(准备工作)
@AfterEach 用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。 释放资源(清理工作)
@BeforeAll 用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。 初始化资源(准备工作)
@AfterAll 用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。 释放资源(清理工作)
图38 JUNIT常见注解
在 cn/zjy/UserServiceTest.java 添加@BeforeAll、@AfterAll、@BeforeEac、@AfterEach四种注解的方法
package cn.zjy;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.*;

public class UserServiceTest {

    @BeforeAll //在所有的单元测试方法运行之前,运行一次
    public static void beforeAll() {
        System.out.println("--- before All 所有的单元测试方法运行之前,运行一次 ---");
    }

    @AfterAll //在所有的单元测试方法运行之后,运行一次
    public static void afterAll() {
        System.out.println("--- after All 所有的单元测试方法运行之后,运行一次 ---");
    }

    @BeforeEach //在每一个单元测试方法运行之前,都会运行一次
    public void beforeEach() {
        System.out.println("\n   === before Each 每一个单元测试方法运行之前,都会运行一次");
    }

    @AfterEach  //在每一个单元测试方法运行之后,都会运行一次
    public void afterEach() {
        System.out.println("   === after Each. 每一个单元测试方法运行之后,都会运行一次");
    }

    @Test
    public void testGetAge() {
        Integer age = new UserService().getAge("110002200505091218");
        System.out.println(age);
    }

    @Test
    public void testGetGender() {
        UserService userService = new UserService();
        String gender = userService.getGender("100000200010011011");
        System.out.println(gender);
    }

    /**
     * 断言
     */
    @Test
    public void testGenderWithAssert() {
        UserService userService = new UserService();
        String gender = userService.getGender("100000200010011011");
        //断言
        // Assertions.assertEquals("男", gender);
        Assertions.assertEquals("男", gender, "性别获取错误有问题");
    }

    @Test
    public void testGenderWithAssert2() {
        UserService userService = new UserService();
        //断言
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getGender(null);
        });
    }

    @Test
    public void testGenderWithAssert3() {
        UserService userService = new UserService();
        //断言
        Assertions.assertThrows(NullPointerException.class, () -> {
            userService.getGender(null);
        });
    }
}
图39 JUNIT常见注解测试验证
参数化测试方法

在 cn/zjy/UserServiceTest.java 注释@BeforeAll、@AfterAll、@BeforeEac、@AfterEach方法添加参数化方法和@DisplayName

package cn.zjy;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

@DisplayName("用户信息测试类")
public class UserServiceTest {
    ...

    // 参数化测试
    @DisplayName("测试用户性别参数化测试")
    @ParameterizedTest
    @ValueSource(strings ={"100000200010011011","100000200010011031","100000200010011051"})
    public void testGetGender2(String idCard) {
        UserService userService = new UserService();
        String gender = userService.getGender(idCard);
        //断言
        Assertions.assertEquals("男", gender);
    }
图40 JUNIT参数化测试验证
图41 JUNIT常见注解小结
单元测试-企业开发规范

原则:编写测试方法时,要尽可能的覆盖业务方法中所有可能的情况(尤其是边界值)。

图42 单元测试-企业开发规范-尽可能覆盖所有可能
cn/zjy/UserService2Test.java
package cn.zjy;

import org.junit.jupiter.api.*;

@DisplayName("用户信息测试类")
public class UserService2Test {
    private UserService userService;

    @BeforeEach
    public void setUp() {
        userService = new UserService();
    }

    /**
     *测试获取性别-null
     */
    @Test
    @DisplayName("获取性别-null值")
    public void testGetGender1(){
        Assertions.assertThrows(IllegalArgumentException.class, ()-> {
            userService.getGender(null);
        });
    }

    /**
     *测试获取性别-''
     */

    @Test
    @DisplayName("获取性别-空串")
    public void testGetGender2() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getGender("");
        });
    }

    /**
     *测试获取性别-长度不足
     */
    @Test
    @DisplayName("获取性别-长度不足")
    public void testGetGender3(){
        Assertions.assertThrows(IllegalArgumentException.class, () ->{
            userService.getGender("110");
        });
    }

    /**
     *测试获取性别-超出长度
     */
     @Test
     @DisplayName("获取性别-长度超出")
     public void testGetGender4() {
         Assertions.assertThrows(IllegalArgumentException.class, () -> {
             userService.getGender("10000020001001101111Q0");
         });
     }

     /**
      * 测试获取性别-正常:男
     */
    @Test
    @DisplayName("获取性别-正常男性身份证")
    public void testGetGender5() {
        String gender = userService.getGender("100000200010011011");
        Assertions.assertEquals("男", gender);
    }

    /**
     * 测试获取性别-正常:女
     */
    @Test
    @DisplayName("获取性别-正常女性身份证")
    public void testGetGender6() {
        String gender = userService.getGender("100000200010011021");
        Assertions.assertEquals("女", gender);
    }

    // ------- 测试获取年龄 -----------
    /**
     测试获取年龄-正常
     */
    @Test
    @DisplayName("获取年龄-正常身份证")
    public void testGetAge() {
        Integer age = userService.getAge("100000200010011011");
        Assertions.assertEquals(24, age);
    }

    /**
     * 测试获取年龄-null值
     */
    @Test
    @DisplayName("获取年龄-null值")
    public void testGetAge2() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getAge(null);
        });
    }

    /**
     *测试获取年龄-长度超
     */
    @Test
    @DisplayName("获取年龄-长度超长")
    public void testGetAge3(){
        Assertions.assertThrows(IllegalArgumentException.class, () ->{
            userService.getAge("10000020001000200000020001");
        });
    }

    /**
     *测试获取年龄-长度不足
     */
     @Test
     @DisplayName("获取年龄-长度不足")
     public void testGetAge4(){
         Assertions.assertThrows(IllegalArgumentException.class, () ->{
            userService.getAge("100000200011");
         });
     }
}
图43 设置覆盖的包或类
图44 单元测试覆盖率
基于AI,测试UserService中的getGender方法
图45 安装通义插件
图46 生成测试代码
cn/zjy/UserServiceAiTest.java
package cn.zjy;
import org.junit.jupiter.api.*;

/**
 1. 被测函数分析
  - **功能**:根据18位身份证号码,提取第17位数字(从0开始计数为第16~17位),判断奇偶性,返回性别。
 - **输入参数**:
 - `idCard`: 长度必须为18的字符串,否则抛出 `IllegalArgumentException`
 - **输出结果**:
 - 若第17位是奇数 → 返回 `"男"`
 - 若第17位是偶数 → 返回 `"女"`

 2. 分支分析

 | 条件 | 分支路径 |
 |------|----------|
 | `idCard == null` | 抛出异常 |
 | `idCard.length() != 18` | 抛出异常 |
 | 第17位字符是奇数 | 返回"男" |
 | 第17位字符是偶数 | 返回"女" |

  3. 测试用例设计

 | 用例编号 | 输入值 | 预期行为 | 说明 |
 |----------|--------|-----------|------|
 | TC01 | null | 抛出异常 | 空指针检查 |
 | TC02 | "" | 抛出异常 | 空字符串 |
 | TC03 | "123456" | 抛出异常 | 长度不足 |
 | TC04 | "12345678901234567890" | 抛出异常 | 长度超出 |
 | TC05 | "100000200010011011" | 返回"男" | 第17位是1(奇数) |
 | TC06 | "100000200010011021" | 返回"女" | 第17位是2(偶数) |



 1. 被测函数分析:getAge(String idCard)
 该方法用于根据18位身份证号码计算用户年龄。其逻辑如下:
 输入参数:String idCard,必须为18位有效身份证号。
 异常处理:
 如果 idCard == null 或者长度不等于18,则抛出 IllegalArgumentException。
 核心逻辑:
 提取身份证中第7到14位(共8位)作为出生日期字符串(格式为 yyyyMMdd)。
 使用 LocalDate.parse() 解析该字符串为 LocalDate 类型。
 使用 Period.between() 计算当前日期与出生日期之间的年份差,返回年龄。
 2. 分支分析
  | 条件 | 是否满足条件 | 行为 |
  | idCard == null | 是 | 抛出异常 | 
  | idCard.length() != 18 | 是 | 抛出异常 | 
  | idCard.length() == 18 且格式正确 | 是 | 正常解析并返回年龄 | 
  | 出生日期非法(如2月30日) |  是 | 抛出 DateTimeParseException | 
 3. 测试用例设计
  | 测试编号 | 输入 | 预期行为 | 测试目的 | 
  | TC01 | null | 抛出异常 | 空值校验 | 
  | TC02 |  "" |  抛出异常 | 空串校验 | 
  | TC03 |  "123456" | 抛出异常 | 长度不足 | 
  | TC04 | "12345678901234567890" | 抛出异常 | 长度超出 | 
  | TC05 |  "100000200010011011" |  返回年龄24(假设当前是2024年) |  正常流程 | 
  | TC06 |  "100000200002291015" |  抛出异常 |  非法日期(闰年2月29日但非闰年) | 
*/


@DisplayName("用户测试类")
public class UserServiceAiTest {

    private UserService userService;

    @BeforeEach
    public void setUp() {
        // 初始化被测对象
        userService = new UserService();
    }

    /**
     * 测试输入为 null 的情况,应抛出 IllegalArgumentException 异常
     */
    @Test
    @DisplayName("获取性别 - 输入为 null")
    public void testGetGender_NullInput_ThrowsException() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getGender(null);
        });
    }

    /**
     * 测试输入为空字符串的情况,应抛出 IllegalArgumentException 异常
     */
    @Test
    @DisplayName("获取性别 - 输入为空字符串")
    public void testGetGender_EmptyString_ThrowsException() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getGender("");
        });
    }

    /**
     * 测试输入长度不足的情况,应抛出 IllegalArgumentException 异常
     */
    @Test
    @DisplayName("获取性别 - 身份证长度不足")
    public void testGetGender_LengthLessThan18_ThrowsException() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getGender("123456");
        });
    }

    /**
     * 测试输入长度超过18的情况,应抛出 IllegalArgumentException 异常
     */
    @Test
    @DisplayName("获取性别 - 身份证长度超过18")
    public void testGetGender_LengthMoreThan18_ThrowsException() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getGender("12345678901234567890");
        });
    }

    /**
     * 测试正常输入且第17位为奇数的情况,应返回"男"
     */
    @Test
    @DisplayName("获取性别 - 正常输入,第17位为奇数")
    public void testGetGender_ValidInput_OddDigit_ReturnsMale() {
        String gender = userService.getGender("100000200010011011");
        Assertions.assertEquals("男", gender);
    }

    /**
     * 测试正常输入且第17位为偶数的情况,应返回"女"
     */
    @Test
    @DisplayName("获取性别 - 正常输入,第17位为偶数")
    public void testGetGender_ValidInput_EvenDigit_ReturnsFemale() {
        String gender = userService.getGender("100000200010011021");
        Assertions.assertEquals("女", gender);
    }


    /**
     * 测试获取年龄 - 正常流程(2000年10月1日出生)
     */
    @Test
    @DisplayName("获取年龄-正常身份证")
    public void testGetAge_Normal() {
        Integer age = userService.getAge("100000200010011011");
        Assertions.assertEquals(24, age); // 2025 - 2000 = 25岁,但生日未到,所以是24岁
    }

    /**
     * 测试获取年龄 - null值
     */
    @Test
    @DisplayName("获取年龄-null值")
    public void testGetAge_Null() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getAge(null);
        });
    }

    /**
     * 测试获取年龄 - 空字符串
     */
    @Test
    @DisplayName("获取年龄-空串")
    public void testGetAge_EmptyString() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getAge("");
        });
    }

    /**
     * 测试获取年龄 - 长度不足
     */
    @Test
    @DisplayName("获取年龄-长度不足")
    public void testGetAge_LengthLessThan18() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getAge("123456");
        });
    }

    /**
     * 测试获取年龄 - 长度超过18
     */
    @Test
    @DisplayName("获取年龄-长度超出")
    public void testGetAge_LengthMoreThan18() {
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            userService.getAge("12345678901234567890");
        });
    }

    /**
     * 测试获取年龄 - 非法日期(如2000年2月29日但不是闰年)
     */
    @Test
    @DisplayName("获取年龄-非法出生日期")
    public void testGetAge_IllegalBirthday() {
        Assertions.assertThrows(Exception.class, () -> {
            userService.getAge("100000190002291015"); // 1900年不是闰年
        });
    }
}
图47 Ai单元测试覆盖率

4.4 依赖范围

图48 思考:测试代码写入Main中
scope值 主程序 测试程序 打包(运行) 范例
compile(默认) Y Y Y log4j
test - Y - junit
provided Y Y - servlet-api
runtime - Y Y jdbc驱动
图49 依赖范围
图50 依赖范围小结

4.5 Maven常见问题

图51 Maven常见问题

①②③④⑤⑥⑦⑧⑨⑩


返回