RustでJSONのゴールデンファイルテストを実行する方法を学んだのでまとめておく。
[include_bytesマクロ]
2023年末にaws-lambda-rust-runtimeにコントリビュートした。その時に、テストコードを読んでいて、Rustのinclude_bytesマクロの存在を知った。
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というクレート(ライブラリ)がある。
これを先ほどの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"]
の記述の追加が必要とわかった。
以下のように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のテストコードのように文字列に戻して再度構造体に変換したものと一致するか比較するなりすればゴールデンファイルテストの出来上がり。