为什么要 Java 集成测试呢?

为什么要 Java 集成测试呢?

何为集成测试?

首先我们先了解什么是集成测试?

集成测试,也叫组装测试联合测试。在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。 ——百度百科

举个🌰,譬如下面这些测试属于 集成测试

  • 数据库有交互
  • 进行了 网络间通信
  • 调用了 文件系统
  • 需要你对环境作特定的准备(如编辑配置文件)才能运行的

简而言之,强依赖外部环境的测试,绝大部分是集成测试。

唠叨

记得 刚刚开始接触测试的时候,不以为然,觉得编写测试代码浪费时间,浪费精力。是一件吃力不讨好的事情。

但是只有自己实践起来,才知道测试的 "好"

好的测试,往往可以大大减少接口返工次数,bug 数量。

有了测试,我们可以放心的重构我们的代码,再也不用担心,改出问题了(引入新的 bug)

不信?

不如看我操作一波,感受一下 Java 集成测试的魅力

Login 例子

还是老样子,以 Login 接口为例。

Controller

@RestController
public class UserController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/login")
    public BaseResponse<Boolean> login(@RequestBody LoginCommand command) {
        return BaseResponse.OK(loginService.login(command));
    }
}

Service

@Service
public class LoginService {

    @Autowired
    private UserRepository userRepository;

    public boolean login(LoginCommand command) {
        Optional<User> userOptional = userRepository.findByUsernameAndPassword(command.getUsername(), command.getPassword());
        return userOptional.isPresent();
    }
}

LoginRepository 使用的是 Jpa,这里就不多赘述了。

三部曲

我们在 test/java/xxx(包名)/ 下,新建一个 LoginITest 测试类,如下所示:

LoginItTest,其中 It 为 Integration Test 简写,集成测试

代码

public class LoginITest {

    @Test
    public void should_return_login_response_when_request_login_interface() {
        // give

        // when
        
        // then
    }
}

方法名,这里是参照 TDD命名方式,是用下划线命名。详细描述 该方法是测什么

这里使用 测试三部曲模板

  • give:给定参数,准备条件
  • when:当执行 xxxMethod 方法时
  • then:期望的结果(断言)。

Give、When、Then 可以使用 Live Templates 快速生成。

然后基于 三部曲,编写测试代码:

测试登录成功

@SpringBootTest
@AutoConfigureMockMvc
public class LoginITest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper mapper;

    @Test
    public void should_return_login_success_when_request_login_interface() {
        // give

        // 数据库增加该记录
        LoginCommand command = new LoginCommand();
        command.setUsername("张三");
        command.setPassword("123456"); 

        // when
        BaseResponse<Boolean> baseResponse = null;

        try {
            																								  // Post 请求,并传入 请求 url
            MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/login")
                    .contentType(MediaType.APPLICATION_JSON)   				// 设置请求类型 
                    .content(mapper.writeValueAsString(command)); 		// 设置入参,通过 mapper,把 入参 LoginCommand 转换为 String类型

            String response = this.mockMvc.perform(builder)
                    .andExpect(MockMvcResultMatchers.status().isOk()) //设置期望返回状态码,isOk为 200
                    .andDo(MockMvcResultHandlers.print()) // 打印 请求日志
                    .andReturn()
                    .getResponse()
                    .getContentAsString();

            // 把 String 结果,转化为具体 BaseResponse 对象。
            baseResponse = this.mapper.readValue(response, new TypeReference<BaseResponse<Boolean>>() {
            });

        } catch (Exception e) {
            e.printStackTrace();
        }

        // then
        assertNotNull(baseResponse);
        assertTrue(baseResponse.getData());
    }
}

在测试类中,引入注解:

  • @SpringBootTest: 配置 SpringBoot 启动所需的上下文环境

  • @AutoConfigureMockMvc : 自动装配 MockMvc

以上是一个完整的测试请求流程,具体参数解释,如上所示

有了请求结果,我们就可以进行 断言,即 判断结果是否符合预期值

// then
assertNotNull(baseResponse); 	     // 返回结果不为空
assertTrue(baseResponse.getData());	// 返回 Data 为 True,登录成功

最后运行测试,测试通过。

看到这里的朋友可能会吐槽到:就这???

这么麻烦,还需要写这么一大堆测试代码,值得吗?

至于测试代码

  • 代码这方面,我们可以优化的嘛,如抽取一个通用的基类,就可减少请求流程代码了。亦或者抽取通用的 common-test 模式进行复用

至于值得吗? 我们对比以前两种做法:

  1. 比起我们写完接口,直接扔给前端,进行联调测试

    • 这样依赖前端进行联调,还需要手动操作,点开页面进行接口测试
    • 那如果接口前端没有对上咧?
  2. 亦或者 直接用 Postman/Swagger ,点点点测试

    • 通过 Postman 视乎可以达到测试接口效果,但是往往不是那么高效,入参不能很好模拟,需要手动输入。

    • 入参代码可以复用,但是需要基于 Postman 进行编程,视乎没有 Java 集成测试那么贴切。

    • 数据库无法隔离,会产生很多测试数据。

而编写集成测试:

  • 使用 Java 语言,SpringBoot 自带 Test 测试依赖。
  • 可以引入 H2 数据库,进行数据隔离,可重复运行。
  • 可以进行代码复用,抽取属于自己的 common-base 模块
  • 自己写的接口,自己测,不依赖别人!

