• 我就是不喜欢写@Test,怎么了?
  • 发布于 2天前
  • 105 热度
    0 评论
各位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()),单元测试要测细;但业务逻辑,一定要盯着用户能感知的“行为”测,别让测试变成“代码监控器”,要让它成为“用户体验守护神”呀。 你们项目里有哪些测着测着就变味的测试?快来评论区吐槽!
用户评论