各位Javaer小伙伴~ 今天换个接地气的例子,聊聊测试——就说咱们系统里超常见的“用户发奖”场景吧! 看完保证你秒懂啥是“测行为”vs“测实现”~
先整个发奖服务的类
假设我们有个UserAwardService,负责给用户发积分奖励,逻辑超简单:新用户首次登录送100积分,老用户登录送10积分~
// UserAwardService.java
public class UserAwardService {
private int userPoints; // 用户当前积分(内部状态)
private boolean isNewUser; // 是否新用户(内部状态)
public UserAwardService(boolean isNewUser) {
this.isNewUser = isNewUser;
this.userPoints = 0; // 初始积分为0
}
// 登录时触发发奖
public void giveLoginAward() {
if (isNewUser) {
userPoints += 100; // 新用户多送点
isNewUser = false; // 发完就不是新用户啦
} else {
userPoints += 10; // 老用户常规奖励
}
}
// 堆代码 duidaima.com
// 给用户看的当前积分
public int getUserPoints() {
return userPoints;
}
}
反例1:盯着内部变量测,坑!
新手常犯的错:测试直接跟内部状态“绑定”,比如这样:
// 错误示范:UserAwardServiceTest.java
import org.junit.Test;
importstatic org.junit.Assert.*;
public class UserAwardServiceTest {
@Test
public void shouldGive100PointsToNewUser() {
UserAwardService service = new UserAwardService(true);
service.giveLoginAward();
// 注意!这里直接测了私有变量(假设userPoints是public的情况)
assertEquals(100, service.userPoints);
// 还盯着isNewUser这个内部标记测
assertFalse(service.isNewUser);
}
}
敲黑板!🚨 这测试问题大了:
如果哪天产品说“新用户奖励改成150积分”,我们改了giveLoginAward()里的数字,功能是对的,但测试会因为100≠150挂掉!更惨的是,如果重构时把userPoints换成PointRecord对象(比如需要记录明细),明明用户看到的积分总数是对的,测试却直接崩了!这不是在测“用户能拿到多少分”,而是在测“代码里的变量叫啥名”啊~
反例2:Mock堆成山,更坑!
实际业务里,发奖可能依赖其他服务(比如日志、通知),于是测试写成这样:
// 带依赖的发奖服务
public class UserAwardService {
private PointService pointService; // 积分服务
private NotifyService notifyService; // 通知服务
// 发奖并通知用户
public void giveAwardAndNotify(Long userId) {
int points = pointService.calculate(userId); // 计算奖励
pointService.add(userId, points); // 加积分
notifyService.send(userId, "恭喜获得" + points + "积分"); // 发通知
}
}
// 错误示范的测试(用Mockito)
@Test
public void shouldGiveAwardAndNotify() {
// 一堆Mock准备工作
PointService pointService = mock(PointService.class );
NotifyService notifyService = mock(NotifyService.class );
UserAwardService service = new UserAwardService(pointService, notifyService);
Long userId = 123L;
// 模拟返回值
when(pointService.calculate(userId)).thenReturn(50);
// 堆代码 duidaima.com
// 执行方法
service.giveAwardAndNotify(userId);
// 验证各种内部调用
verify(pointService, times(1)).calculate(userId);
verify(pointService, times(1)).add(userId, 50);
verify(notifyService, times(1)).send(eq(userId), contains("50"));
}
宝子们品品, 这测试里Mock代码比业务逻辑还长!咱们明明想测“发奖后用户能收到通知和积分”,结果变成了“验证代码调用顺序对不对”。万一哪天giveAwardAndNotify()里先通知后加积分(逻辑没变,顺序变了),或者calculate()改名叫computePoints(),测试就全红了,明明用户端毫无感知啊!
正确姿势:像用户一样验收!
记住:用户只关心“登录后积分多了多少”“收到的通知对不对”,才不管你代码里变量叫啥、调用顺序是啥~
重写发奖测试:
// 正确示范:UserAwardServiceTest.java
import org.junit.Test;
importstatic org.junit.Assert.*;
public class UserAwardServiceTest {
@Test
public void newUserShouldGet100PointsAfterLogin() {
// 1. 准备一个新用户
UserAwardService service = new UserAwardService(true);
// 2. 模拟用户操作:登录(触发发奖)
service.giveLoginAward();
// 3. 验证用户能看到的结果:积分变成100
assertEquals(100, service.getUserPoints());
}
@Test
public void oldUserShouldGet10PointsAfterLogin() {
// 老用户登录两次(第一次发10,第二次再发10)
UserAwardService service = new UserAwardService(false);
service.giveLoginAward();
service.giveLoginAward();
// 验证总积分是20(用户视角的结果)
assertEquals(20, service.getUserPoints());
}
}
是不是舒服多了? 这个测试不管你内部是用int 存积分,还是用对象存;不管isNewUser是布尔值还是枚举,只要用户登录后拿到的积分对,测试就通过!
以后产品改规则(比如新用户送200),我们改业务逻辑,测试不用动;重构代码结构,测试也稳稳的。
最后叨叨两句
Java测试的精髓:把自己当成用户,只验收“输入→输出”的结果。
比如测订单支付:调用payOrder()后,查getOrderStatus()是不是“已支付”,而不是测orderStatus变量是不是1(假设1代表已支付)。
对于工具类(比如AwardCalculator.calculate()),单元测试要测细;但业务逻辑,一定要盯着用户能感知的“行为”测,别让测试变成“代码监控器”,要让它成为“用户体验守护神”呀。 你们项目里有哪些测着测着就变味的测试?快来评论区吐槽!