Junit 5:与任务跟踪器问题的链接测试
#测试 #java #githubactions #documentation

在本指南中,我告诉您:

  1. 如何将JUnit 5测试与任务跟踪器系统中的问题链接?
  2. 如何自动生成文档?
  3. 如何在GitHub Pages上托管结果文档?

您可以在github by this link上找到包含代码示例的整个存储库。生成的文档也是available here

Article meme cover

问题注释

有一个名为JUnit Pioneer的酷图书馆。这是一个扩展包,其中包括Vanilla Junit缺少的一些功能。这些是cartesian product testsJSON argument parameterized sourceretrying 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文件

文档生成过程包括两个步骤:

  1. 以JSON格式生成文档(因为很容易解析)。
  2. 将信息放入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方法由path build/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方法的行数。

在这里我们执行以下步骤:

  1. 获取测试套件的Class实例。
  2. 初始化ClassPool
  3. 将测试类附加到池
  4. 获取测试方法的行号。

最后,我们将信息块放入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

步骤是:

  1. 设置JDK 17
  2. 建立项目
  3. 设置nodejs
  4. 使用先前显示的JS程序生成文档
  5. 将结果部署到github页面

docsGeneratorScript.sh文件是一个微不足道的bash脚本。在下面查看其定义:

mkdir -p public
touch ./public/index.html
node ./generateDocs.js > ./public/index.html

就是这样!现在,每次有人合并拉动请求时,文档is available并自动更新。

结论

这就是我想告诉您的有关将测试与问题链接并为其生成文档的信息。如果您有疑问或建议,请在下面放下评论。感谢您的阅读!

资源

  1. JUnit 5
  2. GitHub Pages
  3. The repository with source code
  4. The result generated documentation
  5. JUnit Pioneer
  6. Issue annotation from JUnit Pioneer
  7. Hibernate task tracker
  8. Java Service Loader
  9. Javaassist library
  10. Tacit CSS