## Day03 Maven基础(核心)

---

##### 什么是Maven

* Maven是一款用于管理和构建Java项目的工具，是apache旗下的一个开源项目。

*  Apache 软件基金会，成立于1999年7月，是目前世界上最大的最受欢迎的开源软件基金会，也是一个专门为支持开源项目而生的非盈利性组织。

  *  开源项目：https://www.apache.org/index.html#projects-list
  
    

  <figure><img src="/AiJavaWeb/imgs/jwai03-01.png"><figcaption>图1 Web标准组成</figcaption></figure>

##### Maven的作用

<figure><img src="/AiJavaWeb/imgs/jwai03-02.png"><figcaption>图2 Maven的作用</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-03.png"><figcaption>图3 依赖管理</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-04.png"><figcaption>图4 项目构建</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-05.png"><figcaption>图5 统一项目结构</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-06.png"><figcaption>图6 Maven市场占有率</figcaption></figure>

* Maven核心
  * Maven概述
  * IDEA集成Maven
  * 依赖管理
  * 单元测试
* Maven进阶
  * 分模块设计
  * 继承
  * 聚合
  * 私服

### 1. Maven概述

#### 1.1 介绍

* ApacheMaven是一个项目管理和构建工具，它基于项目对象模型（POM project object model)的概念，通过一小段描述信息(pom.xml)来管理项目的构建。
* 作用：
  * 方便的依赖管理
  * 标准的项目构建流程
  * 统一的项目结构
* 官网:http://maven.apache.org/
* 仓库：用于存储资源，管理各种jar包。
  * 本地仓库：自己计算机上的一个目录。
  * 中央仓库：由Maven团队维护的全球唯一的。仓库地址：https：//repo1.maven.org/maven2/
  * 远程仓库（私服）：一般由公司团队搭建的私有仓库。

<figure><img src="/AiJavaWeb/imgs/jwai03-07.png"><figcaption>图7 Maven原理及其仓库</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-08.png"><figcaption>图8 Maven小结</figcaption></figure>





#### 1.2 安装

* 下载 apache-maven-3.9.4-bin.zip 解压到D:\develop\apache-maven-3.9.4\

  ```tex
  清华镜像：https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.9.11/binaries/apache-maven-3.9.11-bin.zip
  ```

  安装目录新建mvn_repo，配置本地仓库：修改 conf/settings.xml 中的 ```<localRepository>``` 为一个指定目录。

  ```xml
  <localRepository>D:\develop\apache-maven-3.9.4\mvn_repo</localRepository>
  ```

* 配置阿里云私服：修改 conf/settings.xml 中的``` <mirrors>``` 标签，为其添加如下子标签：

  ```xml
  <mirror>  
     <id>alimaven</id>  
     <name>aliyun maven</name>  
     <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
     <mirrorOf>central</mirrorOf>          
  </mirror>
  ```

* 配置环境变量: MAVEN_HOME 为maven的解压目录，并将其bin目录加入PATH环境变量。

* cmd下 mvn -v

<figure><img src="/AiJavaWeb/imgs/jwai03-09.png"><figcaption>图9 <mark>Maven安装测试</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-10.png"><figcaption>图10 Maven安装小结</figcaption></figure>

### 2. IDEA集成Maven

#### 2.1 IDEA配置Maven环境(全局)

<figure><img src="/AiJavaWeb/imgs/jwai03-11.png"><figcaption>图11 IDEA配置Maven环境(全局)</figcaption></figure>

<mark> 全局配置需关闭Idea重启</mark>

#### 2.2 创建Maven项目

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

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

③ 创建Maven模块 maven-project01

④ 创建 Helloworld.java并运行

<figure><img src="/AiJavaWeb/imgs/jwai03-12.png"><figcaption>图12 创建web-ai-project01和maven-project01</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-13.png"><figcaption>图13 <mark>创建web-ai-project01、maven-project01并运行</mark></figcaption></figure>

#### 2.3 Maven坐标

什么是坐标？

* Maven 中的坐标是<mark>资源(jar)的唯一标识，通过该坐标可以唯一定位资源位置</mark>。
* 使用坐标来<mark>定义本项目</mark>或引入<mark>项目中需要的依赖</mark>。

<figure><img src="/AiJavaWeb/imgs/jwai03-14.png"><figcaption>图14 Maven坐标</figcaption></figure>

Maven 坐标主要组成

