2天前  Java系列 |   抢沙发  4 
文章评分 0 次,平均分 0.0

当你需要二进制数据来编写单元测试时,你会怎么做?

假设你正在开发一个纯Java Git实现。您将需要组成Git存储库的位:blob、树或提交对象。或者,您可能想了解Java类文件中的内容。在这种情况下,您将需要Java类中的数据。

一种解决方案是使用常规文件。如果您使用的是Maven,请将它们放在src/test/resources目录中。然后,您可以使用例如Class.getResourceAsStream方法访问它们的内容。

文件解决方案运行良好。但是,如果出现以下情况,则有另一种选择:

  • 您正在使用JDK 17或更高版本;
  • 您的测试数据相对较小。

它涉及使用两个功能:

  • JDK 17中引入的java.util.HexFormat类;和
  • 文本块,作为JDK 15中的永久功能提供。

在这篇文章中,我将展示如何将它们一起用作二进制数据源。

获取二进制数据的文本表示

假设我们正在用Java编写一个Git blob(松散)对象读取器。简单地说,Git blob对象在Git下存储单个文件的内容。Git内部的更多细节超出了本文的范围。如果你想了解更多,可以参考Pro Git(https://git-scm.com/book/en/v2/Git-Internals-Git-Objects)书中关于Git内部的章节。

为了测试我们的代码,我们需要数据。让我们快速创建一个测试存储库:

$ git init test
$ cd test/
$ cat > README.md <<EOF
# Our test project

This will be our test blob.
Let's see if we can read it from our test.
EOF
$ git add README.md
$ git commit -m "Add README.md file"
$ find .git/objects/ -type f
.git/objects/fe/210da9f7dc83fefa49ef54ba73f74e55e453e6
.git/objects/75/a8b365ca1a5e731f49d3624960b314d0480ca3
.git/objects/18/acf6a96e6e43829c703ec8a8b6092b98829422
$ git cat-file -p 75a8b365ca1a5e731f49d3624960b314d0480ca3
# Our test project

This will be our test blob.
Let's see if we can read it from our test.

因此,Git计算出的README文件内容的哈希值为:

75a8b365ca1a5e731f49d3624960b314d0480ca3

让我们使用xxd工具获取它的十六进制转储:

$ xxd -plain .git/objects/75/a8b365ca1a5e731f49d3624960b314d0480ca3
78013dcb310a80300c4661e79ee20707b782a377105cbc40532356aa9126
d2ebeba2f3fb1e65210c7dd362ba0b8cd57015d9399a73f3961435e50c62
c897e93dbc1bd93a853223ada88c184e140e0b92612d72fcdebb07525820
c6

太棒了我们现在有了二进制数据的文本表示。

使用文本块存储我们的十六进制转储

文本块是适用于多行字符串的java.lang.String文本。即使xxd工具的输出是多行的,行终止符也不是实际数据的一部分。因此,在使用它之前,我们必须去掉这些字符的字符串。为此,我可以想到两个选择:

  1. 在使用字符串之前,执行replaceAll(System.lineSeparator(),“”);
  2. 使用\<line-terminator>转义序列。

这两种选择都很好。在这篇文章中,我们将使用后者:

public class BlobReaderTest {
  private static final String README = """
      78013dcb310a80300c4661e79ee20707b782a377105cbc40532356aa9126\
      d2ebeba2f3fb1e65210c7dd362ba0b8cd57015d9399a73f3961435e50c62\
      c897e93dbc1bd93a853223ada88c184e140e0b92612d72fcdebb07525820\
      c6\
      """;
}

请注意,文本块的每一行都以\(反斜杠)字符结尾。它告诉Java编译器从结果字符串值中取消行终止符。

不错。我们现在可以在Java源代码中获得blob数据。

使用java.util.HexFormat获取我们的字节数组

HexFormat在字节和字符以及十六进制编码的字符串之间进行转换,其中可能包括额外的格式化标记,如前缀、后缀和分隔符。

在我们的例子中,我们想从十六进制编码的字符串转换为字节数组。使用HexFormat类转换xxd工具提供的输出很简单:

@Test
public void readme() {
  var hexFormat = HexFormat.of();

  byte[] bytes = hexFormat.parseHex(README);

  // consume bytes
}

我们首先获得了HexFormat类的一个实例。我们使用了适用于xxd输出的of()工厂。

接下来,我们使用上一节的README字符串调用parseHex方法。它以字节[]的形式返回blob数据。

太棒了我们现在已经准备好使用我们的数据并测试blob读取器。

消费二进制数据

我们如何使用数据取决于我们正在测试的API。假设我们的BlobReader提供了一个读取方法,该方法接受java.io.InputStream,如下所示:

Blob readInputStream(InputStream inputStream) throws IOException;

在这种情况下,我们需要将字节数组包装在ByteArrayInputStream中。测试的完整版本如下:

@Test
public void readme() throws IOException {
  var hexFormat = HexFormat.of();

  var bytes = hexFormat.parseHex(README);

  try (var inputStream = new ByteArrayInputStream(bytes)) {
    var reader = new BlobReader();

    var blob = reader.readInputStream(inputStream);

    assertEquals(
      blob.text(),

      """
      # Our test project

      This will be our test blob.
      Let's see if we can read it from our test.
      """
    );
  }
}

ByteArrayInputStream是一个内存中的InputStream。我的意思是它不做任何实际的I/O。换句话说,它的read和close方法都不会突然返回IOException。无论如何,我们使用try with resources语句。

接下来,我们创建BlobReader实例,并使用InputStream调用它。

最后,我们验证blob内容是否与预期值匹配。

将数据写入临时文件

有时您无法控制您正在测试或在测试中使用的API。假设我们的blob读取器不提供接受InputStream的方法。相反,它需要一个文件。尽管如此,还是有一个java.io.File

Blob readFile(File file) throws IOException;

在调用我们正在测试的方法之前,我们必须将数据写入临时文件。测试的完整版本如下:

@Test
public void readmeWithFile() throws IOException {
  var hexFormat = HexFormat.of();

  var bytes = hexFormat.parseHex(README);

  var file = File.createTempFile("blob-", ".tmp");

  file.deleteOnExit();

  try (var out = new FileOutputStream(file)) {
    out.write(bytes);
  }

  var reader = new BlobReader();

  var blob = reader.readFile(file);

  assertEquals(
    blob.text(),

    """
    # Our test project

    This will be our test blob.
    Let's see if we can read it from our test.
    """
  );
}

我们使用file.createTempFile静态方法创建一个临时文件。我们立即调用deleteOnExit方法:我们希望在完成测试后删除文件。

接下来,我们通过FileOutputStream将字节写入文件。

最后,我们使用BlobReader读取文件,并验证返回的blob是否具有预期的内容。

手动编辑我们的数据

我们的测试数据是Java源代码。因此,如果需要,我们可以手动编辑数据。当然,您也可以编辑二进制文件。但我发现文本文件更容易编辑;可以直接在Java编辑器中完成。

让我们把它付诸实践。我们将修改blob十六进制转储,以便编辑README内容。

在Git中,松散对象使用DEFLATE压缩。所以我们可以得到未压缩的十六进制转储,如下所示:

$ zlib-flate -uncompress \
    < .git/objects/75/a8b365ca1a5e731f49d3624960b314d0480ca3 \
    | xxd -plain
626c6f622039310023204f757220746573742070726f6a6563740a0a5468
69732077696c6c206265206f7572207465737420626c6f622e0a4c657427
73207365652069662077652063616e20726561642069742066726f6d206f
757220746573742e0a

以下列表是对未压缩数据的解释。要理解它,你应该知道:

  • 每两个字符代表一个字节
  • Git blob(松散)对象具有以下格式:blob{ascii中的大小}\0{内容}
  • 它有助于手头有一个ASCII表
626c6f62 -- 'blob' in ASCII/UTF-8
20       -- SPACE
3931     -- object size in ASCII/UTF-8. size=91 bytes
00       -- NULL
23       -- first char of the contents: c='#'
-- rest of the contents
204f757220746573742070726f6a6563740a0a5468
69732077696c6c206265206f7572207465737420626c6f622e0a4c657427
73207365652069662077652063616e20726561642069742066726f6d206f
757220746573742e0a

让我们将README的第一个字符从“#”更改为“=”。等号字符的十六进制代码为0x3d。以下测试通过:

public class UncompressedTest {
  private static final String README = """
      626c6f6220393100\
      3d\
      204f757220746573742070726f6a6563740a0a5468\
      69732077696c6c206265206f7572207465737420626c6f622e0a4c657427\
      73207365652069662077652063616e20726561642069742066726f6d206f\
      757220746573742e0a\
      """;

  @Test
  public void readme() throws IOException {
    var out = new ByteArrayOutputStream();

    try (var outputStream = new DeflaterOutputStream(out)) {
      var hexFormat = HexFormat.of();

      outputStream.write(hexFormat.parseHex(README));
    }

    var bytes = out.toByteArray();

    try (var inputStream = new ByteArrayInputStream(bytes)) {
      var reader = new BlobReader();

      var blob = reader.readInputStream(inputStream);

      assertEquals(
        blob.text(),

        """
        = Our test project

        This will be our test blob.
        Let's see if we can read it from our test.
        """
      );
    }
  }
}

我们已成功编辑blob数据。

使用java.util.Base64的变体

对于本文中的示例,使用java.util.Base64基本相同。事实上,它有几个优点:

  1. java.util.Base64从JDK 8开始可用
  2. 不需要转义字符串文字中的行终止符

以下是我们使用Base64运行示例的片段:

private static final String README = """
    eAE9yzEKgDAMRmHnnuIHB7eCo3cQXLxAUyNWqpEm0uvrovP7HmUhDH3TYroLjNVwFdk5mnPzlhQ1
    5QxiyJfpPbwb2TqFMiOtqIwYThQOC5JhLXL83rsHUlggxg==
    """;

@Test
public void readme() throws IOException {
  var decoder = Base64.getMimeDecoder();

  var bytes = decoder.decode(README);

  // consume the bytes
}

但它有一个(可能)缺点。这使得手动编辑数据变得更加困难。

使用Base64执行与上一节类似的操作不会那么简单。使用Base64编码的未压缩数据如下:

$ zlib-flate -uncompress \
    < .git/objects/75/a8b365ca1a5e731f49d3624960b314d0480ca3 \
    | base64
YmxvYiA5MQAjIE91ciB0ZXN0IHByb2plY3QKClRoaXMgd2lsbCBiZSBvdXIgdGVzdCBibG9iLgpM
ZXQncyBzZWUgaWYgd2UgY2FuIHJlYWQgaXQgZnJvbSBvdXIgdGVzdC4K

每个字符代表6位信息。因此,编辑Base64数据的单个字符意味着更改blob的两个字节。

结论

在这篇文章中,我们看到了一种使用文本块在Java源代码中存储二进制数据的方法。我们使用了java.util.HexFormat类将字符串转换为字节数组。

我们专注于使用这些数据进行测试。但是,如果需要,也可以在生产代码中使用这种技术。

以文本格式存储数据可以更容易地对其进行编辑。这假设数据:

  • 具有定义的格式;
  • 它的二进制格式允许轻松操作。

此外,由于数据是Java源代码,编辑可以在Git差异中可视化。

如前所述,这种技术更适合相对较小的数据。

github源码地址:https://github.com/objectos/blog-examples/tree/master/p010-binary-data

原文地址:https://www.objectos.com.br/blog/alternative-to-binary-files-in-your-java-tests.html#using--to-get-a-textual-representation-of-the-binary-data

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/3003.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册