Java高级进阶 | 单元测试JUnit + 反射机制实战详解

适合人群 :Java基础已掌握,开始接触测试框架与反射机制的进阶学习者

涉及知识点 :JUnit单元测试、Assert断言、反射核心API、Class对象、Field字段操作、setAccessible权限突破**

阅读时长**:约15分钟


目录


一、单元测试:让代码自己证明自己是对的

1.1 什么是单元测试

单元测试(Unit Testing)是指对软件中的最小可测试单元 进行检查和验证。在Java中,最小单元通常是一个方法

对比项 传统 main 方法测试 JUnit 单元测试
测试方式 手动 System.out.println 看输出 自动断言 Assert.assertEquals()
结果判断 人眼比对 程序自动判定通过/失败
测试隔离 所有逻辑挤在 main 里 每个方法独立 @Test
批量执行 不支持 一键运行全部测试
CI/CD集成 不支持 完美对接持续集成

核心思想:测试代码和生产代码分离,用断言(Assert)自动验证结果,而非人工肉眼检查。

1.2 StringUtil 工具类实现

先编写被测试的目标类------一个字符串工具类,包含两个静态方法:

复制代码
package org.example;

/**
 * 字符串工具类
 */
public class StringUtil {

    /**
     * 打印字符串长度
     * 如果参数为null,给出提示并返回
     */
    public static void printNumber(String name) {
        if (name == null) {
            System.out.println("参数长度不能为null");
            return;
        }
        System.out.println("名字长度是:" + name.length());
    }

    /**
     * 求字符串的最大索引
     * null返回-1,空字符串返回-1,正常字符串返回 length-1
     */
    public static int getMaxIndex(String data) {
        if (data == null) {
            return -1;
        }
        return data.length() - 1;
    }
}

设计要点

方法 功能 null处理 返回值
printNumber(String) 打印字符串长度 检测null,打印提示后return void
getMaxIndex(String) 获取最大索引 检测null,返回-1 int

注意getMaxIndex 对空字符串 "" 返回 -1(因为 0 - 1 = -1),这是一个重要的边界情况,测试时必须覆盖。

1.3 JUnit测试类编写

引入JUnit依赖后,编写独立的测试类:

复制代码
package org.example;

import org.junit.Assert;
import org.junit.Test;

public class StringUtilTest {

    @Test
    public void testPrintNumber() {
        // 测试三种场景:null、空字符串、正常字符串
        StringUtil.printNumber(null);     // 预期:打印"参数长度不能为null"
        StringUtil.printNumber("");       // 预期:打印"名字长度是:0"
        StringUtil.printNumber("admin");  // 预期:打印"名字长度是:5"
    }

    @Test
    public void testGetMaxIndex() {
        // 场景1:null → 预期返回 -1
        int i1 = StringUtil.getMaxIndex(null);
        Assert.assertEquals("null测试失败!", -1, i1);

        // 场景2:空字符串 → 预期返回 -1
        int i2 = StringUtil.getMaxIndex("");
        Assert.assertEquals("空字符串测试失败!", -1, i2);

        // 场景3:正常字符串 "admin" → 预期返回 4
        int i3 = StringUtil.getMaxIndex("admin");
        Assert.assertEquals("admin测试失败!", 4, i3);

        System.out.println("测试通过!");
    }
}

JUnit核心注解与API一览

注解/API 作用 使用位置
@Test 标记一个测试方法 方法上方
@Before 每个测试方法执行前运行 初始化资源
@After 每个测试方法执行后运行 清理资源
Assert.assertEquals(msg, expected, actual) 断言期望值与实际值相等 方法体内
Assert.assertTrue(condition) 断言条件为true 方法体内
Assert.assertNull(object) 断言对象为null 方法体内

断言三要素消息(失败时显示)+ 期望值 + 实际值。当 expected != actual 时,测试失败并抛出AssertionError。

1.4 测试结果与要点分析

运行结果

复制代码
参数长度不能为null
名字长度是:0
名字长度是:5
测试通过!

