kdnakt blog

hello there.

RustでJSONのゴールデンファイルテストを実行する

RustでJSONのゴールデンファイルテストを実行する方法を学んだのでまとめておく。

[include_bytesマクロ]

2023年末にaws-lambda-rust-runtimeにコントリビュートした。その時に、テストコードを読んでいて、Rustのinclude_bytesマクロの存在を知った。

doc.rust-lang.org

include_bytesマクロは、ファイルのパスを指定するとバイト列としてデータを読み込んでくれる。ドキュメントには以下のようなサンプルコードが記載されていた。

fn main() {
    let bytes = include_bytes!("spanish.in");
    assert_eq!(bytes, b"adi\xc3\xb3s\n");
    print!("{}", String::from_utf8_lossy(bytes)); // adiós
}

[Serde JSON]

RustでJSONを扱うSerde JSONというクレート(ライブラリ)がある。

docs.rs

これを先ほどのinclude_bytesマクロと組み合わせると、ゴールデンファイルテストを実現できる。aws-lambda-rust-runtimeでは以下のように実装されている。

    #[test]
    #[cfg(feature = "s3")]
    fn example_object_lambda_event_get_object_iam() {
        let data = include_bytes!("../../fixtures/example-s3-object-lambda-event-get-object-iam.json");
        let parsed: S3ObjectLambdaEvent = serde_json::from_slice(data).unwrap();
        let output: String = serde_json::to_string(&parsed).unwrap();
        let reparsed: S3ObjectLambdaEvent = serde_json::from_slice(output.as_bytes()).unwrap();
        assert_eq!(parsed, reparsed);
    }

fixtureディレクトリにゴールデンファイルが保存されており、それらをinclude_bytesマクロで読み込んでいる。その後、serde_jsonの各種関数を利用して、読み込んだjsonファイルのデータをRustの構造体に変換している。

[サンプルプロジェクトで実験]

同じことをサンプルプロジェクトを作成して試してみた。

$ cargo new serde_json_sample
     Created binary (application) `serde_json_sample` package
$ cd serde_json_sample
$ cargo add serde_json       
    Updating crates.io index
      Adding serde_json v1.0.110 to dependencies.
             Features:
             + std
             - alloc
             - arbitrary_precision
             - float_roundtrip
             - indexmap
             - preserve_order
             - raw_value
             - unbounded_depth
    Updating crates.io index

準備として、公式ドキュメントで使われている以下のjsonデータをsrc/person.jsonとして保存しておく。

{
    "name": "John Doe",
    "age": 43,
    "address": {
        "street": "10 Downing Street",
        "city": "London"
    },
    "phones": [
        "+44 1234567",
        "+44 2345678"
    ]
}

続いて、公式ドキュメントaws-lambda-rust-runtimeのテストコードを参考に、作成されたsrc/main.rsを以下のように編集する。

use serde::Deserialize;
use serde_json::Result;

#[derive(Deserialize, Debug)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

fn main() -> Result<()> {
    println!("Hello, world!");

    let data = include_bytes!("./person.json");
    println!("{:?}", data);
    println!("{}", String::from_utf8_lossy(data));
    let p: Person = serde_json::from_slice(data)?;
    println!("Please call {} at the number {}", p.name, p.phones[0]);

    Ok(())
}

これをcargo runで実行してみると、エラーが発生した。

$ cargo run
   Compiling serde_json_sample v0.1.0 (/rust-sandbox/serde_json_sample)
error[E0432]: unresolved import `serde`
 --> src/main.rs:1:5
  |
1 | use serde::Deserialize;
  |     ^^^^^ use of undeclared crate or module `serde`

(省略)

cargo add serdeコマンドでserdeクレートを追加して、再度cargo runを実行したが、別のエラーが発生した。

$ cargo run     
   Compiling serde_json_sample v0.1.0 (/rust-sandbox/serde_json_sample)
error: cannot find derive macro `Deserialize` in this scope
 --> src/main.rs:4:10
  |
4 | #[derive(Deserialize)]
  |          ^^^^^^^^^^^
  |
note: `Deserialize` is imported here, but it is only a trait, without a derive macro
 --> src/main.rs:1:5
  |
1 | use serde::Deserialize;
  |     ^^^^^^^^^^^^^^^^^^

(省略)

よくよくドキュメントを読むと、deriveアトリビュートserde::Deserializeトレイトを実装するには、Cargo.tomlにfeatures = ["derive"]の記述の追加が必要とわかった。

serde.rs

以下のようにCargo.tomlを修正する。

[dependencies]
- serde = "1.0.194"
+ serde = { version = "1.0.194", features = ["derive"] }
serde_json = "1.0.110"

改めてcargo runで実行してみると、うまく動いた。

$ cargo run     
   Compiling serde_json_sample v0.1.0 (/rust-sandbox/serde_json_sample)
(省略)
warning: `serde_json_sample` (bin "serde_json_sample") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/serde_json_sample`
Hello, world!
[123, 10, ...(省略)..., 10, 10]
{
    "name": "John Doe",
    "age": 43,
    "address": {
        "street": "10 Downing Street",
        "city": "London"
    },
    "phones": [
        "+44 1234567",
        "+44 2345678"
    ]
}


Please call John Doe at the number +44 1234567

あとは構造体の個別のフィールドについてアサーションを実行するなり、aws-lambda-rust-runtimeのテストコードのように文字列に戻して再度構造体に変換したものと一致するか比較するなりすればゴールデンファイルテストの出来上がり。

[まとめ]

  • Rustのinclude_bytesマクロはファイルのデータを読み込むのに便利
  • serde_jsonクレートと組み合わせるとゴールデンファイルテストを実装できる
  • serdeクレートのderiveフィーチャーもほぼ必須なので忘れずに追加すること
  • サンプルのソースコードは以下のリポジトリにある

github.com