解決問題
在SpringBoot專案中,如何整合Karate測試框架和Jacoco外掛。以及編寫了feature測試檔案,怎麼樣配置才能看到被測試介面程式碼的覆蓋率。
演示版本及說明
本次講解,基於SpringBoot2.1.4.RELEASE
版本,可根據專案版本靈活更改。下面所有的版本號,可以自行選擇,也可以直接使用下文版本。包括專案目錄,都可以自行建立。
1、整合Karate測試框架,及通用配置包
在SpringBoot專案的pom.xml中,新增以下配置:
<dependencies>
<!-- 引入 Web 功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>com.intuit.karate</groupId>
<artifactId>karate-junit4</artifactId>
<version>1.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.masterthought</groupId>
<artifactId>cucumber-reporting</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
</dependencies>
2、整合Jacoco外掛及配置生成介面覆蓋率檔案
在pom.xml檔案中新增:
<build>
<testResources>
<testResource>
<directory>src/test/java</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</testResource>
</testResources>
</build>
<profiles>
<profile>
<id>coverage</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<includes>
<!-- ** 這個很重要 指定為下面建立的入口啟動類 -->
<include>demo/DemoTestParallel.java</include>
</includes>
<!-- 這裡報紅不用管 -->
<argLine>-Dfile.encoding=UTF-8 ${argLine}</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.9</version>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
3、建立啟動測試和配置的相關類
1、建立基礎啟動類
下面的步驟是示例使用,具體使用的時候結合實際建立。
在src/test/java
中建立test
包,在其中建立下面四個類:
1.1 ServerStart
類:
public class ServerStart {
private static final Logger logger = LoggerFactory.getLogger(ServerStart.class);
private ConfigurableApplicationContext context;
private MonitorThread monitor;
private int port = 0;
public void start(String[] args, boolean wait) throws Exception {
if (wait) {
try {
logger.info("attempting to stop server if it is already running");
new ServerStop().stopServer();
} catch (Exception e) {
logger.info("failed to stop server (was probably not up): {}", e.getMessage());
}
}
// Application 改為自己專案的啟動類
context = Application.run(args);
ServerStartedInitializingBean ss = context.getBean(ServerStartedInitializingBean.class);
port = ss.getLocalPort();
logger.info("started server on port: {}", port);
if (wait) {
int stopPort = port + 1;
logger.info("will use stop port as {}", stopPort);
monitor = new MonitorThread(stopPort, () -> context.close());
monitor.start();
monitor.join();
}
}
public int getPort() {
return port;
}
@Test
public void startServer() throws Exception {
start(new String[]{}, true);
}
}
1.2 ServerStop
類:
public class ServerStop {
@Test
public void stopServer() {
MonitorThread.stop(8081);
}
}
1.3 MonitorThread
類:
public class MonitorThread extends Thread {
private static final Logger logger = LoggerFactory.getLogger(MonitorThread.class);
private Stoppable stoppable;
private ServerSocket socket;
public MonitorThread(int port, Stoppable stoppable) {
this.stoppable = stoppable;
setDaemon(true);
setName("stop-monitor-" + port);
try {
socket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
logger.info("starting thread: {}", getName());
Socket accept;
try {
accept = socket.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream()));
reader.readLine();
logger.info("shutting down thread: {}", getName());
stoppable.stop();
accept.close();
socket.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void stop(int port) {
try {
Socket s = new Socket(InetAddress.getByName("127.0.0.1"), port);
OutputStream out = s.getOutputStream();
logger.info("sending stop request to monitor thread on port: {}", port);
out.write(("\r\n").getBytes());
out.flush();
s.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
1.4 Stoppable
介面:
public interface Stoppable {
void stop() throws Exception;
}
2、建立測試啟動配置類
在專案的src/test/java
下建一個demo
的包,在包中建立下面兩個類:
2.1 TestBase
類:
@RunWith(Karate.class)
public abstract class TestBase {
private static ServerStart server;
public static int startServer() throws Exception {
if (server == null) { // keep spring boot side alive for all tests including package 'mock'
server = new ServerStart();
server.start(new String[]{"--server.port=0"}, false);
}
System.setProperty("demo.server.port", server.getPort() + "");
return server.getPort();
}
@BeforeClass
public static void beforeClass() throws Exception {
startServer();
}
}
2.2 DemoTestParallel
類:
// 該類是測試啟動類,也是配置類,需要將該類配置在pom檔案裡
public class DemoTestParallel {
@BeforeClass
public static void beforeClass() throws Exception {
TestBase.beforeClass();
}
@Test
public void testParallel() {
// 配置想要測試的feature檔案所在目錄,可自行更改
Results results = Runner.path("classpath:demo")
.outputCucumberJson(true)
// 配置測試環境,根據實際修改,也可以不改
.karateEnv("demo")
.parallel(5);
generateReport(results.getReportDir());
assertTrue(results.getErrorMessages(), results.getFailCount() == 0);
}
public static void generateReport(String karateOutputPath) {
Collection<File> jsonFiles = FileUtils.listFiles(new File(karateOutputPath), new String[] {"json"}, true);
List<String> jsonPaths = new ArrayList<>(jsonFiles.size());
jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath()));
Configuration config = new Configuration(new File("target"), "demo");
ReportBuilder reportBuilder = new ReportBuilder(jsonPaths, config);
reportBuilder.generateReports();
}
}
3、建立karate配置檔案
在src/test/java
包下,建立karate-config.js
檔案,這個檔案是為了設定全域性配置資訊的,可以設定全域性uri,port,env等,因為這是示例,所以簡單配置一下uri和port。
function fn() {
var port = '8080';
var config = { demoBaseUrl: 'http://127.0.0.1:' + port };
return config;
}
4、建立服務啟動初始化類
在src/main/java/com/karate/config
下建立一個初始化類,如果目錄沒有,自己建立目錄。
@Component
public class ServerStartedInitializingBean implements ApplicationRunner, ApplicationListener<WebServerInitializedEvent> {
private static final Logger logger = LoggerFactory.getLogger(ServerStartedInitializingBean.class);
private int localPort;
public int getLocalPort() {
return localPort;
}
@Override
public void run(ApplicationArguments aa) throws Exception {
logger.info("server started with args: {}", Arrays.toString(aa.getSourceArgs()));
}
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {
localPort = event.getWebServer().getPort();
logger.info("after runtime init, local server port: {}", localPort);
}
}
4、測試介面並生成覆蓋率檔案
4.1、建立測試介面
在src/main/java/com/karae
中建立controller
包,建立下面的類:
@RestController
public class KarateController {
@GetMapping("/search")
public Map<String, String[]> search(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap == null || parameterMap.size() == 0) {
return null;
}
return request.getParameterMap();
}
}
4.2、建立feature測試檔案
在src/test/java/demo/karate
包中,建立karate_test.feature
檔案
Feature: karate test controller
Background:
* url demoBaseUrl
Scenario: karate test
# create a test
Given path 'search'
And params ({ name: 'Scooby' })
When method get
Then status 200
And match response == { "name": ["Scooby"] }
5、驗證檔案是否缺少
下面的圖片是演示的專案,可以對比一下,有沒有類或檔案沒有建立。
6、啟動命令
歷盡千辛萬苦,終於到了要驗收成果的時候,祝大家好運!
在控制檯中輸入mvn clean test -Pcoverage
。如果使用的是idea
直接在軟體左下方的Terminal
中或者右邊maven
配置中輸入即可。其他軟體,自行找到控制檯,目錄定位到專案下,執行命令即可。命令執行完,在target
目錄下,會有一個site
的資料夾,開啟找到index.html
,到瀏覽器執行即可看到程式碼覆蓋率。
看一下最終的效果截圖:
7、總結
在網上找了很久都沒有很好的解答,所以自己摸索並記錄下來,也希望能幫到更多的人,一起進步!
本次演示,完成了SpringBoot整合Karate測試框架和Jacoco外掛。從零開始,一步步實現了對介面程式碼的測試,以及最終生成被測試程式碼的覆蓋率。演示基於Karate的官網教程,還是比較規範的引入。
後續會繼續寫一些Karate的語法和場景示例,解決實際專案中測試遇到的問題,例如如何模擬資料庫方法或者呼叫其他伺服器介面的方法。