测试覆盖分析

测试场景 输入值 期望结果 实际结果 是否通过
null输入 null 打印提示 / 返回-1 打印提示 / 返回-1 通过
空字符串 "" 长度0 / 返回-1 长度0 / 返回-1 通过
正常字符串 "admin" 长度5 / 返回4 长度5 / 返回4 通过

测试原则 :每个方法至少覆盖 正常值、边界值、异常值 三种场景。本例中 null 是异常值,空字符串是边界值,"admin"是正常值,覆盖完整。


二、反射机制:运行时透视对象的灵魂

2.1 什么是反射

反射(Reflection)是Java在运行期间动态获取类信息、访问和修改对象字段、调用方法的机制。

没有反射时

复制代码
Student s = new Student("杳杉", 45, '男', 180, "篮球,冰球");
// 编译期就知道有哪些字段,直接点出来
s.name;  // 编译期确定

有反射时

复制代码
Object obj = new Student("杳杉", 45, '男', 180, "篮球,冰球");
Class c = obj.getClass();            // 运行时获取Class对象
Field[] fields = c.getDeclaredFields(); // 运行时获取所有字段
// 不需要提前知道对象类型,就能遍历所有字段

一句话理解:反射就是在程序运行期间,"透视"任意对象内部结构的能力。

反射核心API速查表

API 作用 返回类型
obj.getClass() 获取运行时Class对象 Class<?>
c.getSimpleName() 获取类名(不含包名) String
c.getDeclaredFields() 获取所有声明字段(含private) Field[]
field.getName() 获取字段名 String
field.setAccessible(true) 突破private访问限制 void
field.get(obj) 获取obj对象中该字段的值 Object

2.2 ObjectFrame2 框架设计思路

需求:把任意对象的字段名和值保存到文件中,不关心对象是什么类型。

设计思路

复制代码
输入:任意Java对象(Student / Teacher / 任何类)
     ↓
第一步:getClass() 获取Class对象
     ↓
第二步:getSimpleName() 获取类名(作为文件中的分隔标记)
     ↓
第三步:getDeclaredFields() 获取所有字段
     ↓
第四步:遍历字段,setAccessible(true) 突破权限
     ↓
第五步:field.get(obj) 取出每个字段的值
     ↓
输出:写入文件 格式为 字段名=值

实体类定义(Student 和 Teacher):

复制代码
// Student.java
package org.example;

public class Student {
    public String name;
    public int age;
    public char sex;
    public double height;
    public String hobbies;

    public Student(String name, int age, char sex, double height, String hobbies) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.height = height;
        this.hobbies = hobbies;
    }
}

// Teacher.java
package org.example;

public class Teacher {
    public String name;
    public double salary;

    public Teacher(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
}

2.3 核心代码实现

复制代码
package org.example;

import java.io.FileWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;

public class ObjectFrame2 {

    /**
     * 把任意对象的字段名和值保存到文件
     * @param obj 任意Java对象
     */
    public static void saveObject(Object obj) throws Exception {
        // 1. 创建打印流,追加模式写入文件
        PrintWriter pw = new PrintWriter(
            new FileWriter("C:\\Users\\xxx\\Desktop\\day.txt", true)
        );

        // 2. 通过反射获取Class对象
        Class c = obj.getClass();
        String className = c.getSimpleName(); // 获取类名(不含包路径)
        pw.println("===========================" + className + "==========================");

        // 3. 获取Class对象中声明的所有字段(含private)
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            String name = field.getName();       // 字段名
            field.setAccessible(true);            // 突破private访问限制
            String value = field.get(obj) + "";   // 字段值(转为字符串)
            pw.println(name + "=" + value);
        }

        pw.close();
    }
}

逐行解析关键代码

代码行 作用 为什么需要
Class c = obj.getClass() 获取运行时类对象 反射入口,所有操作始于Class
c.getSimpleName() 获取类名 只取类名不含包名,用于文件标记
c.getDeclaredFields() 获取所有字段 含private字段,getFields()不含private
field.setAccessible(true) 取消访问检查 private字段默认不可外部访问
field.get(obj) 获取字段值 从obj对象中取出该字段的运行时值

