從零入門專案整合Karate和Jacoco,配置測試程式碼覆蓋率

煜航發表於2022-12-15

解決問題

在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,到瀏覽器執行即可看到程式碼覆蓋率。
看一下最終的效果截圖:
target目錄
程式碼覆蓋率1
程式碼覆蓋率2

7、總結

在網上找了很久都沒有很好的解答,所以自己摸索並記錄下來,也希望能幫到更多的人,一起進步!
本次演示,完成了SpringBoot整合Karate測試框架和Jacoco外掛。從零開始,一步步實現了對介面程式碼的測試,以及最終生成被測試程式碼的覆蓋率。演示基於Karate的官網教程,還是比較規範的引入。
後續會繼續寫一些Karate的語法和場景示例,解決實際專案中測試遇到的問題,例如如何模擬資料庫方法或者呼叫其他伺服器介面的方法。

相關文章