* groupId：定义当前Maven项目隶属组织名称（通常是域名反写，例如：com.itheima）
* artifactId：定义当前Maven项目名称（通常是模块名称，例如 order-service、goods-service）
* version：定义当前项目版本号
  * SNAPSHOT: 功能不稳定、尚处于开发中的版本，即快照版本
  * RELEASE: 功能趋于稳定、当前更新停止，可以用于发行的版本
  * 默认(没有SNAPSHOT、RELEASE)：也是稳定、发行的版本

<figure><img src="/AiJavaWeb/imgs/jwai03-15.png"><figcaption>图15 Maven坐标小结</figcaption></figure>



#### 2.4 导入Maven项目

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

* 方式一：File -> Project Structure -> Modules -> Import Module -> 选择maven项目的pom.xml。

* 方式二：Maven面板 -> +（Add Maven Projects） -> 选择maven项目的pom.xml。

<figure><img src="/AiJavaWeb/imgs/jwai03-16.png"><figcaption>图16 <mark>Maven项目导入验证</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-17.png"><figcaption>图17 Maven项目导入小结</figcaption></figure>

### 3. 依赖管理

#### 3.1 依赖配置

* 依赖：指当前项目运行所需要的jar包，一个项目中可以引入多个依赖。

* 配置：

  * ①在 pom.xml 中编写 ```<dependencies>``` 标签
  * ②在``` <dependencies> ```标签中 使用 ```<dependency> ```引入坐标
  * ③定义坐标的 groupId，artifactId，version
  * ④点击刷新按钮，引入最新加入的坐标

  

  * 如果不知道依赖的坐标信息，可以到 https://mvnrepository.com/ 中搜索。
  * 排除依赖：指主动断开依赖的资源，被排除的资源<mark>无需指定版本</mark>。

<figure><img src="/AiJavaWeb/imgs/jwai03-18.png"><figcaption>图18 排除依赖</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-19.png"><figcaption>图19 <mark>添加依赖和排除依赖验证</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-20.png"><figcaption>图20 小结</figcaption></figure>

#### 3.2 生命周期

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

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

* clean：清理工作。
* default：核心工作，如：编译、测试、打包、安装、部署等。
* site：生成报告、发布站点等。

<mark>每套生命周期包含一些阶段（phase），阶段是有顺序的，后面的阶段依赖于前面的阶段。</mark>

<figure><img src="/AiJavaWeb/imgs/jwai03-21.png"><figcaption>图21 3套相互独立的生命周期</figcaption></figure>



##### 五个主要生命周期阶段：

* clean：移除上一次构建生成的文件
* compile：编译项目源代码
* test：使用合适的单元测试框架运行测试(junit)
* package：将编译后的文件打包，如：jar、war等
* install：安装项目到本地仓库

<mark>在同一套生命周期中，当运行后面的阶段时，前面的阶段都会运行。</mark>

<figure><img src="/AiJavaWeb/imgs/jwai03-22.png"><figcaption>图22 5个主要生命周期阶段</figcaption></figure>

##### 执行指定生命周期的两种方式：

* 在idea中，右侧的maven工具栏，选中对应的生命周期，双击执行。
* 在命令行中，通过命令执行。



<figure><img src="/AiJavaWeb/imgs/jwai03-23.png"><figcaption>图23 <mark>编译、打包、安装本地仓库验证</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-24.png"><figcaption>图24 Maven生命周期阶段与插件</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-25.png"><figcaption>图25 Maven生命周期阶段小结</figcaption></figure>

### 4. 单元测试

##### 测试概述

* 测试：是一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程。
* 阶段划分：单元测试、集成测试、系统测试、验收测试。
* 测试方法：白盒测试、黑盒测试 及 灰盒测试。



|      | 单元测试                                     | 集成测试                                                     | 系统测试                                       | 验收测试                                             |
| ---- | -------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------- | ---------------------------------------------------- |
| 介绍 | 对软件的基本组成单位进行测试，最小测试单位。 | 将已分别通过测试的单元，按设计要求组合成系统或子系统，再进行的测试 | 对已经集成好的软件系统进行彻底的测试。         | 交付测试，是针对用户需求、业务流程进行的正式的测试。 |
| 目的 | 检验软件基本组成单位的正确性。               | 检查单元之间的协作是否正确。                                 | 验证软件系统的正确性、性能是否满足指定的要求。 | 验证软件系统是否满足验收标准。                       |
| 人员 | 开发人员                                     | 开发人员                                                     | 测试人员                                       | 客户/需求方                                          |



<figure><img src="/AiJavaWeb/imgs/jwai03-26.png"><figcaption>图26 测试阶段划分</figcaption></figure>