getDeclaredFields() vs getFields()

  • getDeclaredFields():获取本类声明的所有字段(含private,不含继承的)
  • getFields():获取所有public字段(含继承的)

2.4 测试类与运行结果

复制代码
package org.example;

public class TestFrame {

    public static void main(String[] args) throws Exception {
        // 创建两个不同类型的对象
        Student s1 = new Student("杳杉", 45, '男', 180, "篮球,冰球");
        Teacher t1 = new Teacher("波妞", 999.0);

        // 需求:把任意对象的字段名和其对应的值保存到文件中
        ObjectFrame2.saveObject(s1);
        ObjectFrame2.saveObject(t1);
    }
}

运行后 day.txt 文件内容

复制代码
===========================Student===========================
name=杳杉
age=45
sex=男
height=180.0
hobbies=篮球,冰球
===========================Teacher===========================
name=波妞
salary=999.0

关键观察saveObject() 方法参数是 Object 类型,方法内部通过反射动态获取字段------无论传入Student还是Teacher,都能自动识别并保存。这就是反射的威力:一份代码,适配任意对象


三、知识点串联与总结

3.1 两大知识点对照

维度 单元测试 JUnit 反射 Reflection
解决什么问题 验证代码正确性 运行时动态操作类和对象
核心思想 断言自动化验证 运行时透视对象结构
关键API @Test Assert.assertEquals() getClass() getDeclaredFields() field.get()
适用场景 方法级测试、回归测试 框架设计、ORM映射、序列化
工程价值 保障代码质量 灵活性与通用性

3.2 反射调用链路图

复制代码
对象 obj
  │
  ├── getClass() ──→ Class对象
  │                     │
  │                     ├── getSimpleName() ──→ 类名字符串
  │                     │
  │                     └── getDeclaredFields() ──→ Field[] 数组
  │                                                     │
  │                                                     ├── getName()      → 字段名
  │                                                     ├── setAccessible() → 突破权限
  │                                                     └── get(obj)       → 字段值
  │
  └── 写入文件:字段名=字段值

3.3 编程思维总结

思维要点 体现
测试先行意识 写完功能立刻写测试,不依赖main方法肉眼验证
边界值思维 null、空字符串、正常值三种场景全覆盖
动态思维 反射打破"编译期必须知道类型"的限制,运行时动态处理
通用框架思维 saveObject(Object obj) 用Object接收任意类型,反射动态解析
权限突破意识 setAccessible(true) 是反射操作private字段的关键一步

进阶方向:反射是Spring IoC、MyBatis ORM、Jackson JSON序列化等主流框架的底层基石。掌握反射,就拿到了理解这些框架源码的钥匙。


如果本文对你有帮助,欢迎点赞收藏! 有问题欢迎评论区交流,我会一一回复。

相关推荐
qq_366086221 小时前
关于接口路径中中文值解码问题
java·开发语言·数据库
大白菜和MySQL1 小时前
elk部署和kibana图形化展示
java·笔记·elasticsearch
Zane19941 小时前
JMM 与 happens-before:一次搞懂 Java 内存模型
java·后端
An_s1 小时前
Java Spring Boot+vue3文件管理系统
java·开发语言
wuqingshun3141591 小时前
接口和抽象类有什么区别?
java
SimonKing2 小时前
别再写 setter 了!MapStruct Plus vs MapStruct,谁才是 Bean 转换的真神?
java·后端·程序员
倒流时光三十年2 小时前
第一阶段 05 · Java 客户端查询类详解(Query / SearchCriteria / Response 与复杂拼接)
java·开发语言·python
rannn_1112 小时前
【力扣hot100】哈希表专题——从两数之和到最长连续序列
java·算法·leetcode·哈希
观远数据2 小时前
云原生BI的战略价值:不是技术选择,而是业务弹性的决定因素
java·人工智能·云原生