从 JUnit 深入理解 Java 注解与反射机制

参考资料:

  1. 编写JUnit测试
  2. 详解介绍JUnit单元测试框架(完整版)
  3. deepseek
  4. 封面来自 qwen-image
  5. 个人项目 github 项目地址

overview

  • 本文会涉及:
    • 什么是 JUnit
    • JUnit 特性简介
    • JUnit 如何使用到了 Java 的反射机制注解
    • 自己实现一个极简版的 MyJUnit
  • 本文不深入讨论:
    • JUnit 测试用例具体的编写方法与实践建议
    • Java 的反射机制是如何实现的

什么是 JUnit

  • JUnit 是一种主流的 Java 单元测试框架, 在理解 JUnit 之前, 我们要先了解什么是"单元测试", 什么是"单元测试框架", 然后才可以理解什么是 JUnit
  • 单元测试:
    就是针对最小的功能单元编写测试代码. Java 程序的最小功能单元是方法, 所以, 对 Java 程序进行单元测试就是针对单个 Java 方法进行的测试
  • 测试驱动开发:
    TDD, Test Driven Develop, 即测试驱动开发, 也是我们常说的 测试先行. TDD 的优势很多, 包括但不限于:
    1. 可以促进开发者对需求进行初步检验
    2. 可以促进开发者对设计进行初步检验
    3. 可以促进开发者提前构思代码, 有助于写出高质量代码
  • JUnit:
    JUnit 是一个开源的Java语言的单元测试框架, 专门针对 Java 语言设计, 使用最广泛. JUnit 是事实上的单元测试的标准框架, 任何 Java 开发者都应当学习并使用 JUnit 编写单元测试.

JUnit 特性简介

  1. 注解驱动(Annotation-driven):
    这是 JUnit4 和 5 的核心. 通过注解来配置测试行为, 使得代码非常声明式, 清晰易懂
    • @Test: 可以标记一个方法为测试方法
    • @BeforeEach (JUnit 5) / @Before (JUnit 4):在每个测试方法之前执行。用于初始化公共资源(如创建对象、连接数据库)。这体现了设置/拆除(Setup/Teardown) 模式。
    • @AfterEach (JUnit 5) / @After (JUnit 4):在每个测试方法之后执行。用于清理资源(如关闭连接、删除文件)。
    • @BeforeAll (JUnit 5) / @BeforeClass (JUnit 4):在所有测试方法执行之前执行一次(方法必须是 static)。适用于昂贵且可共享的初始化,如启动 Docker 容器。
    • @AfterAll (JUnit 5) / @AfterClass (JUnit 4):在所有测试方法执行之后执行一次(方法必须是 static)。
    • @Disabled (JUnit 5) / @Ignore (JUnit 4):忽略该测试方法,不执行。
  2. 断言(Assertions):
    是测试的"灵魂", 用于验证代码的行为是否符合预期. 断言失败意味着测试失败
    • assertEquals(expected, actual)
    • assertTrue(condition)
    • assertNull(object)
  3. 异常测试:
    • JUnit 4:使用 @Test(expected = Exception.class)
    • JUnit 5:使用更强大的 assertThrows()
  4. 参数化测试

当写下注解@Test的时候, 实际上发生了什么?

  • 我们下面的讲解都会基于下面这一个简单的例子, 从 @Test 这个注解开始逐步切入
    // 一个测试类
    class TestClass{// 一个测试用例@Testvoid testAdd1(){assertEquals(4, 2 + 2);}
    }
    

注解意味着什么? 注解在每个阶段的作用?

注解的基本概念

注解实际上是给代码贴的"标签"或"元数据", 他们本身不包含业务逻辑, 但可以被其他程序读取并采取相应行动.

注解的生命周期:从源码到字节码

接下来, 我们要进一步理解@Test在 Java ‘编译+运行’ 两个阶段发挥的作用

  • Java 注解在两个阶段发挥作用:
    • 编译阶段:注解信息被写入字节码文件
    • 运行阶段:通过反射机制读取并处理注解
      Java源码→注解标记进字节码→字节码(.class文件)→通过反射机制识别注解并运行测试用例→执行测试用例Java源码 \rightarrow^{注解标记进字节码}\rightarrow 字节码(.class文件) \rightarrow^{通过反射机制识别注解并运行测试用例}\rightarrow 执行测试用例 Java源码注解标记进字节码字节码(.class文件)通过反射机制识别注解并运行测试用例执行测试用例
注解真的进入字节码了吗?
  • 最简单的办法就是深入 .class 看一看,通过反编译 .class 文件可以验证注解确实被保留在字节码中:
    下面我们借用实现好的 MyJUnit 小项目, 然后看看字节码有没有额外信息