| 方法 | 描述                                                         |
| ---- | ------------------------------------------------------------ |
| 白盒 | 清楚软件内部结构、代码逻辑。用于验证代码、逻辑正确性。       |
| 黑盒 | 不清楚软件内部结构、代码逻辑。用于验证软件的功能、兼容性等方面。 |
| 灰盒 | 结合了白盒测试和黑盒测试的特点，既关注软件的内部结构又考虑外部表现（功能）。 |

<figure><img src="/AiJavaWeb/imgs/jwai03-27.png"><figcaption>图27 测试方法划分</figcaption></figure>

#### 4.1 快速入门

* 单元测试：就是针对最小的功能单元(方法)，编写测试代码对其正确性进行测试。
* JUnit：最流行的Java测试框架之一，提供了一些功能，方便程序进行单元测试（第三方公司提供）。
* main方法测试：
  * ①测试代码与源代码未分开，难维护
  * ②一个方法测试失败，影响后面方法
  * ③无法自动化测试，得到测试报告
* JUnit单元测试
  * ①测试代码与源代码分开，便于维护
  * ②可根据需要进行自动化测试
  * ③可自动分析测试结果，产出测试报告



<figure><img src="/AiJavaWeb/imgs/jwai03-28.png"><figcaption>图28 Main单元测试方法与JUnit测试方法对比</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-29.png"><figcaption>图29 图示main测试方法</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-30.png"><figcaption>图30 图示Junit测试方法</figcaption></figure>



##### 【案例】使用JUnit，对UserService中业务方法进行单元测试。

* ①在pom.xml中，引入JUnit的依赖。
* ②在main/java目录下，创建业务类 cn/dzj/UserService.java
* ③在test/java目录下，创建测试类cn/dzj/UserServiceTest.java，并编写对应的测试方法，并在方法上声明@Test注解。
* ④运行单元测试 (测试通过：绿色；测试失败：红色)。

  pom.xml

```xml
<dependency>  
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.1</version>
</dependency>
```

cn/dzj/UserService.java

```java
package cn.dzj;

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/dzj/UserServiceTest.java

```java
package cn.dzj;
import org.junit.jupiter.api.Test;
public class UserServiceTest {
    @Test
    public void testGetAge(){
        Integer age = new UserService().getAge("110002200505091218");
        System.out.println(age);
    }
}

```

<figure><img src="/AiJavaWeb/imgs/jwai03-31.png"><figcaption>图31 <mark>测试案例完成验证</mark></figcaption></figure></mark>



<figure><img src="/AiJavaWeb/imgs/jwai03-32.png"><figcaption>图32 Junit测试小结</figcaption></figure>



#### 4.2 断言

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

| **断言方法**                                                 | **描述**                                 |
| ------------------------------------------------------------ | ---------------------------------------- |
| 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，表示错误提示信息，可以不指定（有对应的重载方法）。

<figure><img src="/AiJavaWeb/imgs/jwai03-33.png"><figcaption>图33 断言的辅助方法</figcaption></figure>

##### 修改 cn/dzj/UserService.java 性别逻辑

```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/dzj/UserServiceTest.java 断言性别测试

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



<figure><img src="/AiJavaWeb/imgs/jwai03-34.png"><figcaption>图34 <mark>性别断言测试验证</mark></figcaption></figure>

##### 修改 cn/dzj/UserService.java 性别逻辑正确

```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/dzj/UserServiceTest.java 断言性别测试2

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



<figure><img src="/AiJavaWeb/imgs/jwai03-35.png"><figcaption>图35 <mark>断言异常捕获测试验证</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-36.png"><figcaption>图36 <mark>断言Null异常捕获测试验证</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-37.png"><figcaption>图37 断言小结</figcaption></figure>



#### 4.3 常见注解

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

| **注解**           | **说明**                                                     | **备注**                        |
| ------------------ | ------------------------------------------------------------ | ------------------------------- |
| @Test              | 测试类中的方法用它修饰才能成为测试方法，才能启动执行         | 单元测试                        |
| @ParameterizedTest | 参数化测试的注解 (可以让单个测试运行多次，每次运行时仅参数不同) | 用了该注解，就不需要@Test注解了 |
| @ValueSource       | 参数化测试的参数来源，赋予测试方法参数                       | 与参数化测试注解配合使用        |
| @DisplayName       | 指定测试类、测试方法显示的名称 （默认为类名、方法名）        |                                 |
| @BeforeEach        | 用来修饰一个实例方法，该方法会在每一个测试方法执行之前执行一次。 | 初始化资源(准备工作)            |
| @AfterEach         | 用来修饰一个实例方法，该方法会在每一个测试方法执行之后执行一次。 | 释放资源(清理工作)              |
| @BeforeAll         | 用来修饰一个静态方法，该方法会在所有测试方法之前只执行一次。 | 初始化资源(准备工作)            |
| @AfterAll          | 用来修饰一个静态方法，该方法会在所有测试方法之后只执行一次。 | 释放资源(清理工作)              |

<figure><img src="/AiJavaWeb/imgs/jwai03-38.png"><figcaption>图38 JUNIT常见注解</figcaption></figure>

##### 在 cn/dzj/UserServiceTest.java 添加@BeforeAll、@AfterAll、@BeforeEac、@AfterEach四种注解的方法

```java
package cn.dzj;

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);
        });
    }
}

