简介这份Junit单元测试实验报告面向正在学习软件测试、需要完成单元测试实验的学生围绕Junit的安装配置与实例应用展开重点解决如何把单元测试方法落到具体程序中的问题。压缩包内共1个docx文件约1.09MB完整收录实验报告正文、示例代码与测试结果便于按实验流程阅读和复现。报告以软件测试中的佣金问题为案例给出Salary类的销售额与佣金计算代码并采用边界值方法设计测试用例记录不同枪机、枪托、枪管数量组合下的预期输出与验证结果。已有4894人学习或下载适合需要理解Junit断言、测试用例设计、边界值分析和实验报告写法的读者参考也可作为课程实验的对照材料。1. 装错的不是 JUnit是环境一份佣金结算练习引出的单元测试装配上周帮人看一台机器IDEA 里 CtrlShiftT 生成的测试类点绿箭头毫无反应右键 Run 只吐一行Process finished with exit code 0一条断言都没执行。翻到依赖里同时存在 junit 4 和 junit-jupiter 两个包测试类又extends TestCase——这是典型的 Junit 单元测试环境没有配置干净依赖、运行器、目录三者对不上测试跑没跑你根本不知道。这类佣金结算练习题最适合验证环境lock 单价 45、stock 单价 30、barrel 单价 25输入 10、20、30销售金额必须是 1800佣金必须是 220。差一分钱要么是环境把测试吞了要么是断言写歪了。下面按我平时的顺序走版本对齐、IDEA 装配、用例落表、跑不起来怎么救新手照着抄能通熟手可以直奔版本取舍和排查那两章。2. 版本对齐JDK、JUnit 4.x 与测试目录的三方对照装 JUnit 最容易翻车的地方不是下载是版本。同一份Salary.java在 JDK 8 上能编译在 JDK 17 上可能因为模块化报cannot access同一个测试类配 JUnit 4 能跑配 JUnit 5 就变成零用例。所以装之前先把三件事定死JDK 主版本、JUnit 大版本、源码与测试的目录归属。2.1 JDK 选 8 还是 17先看被测代码用了什么这份Salary只用了java.util.Scanner和控制台输出没有任何新语法JDK 8 就够。但我一般不会为了省事停在 8除非机器上还挂着别的老项目。JDK 11 和 17 都能编过这段代码真正要留意的是构建工具链如果你的 IDEA 用 JDK 17 编译而测试运行配置里的 JRE 还是 8就会出现「主代码能编、测试起不来」的割裂状态。常见做法是让三处保持一致——Project SDK、Project language level、Run Configuration 里的 JRE。检查命令很土但有效# 确认命令行里的 JDK 与 IDEA 里选的是同一个 java -version javac -version echo $JAVA_HOME三行输出的主版本号必须一致JAVA_HOME指向的目录要和java所在目录同源。我见过java -version是 17、javac -version是 8 的机器编译产物和运行时字节码版本对不上测试类加载阶段直接抛UnsupportedClassVersionError而报错信息里只提 class file version新手很难第一时间联想到 JDK。Windows 下同理where java和where javac两个路径必须是同一个 JDK 目录下的 bin。提示不要在项目里混用两个 JDK 的JAVA_HOME尤其是装过多个 IDE 的机器环境变量经常被上一个安装包改写。2.2 JUnit 3/4/5 三条线这份代码为什么只能落在 4.xJUnit 有三代写法互相不完全兼容JUnit 3测试类必须extends TestCase方法名必须以test开头没有注解。JUnit 4用Test、Before、After、RunWith可以继承TestCase但会退回 3 的运行器。JUnit 5拆成 Jupiter API包名是org.junit.jupiter.api.*注解是BeforeEach、AfterEach。原料里的测试类同时出现了extends TestCase、import junit.framework.TestCase、Before、After、Test、org.junit.Assert——这是 JUnit 4 的包但套了 JUnit 3 的骨架。这种混写在实际运行时IDEA 会按 JUnit 3 的 Runner 处理整个类注解里的Test被忽略于是方法名testTotalSalary恰好符合 3 的命名约定才勉强能跑一旦你把方法改名叫shouldComputeCommission立刻变成「没有可运行的测试」。所以这个项目的正确落点是JUnit 4.xorg.junit.Test、org.junit.Before、org.junit.After全部来自 junit 4 的 jar测试类不要再继承TestCase。想上 JUnit 5 也行但那是另一套注解和另一个 import两套不能混在同一个类里。2.3 目录骨架先定死src/main 与 src/test 的分工原料里被测类和测试类都是package main;且平铺在同一个包下这在实验报告里没问题在真实工程里会导致测试代码被打进产物。标准 Maven 布局是目录放什么是否进构建产物src/main/javaSalary.java等被测类是src/test/javaSalaryTest.java等测试类否src/main/resources配置文件是src/test/resources测试专用数据否手工建工程的话IDEA 里对应两步File Project Structure Modules把src/main/java标记为 Sources把src/test/java标记为 Tests。标记完目录图标会变色源码蓝、测试绿这个颜色是判断目录有没有认对的唯一凭据。包名保持两边一致被测类在main包测试类也在main包只是根目录不同。测试类能访问包级私有成员靠的就是这个「同包不同根」。2.4 一张环境对照表装之前先核一遍我现在装任何 Java 项目都会先花两分钟填这张表填不出来就说明环境还没摸清组件本次选择检查方式装错的典型表现JDK8 / 11 / 17 三选一与 IDEA 一致java -version、javac -version类加载报 class file versionJUnit4.x依赖树里只有 junit:junit零用例、注解不生效Hamcrestjunit 4 自带依赖 hamcrest-coremvn dependency:treeAssert.assertThat找不到类构建工具Maven 或 IDEA 原生工程mvn -v手动加的 jar 与 pom 重复测试目录src/test/java目录图标为绿色测试类被打包、CtrlShiftT 生成到错位置这张表里最容易被忽略的是 Hamcrest。JUnit 4 的assertThat依赖 hamcrest-core如果只把junit-4.x.jar拖进项目却没带 hamcrest编译期就会报package org.hamcrest does not exist。用 Maven 引依赖不会漏手动加 jar 一定会漏。3. IDEA 里装配 JUnit插件、依赖、CtrlShiftT 三步落地环境定了接下来是装配。IDEA 的坑集中在「你以为装了其实只是插件在库没在」——插件负责识别和生成库负责编译和运行两者缺一不可。3.1 插件确认与 JUnit 库的两种加法第一步是插件。Settings Plugins里搜 JUnit如果 Installed 标签下已经有说明插件内置了不用再装。新版 IDEA 一般默认携带搜不到且 Installed 里也没有才需要去 Marketplace 装。插件装完要重启重启后CtrlShiftT才有反应。第二步是库也就是 jar。有两种加法选一种不要都做。方法 A走 Maven推荐!-- pom.xml -- dependencies dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version !-- 以本地仓库已有的 4.x 为准不要跨大版本 -- scopetest/scope !-- 关键只对测试目录可见不进产物 -- /dependency /dependenciesscope写test是这段配置里最重要的一个词。写成compilejunit 会被打进最终产物漏写默认是compile等于把测试框架发给使用者。写完执行mvn -U dependency:tree输出里应该只有一条 junit且 scope 显示 test。方法 B手工加 jar。File Project Structure Libraries From Maven搜索junit:junit:4.13.2下载或者Modules Dependencies JARs or directories手动选中junit-4.13.2.jar和hamcrest-core-1.3.jar。加完在 Scope 下拉里同样选 Test。方法 B 的风险是版本漂移三台机器上三个 jar 版本测出来的结果不一致。所以我一般只在没有网络、不能连仓库的环境里用 B其余一律 A。3.2 CtrlShiftT 生成测试类路径、命名与包名对齐库就位后把光标放在Salary类名上按CtrlShiftTIDEA 弹出创建测试的对话框。三个字段要逐个看Testing library选 JUnit4不要选 JUnit5。Class name默认SalaryTest保留。Destination package必须是main与Salary同包。Generate至少勾setUp/Before和tearDown/After其余方法按需。点 OK 之后第一件事不是写断言是看文件落在哪个目录。落在src/test/java/main/SalaryTest.java才算对落在src/main/java/main/下说明刚才 2.3 里的目录标记没做测试类会被当源码编译运行配置里也找不到它。生成的骨架长这样注意它默认带了extends TestCase的话要手动删掉package main; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; // 静态导入assertEquals 才能直接写 public class SalaryTest { Before public void setUp() throws Exception { // 每条用例执行前都会走一遍用来造对象、准备入参 } After public void tearDown() throws Exception { // 每条用例执行后走一遍用来清理引用 } Test public void testTotalSalary() { // 断言写在这里 } }Before和After是 JUnit 4 的写法对应 JUnit 5 的BeforeEach/AfterEach。这个类里不要出现import junit.framework.TestCase;也不要extends TestCase否则整类退回 JUnit 3 运行器Before不执行、assertTrue的行为也和 JUnit 4 不完全一致。3.3 不用 IDEA 的路线Maven 依赖与命令行跑测试换成从命令行跑好处是能把「插件问题」和「依赖问题」区分开。IDEA 里跑不动但命令行能跑问题在 IDE两边都跑不动问题在依赖或代码。项目按 Maven 布局建好pom.xml里加 3.1 的依赖然后# 只跑指定的测试类验证环境是否通 mvn -q -DtestSalaryTest test # 全量跑输出里会打印 Tests run 汇总行 mvn test-Dtest后面跟类名支持通配符如-DtestSalary*Test。跑完看最后三行Tests run: X, Failures: Y, Errors: Z, Skipped: W。X 为 0 就是没跑不是通过。这是新手最常误判的一行——构建成功的绿字等于编译通过不等于断言执行过。命令行能跑通之后再回 IDEA 配 Run ConfigurationRun Edit Configurations JUnitTest kind 选 ClassClass 填main.SalaryTestWorking directory 填项目根JRE 与 2.1 里核过的 JDK 一致。4. 把 Salary 接进测试三个方法入口与边界值用例落表装配完真正的活是把被测类接进来。这份Salary看着简单实际有三个坑commission()依赖totalPrice()算出的字段、越界时返回 -1 而不是抛异常、equals被重载成了三参数版本导致Object.equals语义失效。4.1 先读懂被测类的调用顺序totalPrice 必须在 commission 之前commission()第一行判断的是if (totalPrice 0)这里的totalPrice是类字段只有totalPrice()方法被调用过才会被赋值。也就是说Salary s new Salary(10, 20, 30); s.commission(); // 返回 -1因为字段还是默认值 0 s.totalPrice(); // 打印 1800字段被赋值 s.commission(); // 这次才返回 220这不是 bug是不良设计但它决定了测试的写法每条涉及佣金的用例必须先调一次totalPrice()。如果按「一个断言一条用例」的顺序写先测佣金的用例会稳定返回 -1然后你会怀疑是不是 JUnit 装坏了——这就是黑匣子式排查的代价。顺带提一句越界分支totalPrice()在数量超限时打印提示并返回 -1字段不赋值。所以越界用例里commission()也会返回 -1两个断言可以一起写。4.2 27 条手工用例压成 8 行参数化数据原料里的边界值表列了 27 条用例覆盖最小值、略大于最小值、中点、临界点、越界。手工一条条写Test类会膨胀到几百行改一个业务规则要改 20 处断言。我的做法是抽 8 条特征点用 JUnit 4 的Parameterized跑lockstockbarrel期望金额期望佣金覆盖点11110010下界55550050中段10101010001001000 临界含1010111025103.75刚过 100018181818002201800 临界含1818191825225刚过 180070809078001420上界708190-1-1stock 越界这 8 条里1000 和 1800 两个临界值上下各取一点是因为佣金分段规则在这里换挡≤1000 走 10%1000 以上到 1800 走 15%超过 1800 的部分走 20%。分段函数的错误九成出在临界点上写成就少算一段。代码package main; import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; RunWith(Parameterized.class) // 用参数化运行器替代默认运行器 public class SalaryBoundaryTest { private final int lock; private final int stock; private final int barrel; private final double expectTotal; // 期望销售金额 private final double expectCommission; // 期望佣金 // 构造器参数顺序必须与 data() 里每一行的顺序严格一致 public SalaryBoundaryTest(int lock, int stock, int barrel, double expectTotal, double expectCommission) { this.lock lock; this.stock stock; this.barrel barrel; this.expectTotal expectTotal; this.expectCommission expectCommission; } Parameters(name lock{0},stock{1},barrel{2}) public static CollectionObject[] data() { return Arrays.asList(new Object[][]{ {1, 1, 1, 100.0, 10.0}, {5, 5, 5, 500.0, 50.0}, {10, 10, 10, 1000.0, 100.0}, {10, 10, 11, 1025.0, 103.75}, {18, 18, 18, 1800.0, 220.0}, {18, 18, 19, 1825.0, 225.0}, {70, 80, 90, 7800.0, 1420.0}, {70, 81, 90, -1.0, -1.0} // stock 超过 80走越界分支 }); } Test public void boundaryCase() { Salary s new Salary(lock, stock, barrel); assertEquals(expectTotal, s.totalPrice(), 0.001); assertEquals(expectCommission, s.commission(), 0.001); } }几处要说明。RunWith(Parameterized.class)把默认运行器换掉整个类里不能再有其他普通Test逻辑需要混跑就拆成两个类。Parameters(name ...)里的{0}、{1}对应每行数组的下标跑失败时输出的是这一行的名字一眼能看出是哪组数据炸的比默认的[3]好读得多。构造器参数顺序对不上会报ArrayIndexOutOfBoundsException或断言莫名失败这是参数化测试最常见的手误。4.3 断言怎么写assertEquals 的 delta 与 assertTrue 的取舍原料里的断言是Assert.assertTrue(salary.equals(1800,220,salary))。这行有三个问题第一equals(double, double, Object)是三参数重载不是Object.equals语义完全变了。它拿到object后强转成Salary再调一次totalPrice()和commission()——等于每条断言都重复执行了被测方法副作用被放大。第二断言失败时只有一行expected true but was false你无法知道实际金额是多少、佣金差了多少。第三把期望值写死在代码里改数据要改代码。改成assertEqualsTest public void testTotalAndCommission() { Salary s new Salary(10, 20, 30); // 10*45 20*30 30*25 1800字段 totalPrice 在此被赋值 assertEquals(1800.0, s.totalPrice(), 0.001); // 1800 走第二档(1800-1000)*0.15 1000*0.1 220 assertEquals(220.0, s.commission(), 0.001); }double 比较必须带 delta第三个参数是容差。0.001对金额足够浮点乘加误差远小于一分钱。不带 delta 的assertEquals(double, double)在 JUnit 4 里已被标记过时某些版本直接编译报错。该用assertTrue的地方只剩纯布尔判断比如判断越界返回Salary bad new Salary(71, 20, 30); // lock 上限是 70 assertTrue(越界时金额应返回 -1, bad.totalPrice() 0);带消息字符串的写法要在前面加一句人话失败时输出里会带上这句比默认消息有用得多。4.4 Before/After 里的清理动作Before适合放「每条用例都要一份干净对象」的构造private Salary salary; Before public void setUp() { salary new Salary(10, 20, 30); // 每条用例前重建避免上个用例改过字段 } After public void tearDown() { salary null; // 断开引用防止用例间残留状态 }JUnit 4 对每条Test都会新建一个测试类的实例正常情况下不会有状态串扰。但这份被测类把totalPrice存在字段里如果你在Before里只new一次、在After里不置空某些 IDE 的重复运行会出现「上一条用例的字段值还没被覆盖」的错觉——不是 JUnit 的锅是被测对象有状态。清理动作写到After里成本几乎为零能省掉很多玄学排查。5. 排查清单JUnit 测试跑不起来、断言不生效的五种现场环境装完不等于装对。下面五种是我在别人机器上真实见过最多的每条按「现象 → 原因 → 解决」写对着症状找就行。5.1 现场一绿箭头点了测试一条没跑现象右键 Run 之后控制台只有Process finished with exit code 0没有Tests run汇总行左侧方法名旁边也没有绿色对勾。原因类上写了extends TestCase又用TestIDEA 判定为 JUnit 3 测试方法名不符合test前缀约定的直接被跳过或者依赖里根本没有 junit运行配置的 Test kind 选成了别的框架。解决删掉extends TestCase和import junit.framework.TestCase;只保留org.junit.*的注解。然后确认Project Structure Modules Dependencies里 junit 的 scope 是 Test。改完按CtrlShiftF10重跑看汇总行的Tests run是否大于 0。5.2 现场二commission 断言永远是 -1现象assertEquals(220.0, s.commission(), 0.001)报expected:220.0 but was:-1.0但手动在main方法里跑同样的输入结果是对的。原因commission()读的是字段totalPrice字段只在totalPrice()被调用后才赋值。测试里先调了commission()字段还是 0走进else分支返回 -1。解决断言顺序固定为先totalPrice()后commission()或者在被测类里把commission()改成先自己算一遍金额。测试侧改成本更低double total s.totalPrice(); // 先触发计算字段才有值 assertEquals(1800.0, total, 0.001); assertEquals(220.0, s.commission(), 0.001);5.3 现场三double 断言差零点零零零几就红现象期望 103.75实际 103.74999999999999测试红。原因(totalPrice - 1000) * 0.15 1000 * 0.1这类乘加在二进制浮点下必然有舍入误差assertEquals(double, double)不带容差就是精确比较。解决所有金额断言统一带 delta0.001即可如果要对外展示金额业务代码里用BigDecimal做结算测试再对BigDecimal比较compareTo。两者不要混用。5.4 现场四CtrlShiftT 生成的测试类找不到、跑不了现象生成完的SalaryTest.java在项目树里是蓝色的源码色右键没有 Run 选项或者报Cannot resolve symbol Test。原因src/test/java没有被标记为 Test Sources Root测试类被当成主源码编译或者包名写成了main之外的路径与被测类不同包。解决Project Structure Modules Sources选中src/test/java点 Tests 标记目录变绿包名与Salary.java保持一致。改完Build Rebuild Project再试。5.5 现场五JUnit 4 和 JUnit 5 同时存在跑哪个看运气现象mvn test有时跑 8 条有时跑 3 条IDEA 里同一个类重新打开后 Run 配置变了。原因pom.xml里同时有junit:junit和org.junit.jupiter:junit-jupiterclasspath 上两套注解都有运行器按依赖顺序择一。IDEA 缓存的 Run Configuration 也会记住上一次的框架选择。解决只留一套。用 4.x 就删掉所有 jupiter 依赖用 5.x 就把org.junit.Test全部换成org.junit.jupiter.api.TestBefore换BeforeEach。改完执行mvn -U clean test # 输出里的依赖树应只剩一套 junit mvn dependency:tree | grep -i junit清理 IDEA 缓存File Invalidate Caches / Restart选 Invalidate and Restart。这一步不能省缓存的运行器配置会继续生效。6. 跑通验收从红灯跑到绿灯的三步确认装完最后一步是验证而验证的核心不是「看到绿」是「确认这条测试真的会红」。绿得太顺利反而可疑——上一章那些零用例的现场全都是绿的。我一般走三步。第一步故意写错期望值把assertEquals(220.0, s.commission(), 0.001)改成assertEquals(999.0, ...)跑一次。必须出现红色的失败报告并且报告里能读到expected:999.0 but was:220.0。读不到实际值说明断言写法或运行器还有问题先别改回去。第二步恢复期望值跑整个测试类看三处数字对齐检查点通过标准不对时应看哪里用例数量Tests run等于数据行数本例 8参数化 data() 行数、Parameters 是否漏写失败数Failures 与 Errors 均为 0先看第一个失败别一次改八处耗时单条用例毫秒级秒级以上多半是 Scanner 等着读输入第三步换命令行再跑一遍mvn -DtestSalaryBoundaryTest test。IDEA 绿、命令行红问题在 IDE 的 JDK 或依赖作用域两边都绿这套环境才算真的装完。顺便把assertTrue那种不打印实际值的断言也换成assertEquals以后再红报告本身就是半个排查工具。有个细节值得单独说原料里main方法用了Scanner读标准输入测试类里那段Scanner必须注释掉。测试进程没有键盘输入in.nextInt()会一直阻塞表现为用例卡住不结束超时之后 IDE 报一个和断言毫无关系的错——这种误报最费时间。从那以后我每次配完 JUnit都强制走一遍「先弄红、再弄绿、最后命令行再跑一次」的流程不管项目多小。血泪经验是一条从没红过的测试等于没写一次没验证过会失败的安装等于没装。希望帮到你。本文还有配套的精品资源点击获取