解决问题
在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(("rn").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的语法和场景示例,解决实际项目中测试遇到的问题,例如如何模拟数据库方法或者调用其他服务器接口的方法。