39_Java单元测试JUnit入门
Java单元测试JUnit入门文章目录Java单元测试JUnit入门前言一、环境准备与第一个测试二、JUnit常用注解三、断言Assertions四、测试套件Test Suite五、参数化测试六、Mock简介总结✅ 亮点总结适用场景扩展方向前言“这段代码没问题不用测试”——这是软件工程中最危险的自负。一个bug在开发阶段被发现和在生产环境被用户发现修复成本可能相差百倍。单元测试就是开发阶段最有效的质量保障手段而JUnit是Java生态中最主流的单元测试框架。本文将从零开始带你掌握JUnit的核心用法。测试的ROI很多开发者抗拒写单元测试的理由是浪费时间。但实际上调试一个没有测试覆盖的bug所花的时间通常是写测试的3-5倍——因为你需要在脑海中重新构建代码的上下文还要手动构造测试数据、模拟各种边界条件。更重要的是有单元测试保护的代码你可以放心重构而不怕引入回归bug。单元测试就像一份代码的行为说明书几个月后你回来看代码跑一遍测试就知道各方法期望的输入输出是什么。在实际面试中是否有写测试的习惯也是区分初中级和高级工程师的重要标尺。一、环境准备与第一个测试在Maven项目中添加JUnit依赖以JUnit 4为例JUnit 5时代码会更现代dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion4.13.2/versionscopetest/scope/dependency编写被测试的类// src/main/java/com/example/Calculator.javapublicclassCalculator{publicintadd(inta,intb){returnab;}publicintdivide(inta,intb){if(b0){thrownewIllegalArgumentException(除数不能为0);}returna/b;}publicintmultiply(inta,intb){returna*b;}}编写测试类测试类命名规范被测类名Test// src/test/java/com/example/CalculatorTest.javaimportorg.junit.Test;importstaticorg.junit.Assert.*;publicclassCalculatorTest{TestpublicvoidtestAdd(){CalculatorcalcnewCalculator();intresultcalc.add(2,3);assertEquals(2 3 应该等于 5,5,result);}TestpublicvoidtestDivide(){CalculatorcalcnewCalculator();assertEquals(3,calc.divide(6,2));assertEquals(0,calc.divide(0,5));}Test(expectedIllegalArgumentException.class)publicvoidtestDivideByZero(){CalculatorcalcnewCalculator();calc.divide(10,0);// 期望抛出异常}}测试方法命名建议test 方法名 测试场景如testDivideByZero。也可以使用Given-When-Then风格givenTwoNumbers_whenAdd_thenReturnSum。测试方法编写的基本原则——AAA模式Arrange准备测试数据、Act执行被测方法、Assert断言结果。例如上面的testAdd先Arrange创建Calculator对象再Act调用calc.add(2,3)最后Assert断言assertEquals(5, result)。清晰的AAA结构让测试一目了然评审者能快速理解测试意图。注意AAA并不是说每个测试只能有一个Act——有时需要连续调用多个方法来完成一个业务场景——但核心是准备-执行-验证的清晰分工。二、JUnit常用注解JUnit提供了丰富的注解来控制测试的生命周期和行为importorg.junit.*;importstaticorg.junit.Assert.*;publicclassLifecycleTest{// 在所有测试方法之前执行一次必须是staticBeforeClasspublicstaticvoidsetUpBeforeClass(){System.out.println([BeforeClass] 整个测试类初始化一次);// 典型用途建立数据库连接、加载配置文件}// 在所有测试方法之后执行一次必须是staticAfterClasspublicstaticvoidtearDownAfterClass(){System.out.println([AfterClass] 整个测试类清理一次);// 典型用途关闭数据库连接}// 在每个Test方法之前执行BeforepublicvoidsetUp(){System.out.println( [Before] 每个测试方法前执行);// 典型用途初始化测试数据}// 在每个Test方法之后执行AfterpublicvoidtearDown(){System.out.println( [After] 每个测试方法后执行);// 典型用途清理测试数据}TestpublicvoidtestMethod1(){System.out.println( testMethod1);assertTrue(true);}TestpublicvoidtestMethod2(){System.out.println( testMethod2);assertEquals(4,22);}// 忽略此测试暂不执行Ignore(等待需求确认后实现)TestpublicvoidtestNotReady(){// 这个测试暂时跳过}// 超时测试单位毫秒Test(timeout1000)publicvoidtestTimeout(){// 如果超过1秒仍未完成判定为失败try{Thread.sleep(500);}catch(InterruptedExceptione){e.printStackTrace();}}}输出示例[BeforeClass] 整个测试类初始化一次 [Before] 每个测试方法前执行 testMethod1 [After] 每个测试方法后执行 [Before] 每个测试方法前执行 testMethod2 [After] 每个测试方法后执行 [AfterClass] 整个测试类清理一次三、断言Assertions断言是测试的核心JUnit提供了丰富的断言方法。理解各种断言方法的适用场景能让你的测试更精准、失败信息更清晰。典型错误用法用assertTrue(condition)替代所有断言。比如assertTrue(a b)——如果失败你只能看到expected true but was false但看不到a和b的实际值。应该用assertEquals(expected, actual)——失败时会打印expected 5 but was 3直接定位问题。同理不要用assertTrue(list.contains(x))而要用专门的集合断言或assertThat。importorg.junit.Test;importstaticorg.junit.Assert.*;publicclassAssertionDemo{TestpublicvoidtestAssertions(){// 等值断言assertEquals(字符串应相等,hello,hello);assertEquals(浮点数有精度误差,3.14,3.14159,0.01);// 第三个参数是误差范围// 真假断言assertTrue(条件应为真,53);assertFalse(条件应为假,12);// 空值断言Stringstrnull;assertNull(应为null,str);assertNotNull(不应为null,hello);// 相同引用断言 而非 equalsStrings1abc;Strings2s1;assertSame(s1,s2);// 数组断言int[]expected{1,2,3};int[]actual{1,2,3};assertArrayEquals(expected,actual);}}经验法则每个测试方法只测一个行为并使用有意义的断言消息第一个参数这样测试失败时能快速定位问题。一条测试多个断言还是多个测试原则是测试同一个行为的不同方面可以放多个断言测试不同行为必须分开。比如测试divide方法testDivideNormal可以同时断言divide(6,2)3和divide(0,5)0因为这都是在测正常除法但testDivideByZero必须单独写一个测试方法因为它在测异常路径。混在一起的话第一个断言失败后后面的断言就不会执行了你无法知道后面的行为是否也出问题了。四、测试套件Test Suite当测试类越来越多时可以用测试套件将它们组合在一起批量执行importorg.junit.runner.RunWith;importorg.junit.runners.Suite;RunWith(Suite.class)Suite.SuiteClasses({CalculatorTest.class,LifecycleTest.class,AssertionDemo.class})publicclassAllTests{// 此类为空仅作为套件的容器// 运行此类即可执行所有指定的测试类}多个套件还可以嵌套组合RunWith(Suite.class)Suite.SuiteClasses({BusinessTestSuite.class,UtilTestSuite.class})publicclassFullTestSuite{}五、参数化测试当需要测试同一逻辑在不同输入下的表现时参数化测试可以避免写大量相似的测试方法importorg.junit.Test;importorg.junit.runner.RunWith;importorg.junit.runners.Parameterized;importjava.util.Arrays;importjava.util.Collection;importstaticorg.junit.Assert.assertEquals;RunWith(Parameterized.class)publicclassCalculatorParameterizedTest{privateinta;privateintb;privateintexpected;// 构造器接收参数publicCalculatorParameterizedTest(inta,intb,intexpected){this.aa;this.bb;this.expectedexpected;}// 提供参数数据的方法Parameterized.Parameters(name{index}: {0} {1} {2})publicstaticCollectionObject[]data(){returnArrays.asList(newObject[][]{{1,1,2},{2,3,5},{0,0,0},{-1,1,0},{100,200,300}});}TestpublicvoidtestAdd(){CalculatorcalcnewCalculator();assertEquals(expected,calc.add(a,b));}}六、Mock简介单元测试讲究隔离。当被测试的类依赖数据库或外部服务时我们用Mock对象来模拟这些依赖。为什么要Mock单元测试的目标是验证被测类自身的逻辑而不是它所依赖的外部系统。如果你的UserService里调用了PaymentGateway而PaymentGateway又连接了真实的支付接口那么测试会变慢网络延迟测试不稳定支付接口可能挂了会产生副作用真的扣了钱无法测试边缘场景如支付接口返回超时、返回异常Mock对象让你完全掌控依赖的行为可以模拟支付成功“支付失败”支付超时等各种场景而不依赖任何外部系统。// 需要引入 Mockito 依赖// 业务类依赖外部服务classOrderService{privatePaymentGatewaypaymentGateway;publicOrderService(PaymentGatewaypaymentGateway){this.paymentGatewaypaymentGateway;}publicStringplaceOrder(doubleamount){if(paymentGateway.process(amount)){return订单成功;}return支付失败;}}interfacePaymentGateway{booleanprocess(doubleamount);}// 手动MockclassMockPaymentGatewayimplementsPaymentGateway{privatebooleanshouldSucceed;publicMockPaymentGateway(booleanshouldSucceed){this.shouldSucceedshouldSucceed;}Overridepublicbooleanprocess(doubleamount){returnshouldSucceed;}}// 测试TestpublicvoidtestPlaceOrderSuccess(){PaymentGatewaymockGatewaynewMockPaymentGateway(true);OrderServiceservicenewOrderService(mockGateway);assertEquals(订单成功,service.placeOrder(100.0));}TestpublicvoidtestPlaceOrderFailure(){PaymentGatewaymockGatewaynewMockPaymentGateway(false);OrderServiceservicenewOrderService(mockGateway);assertEquals(支付失败,service.placeOrder(100.0));}更推荐使用Mockito框架进行Mockimportstaticorg.mockito.Mockito.*;TestpublicvoidtestWithMockito(){// 创建Mock对象PaymentGatewaygatewaymock(PaymentGateway.class);// 设定行为when(gateway.process(anyDouble())).thenReturn(true);OrderServiceservicenewOrderService(gateway);Stringresultservice.placeOrder(50.0);assertEquals(订单成功,result);// 验证方法被调用了verify(gateway).process(50.0);}总结单元测试不是负担而是开发者的安全网。JUnit的核心要素包括Test注解标记测试方法、断言Assert验证结果、Before/After管理测试生命周期、测试套件批量执行。对于外部依赖使用Mock对象来隔离测试。测试覆盖率不是目的有意义的测试才是。养成写代码前先想测试的习惯你的代码质量将会有质的飞跃。TDD入门测试驱动开发Test-Driven Development的核心理念是先写测试再写实现。三部曲是Red写一个失败的测试→ Green写最少代码让测试通过→ Refactor重构代码测试仍然通过。TDD最大的好处不是先写测试本身而是它迫使你先思考这个类的接口应该是什么样的“边界条件有哪些”“什么算成功什么算失败”——这些思考反过来会让你的API设计更合理。即使你不完全采纳TDD在写复杂业务逻辑前先列一份测试场景清单也是极好的实践。✅ 亮点总结Test注解标记测试方法Before/After管理测试生命周期执行顺序清晰可控丰富的断言方法assertEquals、assertTrue、assertNull、assertArrayEquals覆盖各种验证场景参数化测试Parameterized实现数据驱动一组测试数据覆盖多种输入情况Mock对象隔离外部依赖配合Mockito的when/thenReturn和verify实现行为验证测试套件Suite批量组织和管理测试类支持嵌套分组适用场景日常开发中为Service层业务逻辑编写单元测试确保核心逻辑正确回归测试阶段批量运行测试套件验证代码修改未引入新Bug使用Mock隔离数据库或外部API依赖在CI/CD流水线中实现快速无环境测试扩展方向学习JUnit 5的新特性DisplayName自定义测试名称、Nested内嵌测试类、ParameterizedTest增强参数化深入Mockito框架掌握spy、ArgumentCaptor、doThrow等高级Mock技巧推荐阅读下一篇文章Java日志框架使用指南掌握项目排错的核心工具