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 = "[email protected]";
    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增加一個stepgradle 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寫的,做性能測試的時候會方便很多。

仍在尋找合適的跑性能的工具,如有推薦歡迎留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章