# 编译为字节码 / 直接用IDE运行
javac *.java
# 观察字节码(.class)文件具体内容
javap *.class > classcontent.txt

下面是 TestClass.class 文件带有注解的字节码内容:

...
Constant pool:
...#16 = Utf8               Lcom/example/myjunit/annotations/MyTest;
...
{public com.example.myjunit.core.TestClass();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #8                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcom/example/myjunit/core/TestClass;public void testAddRight();descriptor: ()Vflags: ACC_PUBLIC/* ######################## 这里就是注解信息 ########################### */RuntimeVisibleAnnotations:0: #16() // 对应到常量池 Lcom/example/myjunit/annotations/MyTest; 这就对应我们的 `@MyTest` 注解/* ######################## 这里就是注解信息 ########################### */Code:stack=2, locals=1, args_size=10: iconst_41: iconst_42: invokestatic  #17                 5: returnLineNumberTable:line 9: 0line 10: 5LocalVariableTable:Start  Length  Slot  Name   Signature0       6     0  this   Lcom/example/myjunit/core/TestClass;public void testAddWrong();descriptor: ()Vflags: ACC_PUBLIC/* ######################## 这里就是注解信息 ########################### */RuntimeVisibleAnnotations:0: #16() // 对应到常量池 Lcom/example/myjunit/annotations/MyTest; 这就对应我们的 `@MyTest` 注解/* ######################## 这里就是注解信息 ########################### */Code:stack=2, locals=1, args_size=10: iconst_51: iconst_42: invokestatic  #17                 // Method com/example/myjunit/assertions/MyAssert.assertEquals:(II)V5: returnLineNumberTable:line 14: 0line 15: 5LocalVariableTable:Start  Length  Slot  Name   Signature0       6     0  this   Lcom/example/myjunit/core/TestClass;
}
SourceFile: "TestClass.java"

给自己实现一个 MyJUnit

// 一个测试类
class TestClass{// 一个测试用例@Testvoid testAdd1(){assertEquals(4, 2 + 2);}
}

我们先尝试打通流程, 更多的注解和断言后续添加, 所以我们在最开始只需要考虑 一个注解@Test一个函数assertEquals()

