小架构step系列12:单元测试

发布于:2025-07-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

1 概述

测试的种类很多:单元测试、集成测试、系统测试等,程序员写代码进行测试的可以称为白盒测试,单元测试和集成测试都可以进行白盒测试,可以理解为单元测试是对某个类的某个方法进行测试,集成测试则是测试一连串的方法。测试代码也是代码,即使要求测试代码逻辑简单,但写单元测试代码也是要花时间的,维护也是要花时间的,所以在写测试代码上也要有些平衡。单元测试的颗粒度比较小,最接近方法的业务逻辑,应该详尽的测试;而集成测试牵扯的方法比较多,就比较复杂,就挑重点的地方做测试。本文先了解单元测试。

2 单元测试

2.1 业务逻辑测试

在DDD里有一种思想,就是要把业务逻辑分离出来,这部分业务逻辑是业务的核心,应该是公司的核心价值所在,所以应该进行详尽测试。测试要做得多,成本就要低,所以这块的测试主要集中在逻辑方面,业务逻辑的代码也要写成函数的形式,也就是给予一定的参数,就会反馈相同的结果。这块代码不应该跟数据库、中间件等扯上关系,否则就很难做到轻量化。

有不少业务逻辑代码是比较简单的,不一定会按DDD的模式进行文件或者目录分离,但也要在类或者方法层面做到把业务逻辑分离,以便对这些业务进行良好的单元测试。这种测试仅需要对一些类进行mock,然后就跟测试一个有参数有返回值的方法那样简单。这就要求涉及到数据库或者中间件等外部资源的操作,都应该接口化,这样就比较容易进行mock。

2.2 测试例子

假设有个创建组成员的功能,为这个业务逻辑创建一个服务接口GroupMemberCreator和服务类GroupMemberCreatorImpl,里面有个方法create(GroupMember member),用于编写创建成员的业务逻辑,有部分业务会封装到业务对象GroupMember中;这个方法里需要把数据存储到数据库,则通过GroupMemberRepository提供save()接口把数据存储到数据库中,该接口只有数据库相关操作,而没有业务逻辑。

// 把业务逻辑放到GroupMember和GroupMemberCreatorImpl里
public class GroupMember {
    private Long groupId;
    private String name;

    public GroupMember(Long groupId, String name) {
        // 做业务逻辑:校验参数并组装GroupMember信息
        this.groupId = groupShouldExist(groupId);
        this.name = memberNameShouldNotExist(name);
    }
    public Long getGroupId() {
        return groupId;
    }
    public String getName() {
        return name;
    }
    private Long groupShouldExist(Long groupId) {
        // 校验组存在,否则抛异常
        return groupId;
    }
    private String memberNameShouldNotExist(String name) {
        // 校验成员是否已经存在,否则抛异常
        return name;
    }
}

public interface GroupMemberCreator {
    GroupMember create(Long groupId, String memberName);
}
@Service
public class GroupMemberCreatorImpl implements GroupMemberCreator {
    private GroupMemberRepository repository;

    @Autowired
    public GroupMemberCreatorImpl(GroupMemberRepository repository) {
        this.repository = repository;
    }

    @Override
    public GroupMember create(Long groupId, String memberName) {
        GroupMember member = new GroupMember(groupId, memberName);
        return repository.save(member);
    }
}


// Repository只有数据库相关操作,无业务逻辑
public interface GroupMemberRepository {
    GroupMember save(GroupMember member);
}
@Repository
public class GroupMemberReposistoryImpl implements GroupMemberRepository {
    @Override
    public GroupMember save(GroupMember member) {
        // 调相关DAO操作数据库
        return member;
    }
}


// 测试用例例子
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.ArgumentMatchers.any;
public class GroupMemberCreatorImplTest {
    @Test
    public void create_new_member() {
        Long groupId = 1L;
        String name = "Joy";

        // 1. mock对象
        // 调repository.save()的时候,返回一个mock对象,不真正调用数据库
        GroupMember mockMember = new GroupMember(groupId, name);
        GroupMemberRepository repository = Mockito.mock(GroupMemberRepository.class);
        Mockito.when(repository.save(any())).thenReturn(mockMember);

        // 2. 测试代码逻辑
        GroupMemberCreatorImpl creator = new GroupMemberCreatorImpl(repository);
        GroupMember member = creator.create(groupId, name);

        // 3. 断言(Assertion)
        Assertions.assertThat(member).isNotNull();
        Assertions.assertThat(member.getGroupId()).isEqualTo(groupId);
        Assertions.assertThat(member.getName()).isEqualTo(name);
    }
}

从上面测试用例来看,测试的三个步骤:一是用mockito进行mock对象,指定mock对象执行方法的返回值(可根据参数返回);二是调业务逻辑代码进行测试;三是对测试结果进行断言。

通过测试可以反过来思考方法应该如何设计,其规则大概是看输入输出,也就是有什么输入就预期响应的输出,要注意的是方法返回值不一定是唯一的输出,如果方法内有数据库等操作,写入数据库的内容也算一个输出,这个时候可以考虑把数据库的输入反馈到方法返回值当中,总之是要验证指定的输入有指定的输出。

注:调save()保存数据严格来说也不算业务,其只与技术有关,可以进一步从业务逻辑中剥离出来。上面主要是方便说明mock一个对象的例子。

2.3 文档

2.3.1 Mockito

Mockito还是需要学习一下具体的用法的,重点可以先了解如何匹配参数,然后如何指定返回值(含抛异常)等,再高级的用法则可以用到再查找文档。

Mockito各个版本的文档列表:https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html

Mockito-4.5.1的详细文档:https://javadoc.io/doc/org.mockito/mockito-core/4.5.1/org/mockito/Mockito.html

2.3.2 AssertJ

AssertJ则通过方法名称就大概了解其用法了,重点可以了解一下在抛异常的场景如何进行断言。

文档:https://assertj.github.io/doc/

3 架构一小步

规范:把业务规则抽离出来,不依赖数据库和中间件进行详细的单元测试。

规范:编写可测试的代码,如果测试代码有逻辑则反向说明代码可测试性不足。


网站公告

今日签到

点亮在社区的每一天
去签到