```



<figure><img src="/AiJavaWeb/imgs/jwai03-39.png"><figcaption>图39 <mark>JUNIT常见注解测试验证</mark></figcaption></figure>

##### 参数化测试方法

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

```java
package cn.dzj;

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);
    }
```

<figure><img src="/AiJavaWeb/imgs/jwai03-40.png"><figcaption>图40 <mark>JUNIT参数化测试验证</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-41.png"><figcaption>图41 JUNIT常见注解小结</figcaption></figure>

##### 单元测试-企业开发规范

原则：编写测试方法时，要尽可能的覆盖业务方法中所有可能的情况（尤其是边界值）。

<figure><img src="/AiJavaWeb/imgs/jwai03-42.png"><figcaption>图42 单元测试-企业开发规范-尽可能覆盖所有可能</figcaption></figure>

##### cn/dzj/UserService2Test.java

```java
package cn.dzj;

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");
         });
     }
}
```





<figure><img src="/AiJavaWeb/imgs/jwai03-43.png"><figcaption>图43 设置覆盖的包或类</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai03-44.png"><figcaption>图44 <mark>单元测试覆盖率</mark></figcaption></figure>

##### 基于AI,测试UserService中的getGender方法

<figure><img src="/AiJavaWeb/imgs/jwai03-45.png"><figcaption>图45 安装通义插件</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-46.png"><figcaption>图46 生成测试代码</figcaption></figure>

##### cn/dzj/UserServiceAiTest.java

```java
package cn.dzj;
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年不是闰年
        });
    }
}
```

<figure><img src="/AiJavaWeb/imgs/jwai03-47.png"><figcaption>图47 <mark>Ai单元测试覆盖率</mark></figcaption></figure>



#### 4.4 依赖范围

<figure><img src="/AiJavaWeb/imgs/jwai03-48.png"><figcaption>图48 思考：测试代码写入Main中</figcaption></figure>


* 依赖的jar包，默认情况下，可以在任何地方使用。可以通过``` <scope>…</scope> ```设置其作用范围。
* 作用范围：
  * 主程序范围有效。（main文件夹范围内）
  * 测试程序范围有效。（test文件夹范围内）
  * 是否参与打包运行。（package指令范围内）

  

| scope值         | 主程序 | 测试程序 | 打包（运行） | 范例        |
| --------------- | ------ | -------- | ------------ | ----------- |
| compile（默认） | Y      | Y        | Y            | log4j       |
| test            | -      | Y        | -            | junit       |
| provided        | Y      | Y        | -            | servlet-api |
| runtime         | -      | Y        | Y            | jdbc驱动    |

<figure><img src="/AiJavaWeb/imgs/jwai03-49.png"><figcaption>图49 依赖范围</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai03-50.png"><figcaption>图50 依赖范围小结</figcaption></figure>



#### 4.5 Maven常见问题

* 产生原因：由于网络原因，依赖没有下载完整导致的，在maven仓库中生成了xxx.lastUpdated文件，该文件不删除，不会再重新下载。
* 解决方案：
  * 根据maven依赖的坐标，找到仓库中对应的 <mark>xxx.lastUpdated</mark> 文件，删除，删除之后重新加载项目即可。
  * 通过命令 (del /s *.lastUpdated) 批量递归删除指定目录下的 xxx.lastUpdated 文件，删除之后重新加载项目即可。
* <mark>重新加载依赖，依赖下载了之后，maven面板可能还会报红，此时可以关闭IDEA，重新打开IDEA加载此项目即可</mark>。



<figure><img src="/AiJavaWeb/imgs/jwai03-51.png"><figcaption>图51 Maven常见问题</figcaption></figure>

##### 



①②③④⑤⑥⑦⑧⑨⑩