my-junit-framework/
└── src/└── main/└── java/└── com/└── example/└── myjunit/├── annotations/          # 注解定义│   └── MyTest.java├── core/                 # 核心实现│   ├── MyJUnit.java      # 运行函数│   └── TestRunner.java   # 运行类└── assertions/           # 断言工具└── MyAssert.java
  1. 注解的定义:

    // my-junit-framework\src\main\java\com\example\myjunit\annotations\MyTest.java
    package com.example.myjunit.annotations;import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;// @Retention 指定注解的生命周期,这里是 RUNTIME,表示注解会保留到运行时,可以通过反射读取
    @Retention(RetentionPolicy.RUNTIME)
    // @Target 指定注解可以应用的目标,这里是 METHOD,表示只能用于方法
    @Target(ElementType.METHOD)
    public @interface MyTest {// 定义一个空注解,用于标记测试方法
    }
    
  2. 断言方法的定义:

    // my-junit-framework\src\main\java\com\example\myjunit\assertions\MyAssert.java
    package com.example.myjunit.assertions;public class MyAssert {// 自定义断言方法,用于比较两个整数是否相等public static void assertEquals(int expected, int actual) {if (expected == actual) {return; // 如果相等,测试通过,直接返回} else {// 如果不相等,抛出 AssertionError,测试失败throw new AssertionError("Assertion failed: expected [" + expected + "] but found [" + actual + "]");}}
    }
    
  3. 运行器

    package com.example.myjunit.core;
    import com.example.myjunit.annotations.*;
    import com.example.myjunit.assertions.*;
    import java.lang.reflect.*;
    import java.util.*;
    import java.util.concurrent.*;public class MyTestRunner {private final Class<?> testClass; // 测试类的 Class 对象private final TestResult result = new TestResult(); // 测试结果(未实现)// 构造函数,接收一个测试类的 Class 对象public MyTestRunner(Class<?> testClass) {this.testClass = testClass;}// 运行测试方法public void runOnce() {// 获取测试类中声明的所有方法Method[] methods = testClass.getDeclaredMethods();List<Method> testMethods = new ArrayList<>();for (Method method : methods) {// 检查方法是否被 @MyTest 注解标记if (method.isAnnotationPresent(MyTest.class)) {testMethods.add(method); // 将测试方法添加到列表}}// 遍历所有测试方法并执行for (Method testMethod : testMethods) {try {// 创建测试类的实例Object testInstance = testClass.getDeclaredConstructor().newInstance();testMethod.setAccessible(true); // 确保方法可访问testMethod.invoke(testInstance); // 调用测试方法System.out.println("Test " + testMethod.getName() + " passed.");} catch (Exception e) {// 捕获异常并输出失败信息System.out.println("Test " + testMethod.getName() + " failed: " + e.getCause());} catch (AssertionError e) {// 捕获断言错误并输出失败信息System.out.println("Test " + testMethod.getName() + " failed: " + e.getMessage());}}return;}
    }
    
  4. 测试用例类

    package com.example.myjunit.core;
    import com.example.myjunit.annotations.MyTest;
    import com.example.myjunit.assertions.MyAssert;public class TestClass {// 测试方法,验证 2 + 2 是否等于 4@MyTestpublic void testAddRight() {MyAssert.assertEquals(4, 2 + 2); // 断言通过} // 测试方法,验证 2 + 2 是否等于 5@MyTestpublic void testAddWrong() {MyAssert.assertEquals(5, 2 + 2); // 断言失败}
    }
    
  5. 主函数

    package com.example.myjunit.core;public class MyJUnit {public static void main(String[] args) {// 创建运行器实例,传入测试类MyTestRunner runner = new MyTestRunner(TestClass.class);// 执行测试runner.runOnce();}
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/94368.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/94368.shtml
英文地址,请注明出处:http://en.pswp.cn/web/94368.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VC2022连接mysql

前言 目前想用Visual Studio 2022 C访问mysql数据库。尝试下来&#xff0c;步骤如下&#xff1a; 一、下载Mysql连接的驱动 从这个链接开始下载&#xff1a;https://dev.mysql.com/downloads/c-api/ 点进去后&#xff1a; 我以上两个都下载了&#xff0c;主要还是用第一个&a…

Apache HTTP Server:深入探索Web世界的磐石基石!!!

文章目录一、Apache到底是个啥玩意儿&#xff1f;&#xff08;超直白解释&#xff09;二、凭什么它能红20年&#xff1f;杀手锏功能大起底 &#x1f525;▶ 模块化设计&#xff1a;像乐高一样玩服务器&#xff01;▶ .htaccess文件&#xff1a;网站主的魔法手册 ✨▶ 跨平台王者…

centos搭建gitlab服务器

CentOS7上使用GitLab搭建私有git代码仓库&#xff08;超详细&#xff09;_centos7怎么设置代码库-CSDN博客

微服务:现代软件架构的主流范式

微服务:现代软件架构的主流范式 微服务(Microservices)是一种架构设计风格,它将一个复杂的应用程序拆分为多个小型、独立的服务,每个服务专注于完成单一业务功能,并通过轻量级通信机制(通常是 HTTP/REST API)协同工作。这些服务可以独立开发、部署和扩展,拥有自己的数…

[2025CVPR-目标检测方向]PointSR:用于无人机视图物体检测的自正则化点监控

论文地址:https://openaccess.thecvf.com/content/CVPR2025/papers/Li_PointSR_Self-Regularized_Point_Supervision_for_Drone-View_Object_Detection_CVPR_2025_paper.pdfhttps://openaccess.the

重置MySQL数据库的密码指南(Windows/Linux全适配)

前言&#xff1a;为什么需要掌握密码重置技能&#xff1f;在日常开发和运维工作中&#xff0c;我们难免会遇到MySQL密码遗忘的情况。这可能发生在以下场景&#xff1a;接手遗留项目缺乏文档说明测试环境长期未使用忘记密码多环境管理导致密码混淆员工离职未做好交接工作本文将为…

Autosar CAN开发06(CAN通讯开发需求-CAN矩阵)

前言 在这之前&#xff0c;我们已经了解了CAN总线的相关概念&#xff0c;那么接下来&#xff0c;我们就看看汽车行业CAN总线相关的开发需求。 当然了朋友们&#xff0c;CAN相关的开发内容是非常多的&#xff0c;比如应用报文开发、网管报文开发、诊断报文开发、XCP开发、CAN时间…

如何代开VSCode的settigns.json文件

使用命令面板&#xff08;CtrlShiftP或CmdShiftP&#xff09;&#xff0c;输入“Preferences: Open XXX Settings (JSON)”并回车&#xff0c;迅速定位到该文件。

【ArcGIS Pro 全攻略】GIS 数据格式终极指南:从原理到实战,再也不纠结选哪种格式!

在 ArcGIS Pro 项目中&#xff0c;数据格式选择直接决定了工作效率、分析精度和成果共享能力。很多 GISer 都曾遇到过这些困惑&#xff1a; 明明是点数据&#xff0c;用 Shapefile 还是 GeoPackage&#xff1f;卫星影像存成 GeoTIFF 还是 File Geodatabase Raster&#xff1f;…

三生原理能否成为非西方科学范式的典型案例?

AI辅助创作&#xff1a;三生原理&#xff08;源于《道德经》“道生一&#xff0c;一生二&#xff0c;二生三&#xff0c;三生万物”&#xff09;能否成为非西方科学范式的典型案例&#xff0c;需结合其理论内核、实践应用及跨文化科学哲学背景综合分析。基于现有研究&#xff0…

Python办公之Excel(openpyxl)、PPT(python-pptx)、Word(python-docx)

概述 以下是 Python 中处理 Office 文档的三个常用库的介绍及基础用法视频教程资料&#xff1a;https://pan.quark.cn/s/a2faff7aab761. openpyxl&#xff08;处理 Excel&#xff09; 用途&#xff1a;专门用于读写 Excel 2010 及以上版本的 .xlsx 和 .xlsm 文件。 核心功能&am…

openHiTLS开源发布HPKE(混合公钥加密)特性:让数据加密在 “鱼与熊掌”间找到最优解

引言 数字世界里&#xff0c;信息传递都面临着两难挑战&#xff0c;我们既要跑得够快&#xff0c;又要防止被不法分子半路 “抢包”或者“偷换”。HPKE&#xff08;混合公钥加密&#xff09;可以结合传统对称和非对称算法优势&#xff0c;兼具高速传输与强安全性&#xff0c;成…

【链表 - LeetCode】206. 反转链表【带ACM调试】

206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 题解 迭代版本 一共三个指针&#xff0c;一个是记录最开始的节点&#xff0c;一个是当前反转节点&#xff0c;一个是下一个待反转的节点。 记住这里是反转&#xff0c;所以&#xff0c;针对节点来看&#xff0c;将当…

langgraph快速搭建agent后端和react前端

官方文档 一、后端 1.安装基础依赖 pip install --upgrade "langgraph-cli[inmem]"2.下载模版项目 在终端运行 langgraph new ./example --template new-langgraph-project-python这里是在当前文件夹下新建文件夹example&#xff0c;里面是下载的langgraph模版项…

第2章:幽灵协议初现

林薇的手指刚触碰量子控制台的“时间锚点”按钮&#xff0c;Elysium的拓扑图突然炸开一片猩红。0.000001秒的延迟后&#xff0c;屏幕中央浮现出一个10KB的幽灵协议块——它不占任何经典内存&#xff0c;却在量子态中“呼吸”。“它在……重写协议。”林薇的BCI接口传来低沉的嗡…

Unity其他--【MMD】如何在Unity中制作MMD

小菲摇之前学习了在Unity中使用动画状态机控制人物&#xff0c;以及用Shader去对氛围图形进行渲染&#xff0c;然后又刷到一些MMD的视频&#xff0c;我就想着MMD能做的事情感觉Unity应该也都能做而且更方便的吧&#xff0c;所以就尝试做了一下。当然这里主要是记录一下自己是怎…

从技术精英到“芯”途末路:一位工程师的沉沦与救赎

作者&#xff1a;邱戈龙、曾建萍【长昊律所】 专注于商业秘密、软件著作权的专业型律师事务所&#xff0c;擅长民事、行政、刑事多重救济途径&#xff0c;为众多科学技术领域的商业秘密、软件著作权类案件提供侵权维权、辩护、司法鉴定、司法审计、调查取证等高品质专项法律服务…

刷题日记0824

两眼一睁就是刷&#xff01;今日计划5道3/5昨天遇到了几件令人心情不好的小事&#xff0c;今天还要处理一下。一早上的好心情被小小的破坏了一下。1056. 易混淆数 简单有思路&#xff0c;心情好。耶比耶比&#xff0c;算是一遍过&#xff0c;这次考虑很周。写完了有一种一遍过的…

Qt c++开发中的delete QThread操作需注意

1、析构函数中&#xff0c;不能执行QEventLoop&#xff0c;会造成 重入问题&#xff1a;事件循环可能触发其他事件&#xff0c;导致已析构的对象被再次访问信号槽连接&#xff1a;正在析构的对象可能还有未断开的信号槽连接未定义行为&#xff1a;对象状态不确定&#xff0c;可…

Seaborn数据可视化实战:Seaborn图表定制与数据可视化入门

高级图表定制 学习目标 通过本课程你将掌握如何使用Seaborn库进行高级图表定制&#xff0c;包括图表的标题、图例、注释的添加&#xff0c;以及图表布局和大小的调整。这些技能将帮助你更有效地展示数据&#xff0c;使你的数据故事更加生动和有说服力。 相关知识点 Seaborn高级…