单元测试 vs Main方法调试:何时使用哪种方式?
单元测试 vs Main方法调试:何时使用哪种方式?
在Java开发过程中,我们经常需要编写测试代码来验证功能是否正常工作。但是,并非所有的测试代码都应该写成单元测试。本文将通过实际案例探讨何时使用JUnit单元测试,何时更适合使用main方法进行调试。
1. 单元测试的核心价值
正确用途
- 验证业务逻辑正确性
- 支持持续集成和自动化测试
- 提供可重复执行的质量保障
- 成为代码重构的安全网
典型应用场景
// 业务逻辑测试示例
@Test
public void testUserService() {UserService userService = new UserService();User user = userService.findById(1L);assertNotNull(user);assertEquals("张三", user.getName());
}
2. 不当使用单元测试的反面教材
原始的JwtTest.java问题分析
让我们看看一个典型的不当使用案例:
public class JwtTest {@Testpublic void testGenerateToken() {String token = getToken();System.out.println(token);}private String getToken() {Map<String, Object> claims = new HashMap<>();claims.put("id", 1);claims.put("nickname", "张三");claims.put("email", "zhangsan@main.com");String token = JWT.create().withClaim("user", claims).withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 10)).sign(Algorithm.HMAC256("reflection"));return token;}@Testpublic void testParseToken() {// 用当时手动的token会出现过期问题,需要生成新的// String token ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Im5pY2tuYW1lIjoi5byg5LiJIiwiaWQiOjEsImVtYWlsIjoiZm9pZG9hamZAZmlzZi5jb20ifSwiZXhwIjoxNzUyMDQ3OTk3fQ.URM-AHQ2lf_3jsLAxbB7Q96igJcRQZxS98V8Tliy0to";String token = getToken();JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("reflection")).build();DecodedJWT decodedJWT = jwtVerifier.verify(token);Map<String, Claim> claims = decodedJWT.getClaims();System.out.println(claims.get("user"));}
}
存在的问题
-
测试目的不明确
- 这些测试主要是为了学习
JWT
库的使用方式,而非验证业务逻辑
- 这些测试主要是为了学习
-
浪费构建资源
- 在项目打包时会执行这些"伪测试",消耗不必要的时间
-
容易导致构建失败
- 如代码注释所述,固定token过期导致[testParseToken](file:///Users/reflection/demoProjects/big-event/src/test/java/com/itheima/JwtTest.java#L37-L46)测试失败
- 影响正常的构建流程
-
误导团队成员
- 其他开发者可能误以为这是重要的业务测试
3. 需要Spring环境的工具调试场景
RedisTest.java问题分析
有些调试场景需要完整的Spring环境支持,例如:
@SpringBootTest
public class RedisTest {@Autowiredprivate RedisTemplate<String,String > redisTemplate;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void testRedis() {// 存储数据redisTemplate.opsForValue().set("name", "张三");// 获取数据String name = redisTemplate.opsForValue().get("name");System.out.println(name);}@Testpublic void testRedisDelete(){String value = redisTemplate.opsForValue().getAndDelete("name");System.out.println(value);}@Testpublic void testRedisKeys(){Set<String> keys = redisTemplate.keys("*");System.out.println("keys = " + keys);}@Testpublic void testRedisTokenDelete(){//修改密码以后,要使用户的所有jwt token 作废,user:login:userId:*Set<String> keys = stringRedisTemplate.keys("user:login:1:*");System.out.println("keys.size() = " + keys.size());if(CollectionUtil.isNotEmpty(keys)) {stringRedisTemplate.delete(keys);System.out.println("delete . . .");}}
}
场景特点
-
需要Spring容器支持
- 必须使用[@SpringBootTest](file:///Users/reflection/demoProjects/big-event/src/test/java/com/itheima/redis/RedisTest.java#L12-L12)注解启动Spring环境
- 依赖[@Autowired](file:///Users/reflection/demoProjects/big-event/src/test/java/com/itheima/redis/RedisTest.java#L14-L14)注入的Bean对象
-
本质上仍是工具调试
- 主要目的是学习
RedisTemplate
和StringRedisTemplate
的使用 - 验证Redis操作的基本语法和效果
- 并非验证项目核心业务逻辑
- 主要目的是学习
更好的解决方案:使用@Disabled注解
对于这类需要Spring环境但又是工具调试性质的代码,推荐使用[@Disabled](file:///Users/reflection/demoProjects/big-event/pom.xml#)注解:
@SpringBootTest
public class RedisToolDemo {@Autowiredprivate RedisTemplate<String,String > redisTemplate;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Test@Disabled("工具调试代码,手动执行")public void testRedisOperations() {// 存储数据redisTemplate.opsForValue().set("name", "张三");// 获取数据String name = redisTemplate.opsForValue().get("name");System.out.println("获取到的值: " + name);// 删除数据Boolean deleted = redisTemplate.delete("name");System.out.println("删除结果: " + deleted);}@Test@Disabled("工具调试代码,手动执行")public void testRedisTokenDelete(){// 模式匹配删除Set<String> keys = stringRedisTemplate.keys("user:login:1:*");System.out.println("匹配到的key数量: " + keys.size());if(CollectionUtil.isNotEmpty(keys)) {stringRedisTemplate.delete(keys);System.out.println("已删除匹配的keys");}}
}
4. Main方法调试的正确使用
适用场景
- 学习新API的使用方法
- 临时验证某个工具类的功能
- 调试第三方库的使用方式
- 编写示例代码供后续参考
改造后的JwtTest.java
public class JwtTest {public static void main(String[] args) {// JWT工具API调试和学习System.out.println("=== JWT Token Generation ===");String token = generateToken();System.out.println("Generated Token: " + token);System.out.println("\n=== JWT Token Parsing ===");parseToken(token);}private static String generateToken() {Map<String, Object> claims = new HashMap<>();claims.put("id", 1);claims.put("nickname", "张三");claims.put("email", "zhangsan@main.com");return JWT.create().withClaim("user", claims).withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60)) // 1分钟有效期.sign(Algorithm.HMAC256("reflection"));}private static void parseToken(String token) {try {JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("reflection")).build();DecodedJWT decodedJWT = jwtVerifier.verify(token);Map<String, Claim> claims = decodedJWT.getClaims();System.out.println("Parsed claims: " + claims.get("user").asMap());} catch (Exception e) {System.err.println("Token verification failed: " + e.getMessage());}}
}
5. 选择原则
应该使用单元测试的情况
✅ 验证业务功能正确性
✅ 需要持续集成自动化测试
✅ 测试结果需要被监控和报告
✅ 团队协作需要统一测试标准
应该使用Main方法调试的情况
✅ 学习新API的使用方法
✅ 临时验证某个工具类的功能
✅ 调试第三方库的使用方式
✅ 编写示例代码供后续参考
应该使用@Disabled注解的情况
✅ 需要Spring等框架环境支持
✅ 本质上是工具调试而非业务测试
✅ 需要手动触发执行
✅ 不应参与自动化构建流程
6. 最佳实践建议
明确分离三类代码
// ✅ 正确做法:业务测试类使用JUnit(正常执行)
public class UserServiceTest {@Testpublic void testSaveUser() { // 真正的业务逻辑测试}
}// ✅ 正确做法:工具调试类使用main方法
public class JwtToolDemo {public static void main(String[] args) { // API学习和调试}
}// ✅ 正确做法:需要Spring环境的工具调试使用@Disabled
@SpringBootTest
public class RedisToolDemo {@Test@Disabled("工具调试代码,手动执行")public void testRedisOps() { // 需要Spring环境的工具调试}
}
7. 实际项目影响
构建流程中的问题
- 打包时执行无意义的测试浪费时间
- 过期token等调试代码导致构建失败
- 混淆真正的业务测试和调试代码
开发效率的影响
- 增加不必要的测试执行时间
- 可能掩盖真正的测试问题
- 降低团队对测试质量的信任
8. 总结
选择单元测试还是main方法调试的关键在于明确测试目的:
- 单元测试:面向业务逻辑验证,追求稳定性和可重复性
- Main方法调试:面向API学习和临时验证,追求灵活性和便捷性
- @Disabled测试:需要框架环境支持的工具调试,兼顾便利性和规范性
合理的代码组织不仅能提高开发效率,还能保证项目质量和构建稳定性。记住,代码即文档,好的测试组织方式本身就是一种良好的编程习惯。