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时的一些配置,上述代码是用了随机端口,当然也可以预定义端口,像这样
@springboottest(webenvironment = springboottest.webenvironment.defined_port, properties = {"server.port=9990"})
activeprofiles 强制使用了it的profile,从最佳实践上来说it profile所配置的数据库或者其他资源组件的地址,应该是与开发或者staging环境隔离的。因为当一个it跑完之后很多情况下我们需要清除测试数据。
你能够发现这样的case可以使用 autowired 注入任何想要的service。这是因为spring将整个上下文都加载了起来,与实际运行的环境是一样的,包含了数据库,缓存等等组件。如果觉得测试时不需要全部的资源,那么在profile删除对应的配置就可以了。这就是一个完整的运行环境,唯一的区别是当用例跑完会自动shutdown。
测试一个rest api
强烈推荐一个库,加入到gradle中
testcompile 'io.rest-assured:rest-assured:3.0.3'
支持jsonpath,十分好用,具体文档戳 这里
@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,效果是一样的。
返回结果被一个response接住,之后就可以用jsonpath获取其中数据进行验证。当然还有一种更直观的办法,可以通过 response.asstring() 获取完整的response,再反序列化成java对象进行验证。
至此,最基本的it就完成了。 在jenkins增加一个step gradle test 就可以实现每次提交代码都进行一次测试。
一些复杂的情况
数据混杂
这是最容易发生,一个项目有很多dev,每个dev都会写自己的it case,那么如果数据之间产生了影响怎么办。很容易理解,比如一个测试批量写的场景,最后验证方式是看写的数据量是不是10w行。那么另外一个dev写了其他的case恰好也新增了一条数据到这张表,结果变成了10w+1行,那么批量写的case就跑不过了。
为了杜绝这种情况,我们采用每次跑完一个测试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写的,做性能测试的时候会方便很多。
推荐阅读
-
spring boot2.0.4集成druid,用jmeter并发测试工具调用接口,druid查看监控的结果
-
Spring之junit测试集成
-
spring-boot-2.0.3不一样系列之源码篇 - run方法(三)之createApplicationContext,绝对有值得你看的地方
-
详解Spring Boot实战之单元测试
-
spring-boot-2.0.3不一样系列之番外篇 - 自定义session管理,绝对有值得你看的地方
-
SpringBoot系列:Spring Boot集成定时任务Quartz
-
spring-boot-2.0.3之quartz集成,数据源问题,源码探究
-
spring-boot-2.0.3之quartz集成,最佳实践
-
spring-boot-2.0.3不一样系列之番外篇 - springboot事件机制,绝对有值得你看的地方
-
spring-boot-2.0.3不一样系列之源码篇 - SpringApplication的run方法(一)之SpringApplicationRunListener,绝对有值得你看的地方