这样看下来,你觉得值得吗?

重构

@SpringBootTest
@AutoConfigureMockMvc
public class BaseITest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper mapper;

    public <T> BaseResponse<T> request(String url, Object object, TypeReference<BaseResponse<T>> typeReference) {

        BaseResponse<T> baseResponse = null;

        try {
            MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post(url)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(mapper.writeValueAsString(object));

            String response = this.mockMvc.perform(builder)
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print())
                    .andReturn()
                    .getResponse()
                    .getContentAsString();

            baseResponse = this.mapper.readValue(response, typeReference);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return baseResponse;
    }
}

首先我们先抽取 通用基类,BaseITest类,把 请求方法 移至 BaseITest 类中,如上所示

然后我们在 LoginITest 中调用变成:

@Test
public void should_return_login_response_when_request_login_interface() {
     // give
     LoginCommand command = new LoginCommand();
     command.setUsername("张三");
     command.setPassword("123456");

     // when
     BaseResponse<Boolean> baseResponse = request("/login", command, new TypeReference<BaseResponse<Boolean>>() {
     });

     // then
     assertNotNull(baseResponse);
     assertTrue(baseResponse.getData());
}

传入请求的 url参数返回结果类型,即可获取 返回结果,BaseResposne

重构后,运行测试,测试正常通过。

然后我可以继续下一个测试用例, 测试登录失败的情况。

测试登录失败

@Test
public void should_return_login_fail_when_request_login_interface() {
    // give
    LoginCommand command = new LoginCommand();
    command.setUsername("张三");
    command.setPassword("123"); // 测试输入错误密码

    // when
    BaseResponse<Boolean> baseResponse = request("/login", command, new TypeReference<BaseResponse<Boolean>>() {
    });

    // then
    assertNotNull(baseResponse);
    assertFalse(baseResponse.getData());  // 返回 Data 为 False,登录失败
}

运行测试,测试通过,相信看到这里的朋友,应该都看得懂吧。

依赖

测试视乎进展的很顺利。

但是这里会隐藏了一个 严重的问题

如果我把数据库 张三这条数据 删了,或者修改这条数据的 账号或密码。

那岂不是 我们的测试 就 不通过了 ?

每次测试,都需要去数据库手动添加一条记录?

那我们测试 就变得很脆弱,强依赖数据库。

所以这里,我们还需要引入 一个 内存数据库 (H2)

H2

好处

  • 隔离我们真实的数据库:内存数据库,就是数据库文件存在于内存中,没有持久化,当应用进程关闭时数据库与数据表会消失。所以我们可以在 H2 中,增删改查,都不会影响到 我们 真实的数据库,这样大大方便我们进行测试。
  • H2 配置上 MySQL 模式,几乎可以 “完美” 兼容MySQL 语法(但是会有一点点小区别)

引入依赖

<dependency>
     <groupId>com.h2database</groupId>
     <artifactId>h2</artifactId>
</dependency>

并且在 /test/resources 包下 建立一个 测试环境的 yml ,可以在这里配置 h2 相关配置。

spring:
  datasource:
    name: database
    platform: h2
    driver-class-name: org.h2.Driver
    # 配置 MODE=MySQL 模式
    url: jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;
    username: sa  # 默认为 sa
    password:     # 默认为空
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update   # Jpa默认为 update,没有表新建,有表更新操作。

Domain 配置上 @Entity,H2 数据库可以基于 Jpa 自动创建表

代码

接下来对测试代码,进行修改,由于是 内存数据库,所以每次启动都是 一个 新的数据库,所以我们需要在测试中构造 一个 测试账号。

@Test
public void should_return_login_success_when_request_login_interface() {
    // give
    // 构建一个测试账号User
    User user = new User();
    user.setUsername("李四");
    user.setPassword("1234");
    userRepository.save(user);

    LoginCommand command = new LoginCommand();
    command.setUsername("李四");
    command.setPassword("1234");

    // when
    BaseResponse<Boolean> baseResponse = request("/login", command, new TypeReference<BaseResponse<Boolean>>() {
    });

    // then
    assertNotNull(baseResponse);
    assertTrue(baseResponse.getData());
}

运行测试,测试通过。

扩展

后续可能会有 注册相关等测试需要使用到 User,所以可以把 创建 User 抽取一个 UserFixture ,代码如下所示

UserFixture,User 的 夹具,参照 DDD 架构模式。

public class UserFixture {
    public static User create() {
        User user = new User();
        user.setUsername("李四");
        user.setPassword("1234");
        return user;
    }
}

// 使用
User user = UserFixture.create();
userRepository.save(user);

总结

在我们编写完接口后,我们不妨编写对应的接口测试,以此来保证接口质量。

在编写测试时,应该遵循 三部曲,划分好 入参测试方法断言,对应的是 Give、When、Then

在编写集成测试时,应当采用 H2数据库,避免测试数据污染 真实数据库。

在编写测试代码时,切勿忽视测试代码,测试代码也需要重构,优化,编写出优雅的测试代码,利于我们理解测试,不然都不知道再测啥 :)

最后

以上就是全部内容,希望能帮助到你~
如有不妥,欢迎指出,大家一起交流学习。

感谢阅读,下次再见。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值