☰
银行账户单元测试实验报告
2026/10/27 18:33:56 网站建设 项目流程

银行账户单元测试实验报告

一、实验目的

  1. 掌握 Maven 项目配置,学会引入 JUnit 5 单元测试依赖
  2. 学会使用 JUnit5 编写单元测试,理解@Nested分组结构化测试
  3. 理解单元测试三大用例类型:正常场景、边界场景、异常场景
  4. 学会编写独立无依赖的测试用例,验证业务规则、捕获非法输入

二、实验内容

本次实验实现一个银行账户类BankAccount,提供存款、取款、转账、利息计算功能,内置业务约束:

  • 账户余额不能为负数
  • 取款、转账金额必须大于 0,且不能超过当前余额
  • 计息利率范围:0.0 ~ 1.0 参数非法时,程序主动抛出异常。 我们为这个类编写全套单元测试,验证每一条业务规则是否生效。

三、项目结构

【示意图 1:项目目录截图】

src ├── main │ └── java │ └── com/example │ └── BankAccount.java // 业务源代码 └── test └── java └── com/example └── BankAccountTest.java // JUnit5测试代码

四、核心代码

4.1 pom.xml(Maven 依赖配置)

作用:引入 JUnit5 测试依赖,让项目支持单元测试

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>bank-account-demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <junit-jupiter.version>5.9.2</junit-jupiter.version> </properties> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>${junit-jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit-jupiter.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.2</version> </plugin> </plugins> </build> </project>

4.2 BankAccount.java 业务类代码

package com.example; /** * 功能:管理账户的基本操作,包括存款、取款、转账和利息计算 * 要求: * - 账户余额不能为负数 * - 取款金额必须大于0且不超过余额 * - 转账金额必须大于0且不超过余额 * - 利率范围:0.0 ~ 1.0(即0% ~ 100%) */ public class BankAccount { private String accountId; private String ownerName; private double balance; // 常量 private static final double MIN_BALANCE = 0.0; private static final double MIN_AMOUNT = 0.0; // 构造函数 public BankAccount(String accountId, String ownerName, double initialBalance) { // 参数校验 if (accountId == null || accountId.trim().isEmpty()) { throw new IllegalArgumentException("账户ID不能为空"); } if (ownerName == null || ownerName.trim().isEmpty()) { throw new IllegalArgumentException("账户持有人姓名不能为空"); } if (initialBalance < MIN_BALANCE) { throw new IllegalArgumentException("初始余额不能为负数"); } this.accountId = accountId; this.ownerName = ownerName; this.balance = initialBalance; } // Getter方法 public String getAccountId() { return accountId; } public String getOwnerName() { return ownerName; } public double getBalance() { return balance; } /** * 存款操作 * @param amount 存款金额,必须 > 0 * @throws IllegalArgumentException 当存款金额 ≤ 0 时 */ public void deposit(double amount) { if (amount <= MIN_AMOUNT) { throw new IllegalArgumentException("存款金额必须大于0"); } this.balance += amount; } /** * 取款操作 * @param amount 取款金额,必须 > 0 且 ≤ 余额 * @throws IllegalArgumentException 当取款金额 ≤ 0 时 * @throws IllegalStateException 当余额不足时 */ public void withdraw(double amount) { if (amount <= MIN_AMOUNT) { throw new IllegalArgumentException("取款金额必须大于0"); } if (amount > this.balance) { throw new IllegalStateException("余额不足,无法取款。当前余额:" + this.balance); } this.balance -= amount; } /** * 转账操作(从当前账户转出) * @param targetAccount 目标账户,不能为null * @param amount 转账金额,必须 > 0 且 ≤ 余额 * @throws IllegalArgumentException 当目标账户为null或转账金额 ≤ 0时 * @throws IllegalStateException 当余额不足时 */ public void transferTo(BankAccount targetAccount, double amount) { if (targetAccount == null) { throw new IllegalArgumentException("目标账户不能为空"); } if (amount <= MIN_AMOUNT) { throw new IllegalArgumentException("转账金额必须大于0"); } if (amount > this.balance) { throw new IllegalStateException("余额不足,无法转账。当前余额:" + this.balance); } // 执行转账:先扣款,再存款 this.balance -= amount; targetAccount.balance += amount; } /** * 计算利息 * @param rate 年利率,范围 [0.0, 1.0] * @return 利息金额 * @throws IllegalArgumentException 当利率不在有效范围内时 */ public double calculateInterest(double rate) { if (rate < 0.0 || rate > 1.0) { throw new IllegalArgumentException("利率必须在0.0 ~ 1.0之间"); } return this.balance * rate; } }

4.3 BankAccountTest.java 测试代码(@Nested 分组结构化)

package com.example; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @DisplayName("BankAccount 银行账户单元测试") public class BankAccountTest { // ========== 构造方法测试 ========== @Test @DisplayName("构造账户:正常创建账户,初始余额合法") void shouldCreateAccountSuccessfullyWhenParamsValid() { BankAccount account = new BankAccount("A001", "张三", 1000.0); assertEquals("A001", account.getAccountId()); assertEquals("张三", account.getOwnerName()); assertEquals(1000.0, account.getBalance()); } @Test @DisplayName("构造账户:账户ID为null,抛出异常") void shouldThrowIllegalArgExWhenAccountIdNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new BankAccount(null, "张三", 100)); assertEquals("账户ID不能为空", ex.getMessage()); } @Test @DisplayName("构造账户:账户ID为空串,抛出异常") void shouldThrowIllegalArgExWhenAccountIdBlank() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new BankAccount(" ", "张三", 100)); assertEquals("账户ID不能为空", ex.getMessage()); } @Test @DisplayName("构造账户:持有人姓名为null,抛出异常") void shouldThrowIllegalArgExWhenOwnerNameNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new BankAccount("A001", null, 100)); assertEquals("账户持有人姓名不能为空", ex.getMessage()); } @Test @DisplayName("构造账户:初始余额为负数,抛出异常") void shouldThrowIllegalArgExWhenInitialBalanceNegative() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new BankAccount("A001", "张三", -50)); assertEquals("初始余额不能为负数", ex.getMessage()); } @Test @DisplayName("构造账户:初始余额等于0,创建成功(边界)") void shouldCreateAccountWhenInitialBalanceZero() { BankAccount account = new BankAccount("A002", "李四", 0.0); assertEquals(0.0, account.getBalance()); } // ===================== 嵌套:存款测试 ===================== @Nested @DisplayName("存款 deposit") class DepositTest { @Test @DisplayName("正常存入正数金额,余额增加") void shouldIncreaseBalanceWhenDepositValidAmount() { BankAccount account = new BankAccount("A001", "张三", 100); account.deposit(200); assertEquals(300, account.getBalance()); } @Test @DisplayName("存款金额等于0,抛出异常") void shouldThrowIllegalArgExWhenDepositAmountZero() { BankAccount account = new BankAccount("A001", "张三", 100); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> account.deposit(0)); assertEquals("存款金额必须大于0", ex.getMessage()); } @Test @DisplayName("存款金额负数,抛出异常") void shouldThrowIllegalArgExWhenDepositAmountNegative() { BankAccount account = new BankAccount("A001", "张三", 100); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> account.deposit(-10)); assertEquals("存款金额必须大于0", ex.getMessage()); } } // ===================== 嵌套:取款测试 ===================== @Nested @DisplayName("取款 withdraw") class WithdrawTest { @Test @DisplayName("正常取款,余额减少") void shouldDecreaseBalanceWhenWithdrawValidAmount() { BankAccount account = new BankAccount("A001", "张三", 500); account.withdraw(200); assertEquals(300, account.getBalance()); } @Test @DisplayName("取款金额等于余额,余额变为0(边界)") void shouldBalanceZeroWhenWithdrawAllBalance() { BankAccount account = new BankAccount("A001", "张三", 500); account.withdraw(500); assertEquals(0, account.getBalance()); } @Test @DisplayName("取款金额为0,抛出异常") void shouldThrowIllegalArgExWhenWithdrawAmountZero() { BankAccount account = new BankAccount("A001", "张三", 500); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> account.withdraw(0)); assertEquals("取款金额必须大于0", ex.getMessage()); } @Test @DisplayName("取款金额大于余额,抛出余额不足异常") void shouldThrowIllegalStateExWhenWithdrawOverBalance() { BankAccount account = new BankAccount("A001", "张三", 500); IllegalStateException ex = assertThrows(IllegalStateException.class, () -> account.withdraw(600)); assertTrue(ex.getMessage().contains("余额不足")); } } // ===================== 嵌套:转账测试 ===================== @Nested @DisplayName("转账 transferTo") class TransferToTest { @Test @DisplayName("正常转账,转出账户扣钱、目标账户加钱") void shouldTransferSuccessWhenParamValid() { BankAccount from = new BankAccount("A001", "张三", 1000); BankAccount to = new BankAccount("A002", "李四", 200); from.transferTo(to, 300); assertEquals(700, from.getBalance()); assertEquals(500, to.getBalance()); } @Test @DisplayName("目标账户为null,抛出异常") void shouldThrowIllegalArgExWhenTargetAccountNull() { BankAccount from = new BankAccount("A001", "张三", 1000); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> from.transferTo(null, 100)); assertEquals("目标账户不能为空", ex.getMessage()); } @Test @DisplayName("转账金额为0,抛出异常") void shouldThrowIllegalArgExWhenTransferAmountZero() { BankAccount from = new BankAccount("A001", "张三", 1000); BankAccount to = new BankAccount("A002", "李四", 200); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> from.transferTo(to, 0)); assertEquals("转账金额必须大于0", ex.getMessage()); } @Test @DisplayName("转账金额超过余额,抛出余额不足异常") void shouldThrowIllegalStateExWhenTransferOverBalance() { BankAccount from = new BankAccount("A001", "张三", 500); BankAccount to = new BankAccount("A002", "李四", 200); IllegalStateException ex = assertThrows(IllegalStateException.class, () -> from.transferTo(to, 600)); assertTrue(ex.getMessage().contains("余额不足")); } } // ===================== 嵌套:利息计算测试 ===================== @Nested @DisplayName("利息计算 calculateInterest") class CalculateInterestTest { @Test @DisplayName("利率0.0,利息为0(边界)") void shouldReturnZeroInterestWhenRateZero() { BankAccount account = new BankAccount("A001", "张三", 1000); double interest = account.calculateInterest(0.0); assertEquals(0, interest); } @Test @DisplayName("利率1.0,利息等于本金(边界)") void shouldReturnFullBalanceInterestWhenRateOne() { BankAccount account = new BankAccount("A001", "张三", 1000); double interest = account.calculateInterest(1.0); assertEquals(1000, interest); } @Test @DisplayName("正常利率0.05,计算正确") void shouldCalculateCorrectInterestWhenRateValid() { BankAccount account = new BankAccount("A001", "张三", 1000); double interest = account.calculateInterest(0.05); assertEquals(50, interest); } @Test @DisplayName("利率小于0,抛出异常") void shouldThrowIllegalArgExWhenRateLessThanZero() { BankAccount account = new BankAccount("A001", "张三", 1000); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> account.calculateInterest(-0.01)); assertEquals("利率必须在0.0 ~ 1.0之间", ex.getMessage()); } @Test @DisplayName("利率大于1.0,抛出异常") void shouldThrowIllegalArgExWhenRateGreaterThanOne() { BankAccount account = new BankAccount("A001", "张三", 1000); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> account.calculateInterest(1.1)); assertEquals("利率必须在0.0 ~ 1.0之间", ex.getMessage()); } } }

五、测试运行效果

【示意图 2:IDEA 测试运行树截图】 IDEA 测试窗口会分级展示:

说明:@Nested就是用来分组,把存款、取款、转账、利息分开,层次一目了然。所有测试用例互相独立,每个测试方法都会新建账户对象,测试顺序打乱也不会互相影响。

【示意图 3:测试全部通过的绿色运行结果截图】 运行方式:IDEA 直接右键运行测试类;或者命令行执行mvn test。 绿色代表用例通过,红色代表功能不符合预期。

六、测试思路说明

一共三类测试用例:

  1. 正常场景:输入合法数据,验证功能结果是否正确;
  2. 边界场景:测试临界值,比如全部余额取出、利率取 0、利率取 1;边界位置最容易出错;
  3. 异常场景:传入非法参数,校验程序是否抛出指定异常,拦截错误操作。

七、实验心得

本次实验使用 JUnit5 完成银行账户类单元测试,借助@Nested注解实现测试用例分组,结构清晰直观。测试覆盖正常、边界与异常场景,验证了存款、取款、转账、计息的业务规则。我认识到单元测试可以提前发现代码缺陷,保障程序健壮性;同时测试用例必须相互独立、命名语义化,方便后期查看和维护。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询