從 JUnit 深入理解 Java 注解與反射機制

從 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高級…