spring-boot系列之集成测试

若是但愿很方便针对API进行测试,而且方便的集成到CI中验证每次的提交,那么spring boot自带的IT绝对是不二选择。前端

迅速编写一个测试Case

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles({Profiles.ENV_IT})
public class DemoIntegrationTest {

    @Autowired
    private FooService fooService;

    @Test
    public void test() {
        System.out.println("tested");
    }

}

其中SpringBootTest定义了跑IT时的一些配置,上述代码是用了随机端口,固然也能够预约义端口,像这样java

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = {"server.port=9990"})

ActiveProfiles强制使用了IT的Profile,从最佳实践上来讲IT Profile所配置的数据库或者其余资源组件的地址,应该是与开发或者Staging环境隔离的。由于当一个IT跑完以后不少状况下咱们须要清除测试数据。git

你可以发现这样的Case可使用Autowired注入任何想要的Service。这是由于spring将整个上下文都加载了起来,与实际运行的环境是同样的,包含了数据库,缓存等等组件。若是以为测试时不须要所有的资源,那么在profile删除对应的配置就能够了。这就是一个完整的运行环境,惟一的区别是当用例跑完会自动shutdown。github

测试一个Rest API

强烈推荐一个库,加入到gradle中web

testCompile 'io.rest-assured:rest-assured:3.0.3'

支持JsonPath,十分好用,具体文档戳这里spring

@Sql(scripts = "/testdata/users.sql")
@Test
public void test001Login() {
    String username = "demo@demo.com";
    String password = "demo";

    JwtAuthenticationRequest request = new JwtAuthenticationRequest(username, password);

    Response response = given().contentType(ContentType.JSON).body(request)
            .when().post("/auth/login").then()
            .statusCode(HttpStatus.OK.value())
            .extract()
            .response();

    assertThat(response.path("token"), is(IsNull.notNullValue()));
    assertThat(response.path("expiration"), is(IsNull.notNullValue()));
}

@Sql用于在测试前执行sql插入测试数据。注意given().body()中传入的是一个java对象JwtAuthenticationRequest,由于rest-assured会自动帮你用jackson将对象序列化成json字符串。固然也能够将转换好的json放到body,效果是同样的。sql

返回结果被一个Response接住,以后就能够用JsonPath获取其中数据进行验证。固然还有一种更直观的办法,能够经过response.asString()获取完整的response,再反序列化成java对象进行验证。数据库

至此,最基本的IT就完成了。 在Jenkins增长一个stepgradle test就能够实现每次提交代码都进行一次测试。json

一些复杂的状况

数据混杂

这是最容易发生,一个项目有不少dev,每一个dev都会写本身的IT case,那么若是数据之间产生了影响怎么办。很容易理解,好比一个测试批量写的场景,最后验证方式是看写的数据量是否是10w行。那么另一个dev写了其余的case刚好也新增了一条数据到这张表,结果变成了10w+1行,那么批量写的case就跑不过了。api

为了杜绝这种状况,咱们采用每次跑完一个测试Class就将数据清空。既然是基于类的操做,能够写一个基类解决。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles({Profiles.ENV_IT})
public abstract class BaseIntegrationTest {

    private static JdbcTemplate jdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    @Value("${local.server.port}")
    protected int port;

    @Before
    public void setupEnv() {
        RestAssured.port = port;
        RestAssured.basePath = "/api";
        RestAssured.baseURI = "http://localhost";
        RestAssured.config = RestAssured.config().httpClient(HttpClientConfig.httpClientConfig().httpMultipartMode(HttpMultipartMode.BROWSER_COMPATIBLE));
    }

    public void tearDownEnv() {
        given().contentType(ContentType.JSON)
                .when().post("/auth/logout");
    }

    @AfterClass
    public static void cleanDB() throws SQLException {
        Resource resource = new ClassPathResource("/testdata/CleanDB.sql");
        Connection connection = jdbcTemplate.getDataSource().getConnection();
        ScriptUtils.executeSqlScript(connection, resource);
        connection.close();
    }

}

@AfterClass中使用了jdbcTemplate执行了一个CleanDB.sql,经过这种方式清除全部测试数据。

@Value("${local.server.port}")也要提一下,由于端口是随机的,那么Rest-Assured不知道请求要发到losthost的哪一个端口上,这里使用@Value获取当前的端口号并设置到RestAssured.port就解决了这个问题。

共有数据怎么处理

跑一次完整的IT,可能须要经历数十个Class,数百个method,那么若是一些数据是全部case都须要的,只有在全部case都跑完才须要清除怎么办?换句话说,这种数据清理不是基于的,而是基于一次运行。好比初始用户数据,城市库等等

咱们耍了个小聪明,借助了flyway

@Configuration
@ConditionalOnClass({DataSource.class})
public class UpgradeAutoConfiguration {

    public static final String FLYWAY = "flyway";

    @Bean(name = FLYWAY)
    @Profile({ENV_IT})
    public UpgradeService cleanAndUpgradeService(DataSource dataSource) {
        UpgradeService upgradeService = new FlywayUpgradeService(dataSource);
        try {
            upgradeService.cleanAndUpgrade();
        } catch (Exception ex) {
            LOGGER.error("Flyway failed!", ex);
        }
        return upgradeService;
    }

}

能够看到当Profile是IT的状况下,flyway会drop掉全部表并从新依次执行每次的upgrade脚本,由此建立完整的数据表,固然都是空的。在项目的test路径下,增长一个版本极大的sql,这样就可让flyway在最后插入共用的测试数据,例如src/test/resources/db/migration/V999.0.1__Insert_Users.sql ,完美的解决各类数据问题。

小结

用Spring boot内置的测试服务能够很快速的验证API,我如今都不用把服务启动再经过人工页面点击来测试本身的API,直接与前端同事沟通好Request的格式,写个Case就能够验证。

固然这种方式也有一个不足就是不方便对系统进行压力测试,以前在公司的API测试用例都是Jmeter写的,作性能测试的时候会方便不少。

仍在寻找合适的跑性能的工具,若有推荐欢迎留言。