在本指南中,我告诉您:
- 如何将JUnit 5测试与任务跟踪器系统中的问题链接?
- 如何自动生成文档?
- 如何在GitHub Pages上托管结果文档?
您可以在github by this link上找到包含代码示例的整个存储库。生成的文档也是available here。
问题注释
有一个名为JUnit Pioneer的酷图书馆。这是一个扩展包,其中包括Vanilla Junit缺少的一些功能。这些是cartesian product tests,JSON argument parameterized source,retrying tests等。但是我对Issue annotation特别感兴趣。查看下面的代码示例:
class TestExample {
@Test
@Issue("HHH-16417")
void testSum() {
...
}
@Test
@Issue("HHH-10000")
void testSub() {
...
}
}
我将实际任务ID放在Hibernate task tracker中,以使结果文档更简洁。
您可以看到,我们可以将@Issue
注释与与测试相关联的任务ID添加。因此,每次您注意到测试失败时,都会知道自己损坏了。
设置服务加载程序
Junit Pioneer提供了一个API,该API允许获取有关用@Issue
注释标记的测试的信息。这意味着我们可以将信息结合在HTML报告中,并与其他团队成员共享。例如,质量保证工程师可能会发现它有益。因为现在他们知道该项目包含哪些测试以及检查哪些错误。
有一个特殊的接口IssueProcessor
。它的实施就像回调一样。查看下面的代码段:
public class SimpleIssueProcessor implements IssueProcessor {
@Override
public void processTestResults(List<IssueTestSuite> issueTestSuites) {
...
}
}
但是,我们还需要将SimpleIssueProcessor
设置为Java Service Loader。否则,Junit Runner不会注册它。在src/test/resources/META-INF/services
目录中创建一个带有org.junitpioneer.jupiter.IssueProcessor
名称的新文件。它必须包含一行,并具有完全合格的实现名称(在我们的情况下,SimpleIssueProcessor
)。查看下面的代码段:
org.example.SimpleIssueProcessor
此外,还有另一个需要注册的服务加载程序。这是Junit Pioneer Library提供的一个,它具有解析信息的复杂逻辑并将控制权委派给IssueProcessor
实施。在同一目录中创建一个带有org.junit.platform.launcher.TestExecutionListener
名称的新文件。查看下面所需的文件内容:
org.junitpioneer.jupiter.issue.IssueExtensionExecutionListener
现在我们准备好了。您可以将println
语句放在IssueProcessor
实现中,以检查框架在执行后是否调用它。
创建元信息JSON文件
文档生成过程包括两个步骤:
- 以JSON格式生成文档(因为很容易解析)。
- 将信息放入HTML模板中。
查看下面的SimpleIssueProcessor
代码:
public class SimpleIssueProcessor implements IssueProcessor {
@Override
@SneakyThrows
public void processTestResults(List<IssueTestSuite> issueTestSuites) {
writeFileToBuildFolder(
"test-issues-info.json",
new ObjectMapper().writeValueAsString(
issueTestSuites.stream()
.map(issueTestSuite -> Map.of(
"issueId", issueTestSuite.issueId(),
"tests", issueTestSuite.tests()
.stream()
.map(test -> parseTestId(test.testId()))
.toList()
))
.toList()
)
);
}
...
}
writeToBuildFolder
方法由pathbuild/classes/java/test/test-issues-info.json
创建一个文件。我使用Gradle,但是如果您更喜欢Maven,您的路径会有所不同。您可以通过this link查看功能的源代码。
结果JSON是一个数组。查看下面生成的示例:
[
{
"tests": [
{
"testId": "TestExample.testSum",
"urlPath": "org/example/TestExample.java#L12"
}
],
"issueId": "HHH-16417"
},
{
"tests": [
{
"testId": "TestExample.testSub",
"urlPath": "org/example/TestExample.java#L18"
}
],
"issueId": "HHH-10000"
}
]
有一个问题ID和一组参考它的测试(从理论上讲,可能有几个指向相同问题的测试)。
现在,我们需要从提供的List<IssueTestSuite>
中解析所需的信息。查看下面的parseTestId
功能。
@SneakyThrows
private static Map<String, Object> parseTestId(String testId) {
final var split = testId.split("/");
final var className = split[1].substring(7, split[1].length() - 1);
final var method = split[2].substring(8, split[2].length() - 1).replaceAll("\\(.*\\)", "");
final Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
final var classPool = ClassPool.getDefault();
classPool.appendClassPath(new ClassClassPath(clazz));
final var methodLineNumber = classPool.get(className)
.getDeclaredMethod(method)
.getMethodInfo()
.getLineNumber(0);
return Map.of(
"testId", lastArrayElement(className.split("\\.")) + "." + method,
"urlPath", className.replace(".", "/") + ".java#L" + methodLineNumber
);
}
让我们逐步解构此代码片段。
库将testId
列为下面的字符串模式:
// [engine:junit-jupiter]/[class:org.example.TestExample]/[method:testSum()]
首先,我们获得了完全合格的班级名称和方法名称。查看下面的代码:
final var split = testId.split("/");
// [class:org.example.TestExample] => org.example.TestExample
final var className = split[1].substring(7, split[1].length() - 1);
// [method:testSum()] => testSum
final var method = split[2].substring(8, split[2].length() - 1).replaceAll("\\(.*\\)", "");
之后,我们确定测试方法的行号。将指向特定代码行的链接设置为有用。查看下面的片段:
// Load test class
final Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
final var classPool = ClassPool.getDefault();
classPool.appendClassPath(new ClassClassPath(clazz));
final var methodLineNumber = classPool.get(className)
.getDeclaredMethod(method)
.getMethodInfo()
.getLineNumber(0);
ClassPool
来自Javaassist库。它提供了方便的API来检索Java方法的行数。
在这里我们执行以下步骤:
- 获取测试套件的
Class
实例。 - 初始化
ClassPool
。 - 将测试类附加到池
- 获取测试方法的行号。
最后,我们将信息块放入java.util.Map
中,最终将其转换为JSON。查看下面的最后一块代码:
return Map.of(
// TestExample.testSum
"testId", lastArrayElement(className.split("\\.")) + "." + method,
// org/example/TestExample.java#L11
"urlPath", className.replace(".", "/") + ".java#L" + methodLineNumber
);
testId
属性只是一个简单的类名称和测试方法名称的组合。而urlPath
是GitHub上指向我们声明测试的特定行的链接的一部分。
生成文档
最后,现在该将生成的JSON构成一个很好的HTML页面。查看下面的整个片段。然后我向您解释每个部分。
const fs = require('fs');
function renderIssues(issuesInfo) {
issuesInfo.sort((issueLeft, issueRight) => {
const parseIssueId = issue => Number.parseInt(issue.issueId.split("-")[1])
return parseIssueId(issueRight) - parseIssueId(issueLeft);
})
return `
<table>
<tr>
<th>Issue</th>
<th>Test</th>
</tr>
${issuesInfo.flatMap(issue => issue.tests.map(test => `
<tr>
<td>
<a target="_blank" href="https://hibernate.atlassian.net/browse/${issue.issueId}">${issue.issueId}</a>
</td>
<td>
<a target="_blank" href="https://github.com/SimonHarmonicMinor/junit-pioneer-issue-doc-generation-example/blob/master/src/test/java/${test.urlPath}">${test.testId}</a>
</td>
</tr>
`)).join('')}
</table>
`
}
console.log(`
<!DOCTYPE html>
<html lang="en">
<head>
<title>List of tests validation particular issues</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/yegor256/tacit@gh-pages/tacit-css-1.5.5.min.css"/>
</head>
<body>
<h1>List of tests validation particular issues</h1>
<h3>Click on issue ID to open it in separate tab. Click on test to open its declaration in separate tab.</h3>
${renderIssues(JSON.parse(fs.readFileSync('./build/classes/java/test/test-issues-info.json', 'utf8')))}
</body>
</html>
`)
我正在使用javascript和nodejs运行时环境。
renderIssues
函数可以完成整个工作。让我们逐步解构它。
function renderIssues(issuesInfo) {
issuesInfo.sort((issueLeft, issueRight) => {
const parseIssueId = issue => Number.parseInt(issue.issueId.split("-")[1])
return parseIssueId(issueRight) - parseIssueId(issueLeft);
})
...
}
issuesInfo
是我们先前使用IssueProcessor
生成的数组。因此,每个元素都有issueId
和属于它的测试。
只要每个问题ID具有MMM-123
的格式,我们就可以按数字对其进行排序。在这种情况下,我们会以降序排序的问题。
查看以下功能的其余部分:
const issueBaseUrl = "https://hibernate.atlassian.net/browse/";
const repoBaseUrl = "https://github.com/SimonHarmonicMinor/junit-pioneer-issue-doc-generation-example/blob/master/src/test/java/"
return `
<table>
<tr>
<th>Issue</th>
<th>Test</th>
</tr>
${issuesInfo.flatMap(issue => issue.tests.map(test => `
<tr>
<td>
<a target="_blank" href="${issueBaseUrl}${issue.issueId}">${issue.issueId}</a>
</td>
<td>
<a target="_blank" href="${repoBaseUrl}${test.urlPath}">${test.testId}</a>
</td>
</tr>
`)).join('')}
</table>
`
问题和测试转换为表行的每个当前组合。另外,这些片段只是纯文本,但链接。您可以通过单击它打开问题描述和测试声明。很酷,不是吗?
然后是输出。查看以下脚本的最后一部分:
console.log(`
<!DOCTYPE html>
<html lang="en">
<head>
<title>List of tests validation particular issues</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/yegor256/tacit@gh-pages/tacit-css-1.5.5.min.css"/>
</head>
<body>
<h1>List of tests validation particular issues</h1>
<h3>Click on issue ID to open it in separate tab. Click on test to open its declaration in separate tab.</h3>
${renderIssues(JSON.parse(
fs.readFileSync('./build/classes/java/test/test-issues-info.json',
'utf8')))}
</body>
</html>
`)
我将输出写入控制台,因为以后我将其重定向到文件。
样式表称为Tacit CSS。这是自动应用的一组CSS规则。如果您需要格式化HTML页面,但不想处理复杂的布局,那是一个完美的解决方案。
这个想法是将生成的HTML表放入预定义的模板中。
设置github页面
直到您可以检查文档。因此,让我们在Github页面上主持它。查看下面的管道:
name: Java CI with Gradle
on:
push:
branches: [ "master" ]
permissions:
contents: read
pages: write
id-token: write
jobs:
build-and-deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew build
- name: Set up NodeJS
uses: actions/setup-node@v3
with:
node-version: 16
- name: Run docs generator
run: ./docsGeneratorScript.sh
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: public/
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
步骤是:
- 设置JDK 17
- 建立项目
- 设置nodejs
- 使用先前显示的JS程序生成文档
- 将结果部署到github页面
docsGeneratorScript.sh
文件是一个微不足道的bash脚本。在下面查看其定义:
mkdir -p public
touch ./public/index.html
node ./generateDocs.js > ./public/index.html
就是这样!现在,每次有人合并拉动请求时,文档is available并自动更新。
结论
这就是我想告诉您的有关将测试与问题链接并为其生成文档的信息。如果您有疑问或建议,请在下面放下评论。感谢您的阅读!