如果希望很方便针对API进行测试,并且方便的集成到CI中验证每次的提交,那么spring boot自带的IT绝对是不二选择。
迅速编写一个测试Case
1
2
3
4
5
6
7
8
9
10
11
|
@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时的一些配置,上述代码是用了随机端口,当然也可以预定义端口,像这样
1
|
@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中
1
|
testCompile 'io.rest-assured:rest-assured:3.0.3'
|
支持JsonPath,十分好用,具体文档戳 这里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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就将数据清空。既然是基于类的操作,可以写一个基类解决。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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写的,做性能测试的时候会方便很多。
原文链接:http://www.deanwangpro.com/2018/03/22/spring-boot-integration-test/