Rustのマクロのテストユーティリティを組んだ

2022/07/02に公開

まえがき

当記事では、cargoよりインストールできる自作のOSS、assert-parseについての解説をします。当記事への指摘及び、ユーティリティのリポジトリへのIssueは歓迎します。
なお、リポジトリは以下になります。
https://github.com/NULL-header/assert-parse

使用例

このユーティリティはTDDなど、マクロのパース部分に対してもテストを用いたい場面で利用するものです。
また、基本的にはDisplayトレイトが実装されたエラー型で、エラーハンドリングがなされている場合にのみ機能します。
その前提において、例のコードを、該当ユーティリティのリポジトリのREADMEから抜粋します。

通常API

以下のように、rstestのfixtureを利用する形で使います。

#[cfg(test)]
mod test {
   use super::*;
   use quote::quote;
   use rstest::*;

   type Assert = Assert<Input, InputError>;

   #[fixture]
   fn assert() -> Assert {
       make_assert()
   }

   #[rstest]
   fn error(assert: Assert) {
       let input = quote! {1};
       assert.error(input, InputError::NotIdent);
   }

   #[rstest]
   fn ok() {
       let input = quote! {mock};
       assert.ok(input, |i| {
           assert_eq!(i.to_string(), "mock".to_string());
       });
   }
}

https://github.com/NULL-header/assert-parse よりREADME.mdから一部抜粋

fixtureとして渡されたassertに対して、メソッドコールの形でアサートするのが最もベーシックな使い方です。

Shorthand

ユーティリティのマクロを利用することで、このユーティリティを用いる上でのボイラーテンプレートを省略することもできます。

-    type Assert = Assert<Input, InputError>;
-
-    #[fixture]
-    fn assert() -> Assert {
-        make_assert()
-    }
+    register_assert!(Input,InputError);

解説

上記のもので、当記事で紹介しているユーティリティの使い方はなんとなく理解できたかと思います。
以下からは、このユーティリティがどのようにして作られたか、またこのユーティリティを使うとどう嬉しいのかといった視点で解説します。
ユーティリティのドキュメントとしては上記までの内容にて終了となりますので、以下からは読み物として見て頂けると幸いです。

fixture

まずもって、rstestのfixtureをなぜ利用しているのか解説します。
使用例を一見するだけでは、rstestのfixtureを利用する必要はないし、もっと言うのならAssertが構造体である必要もないように見られるかと思います。
せいぜいアサーションをまとめるためにモジュールとして纏める程度でよい、とすることもできますので、その考え方は一側面として正しいです。
実際、Assert構造体は状態としての値を持ちません。であれば、アサーションする際のパニック以外には副作用もありませんし、構造体として振る舞う必要性は薄く感じることでしょう。
ならばなぜ構造体としているか、その理由は一つで、型のアサーションをするためです。
マクロのパース部分の単体テストでのみ用いると考えた場合、一つのアサーションが担保するのは一つのパーサーと一つのエラー列挙型です。
この場合、構造体としてジェネリクスを持ち、アサート関数の引数に型を指定すればより堅実なテストが可能となるはずです。
つまり、アサーションに型情報で副作用を起こしたいため、値は必要ないが型を状態として持つ必要があり、それによって構造体である必要性がある、ということです。
この部分は以下にてgithubからでも確認できます。

https://github.com/NULL-header/assert-parse/blob/de9de0019b11c6144503aeee2f96d71c471688a5/assert_parse_core/src/lib.rs#L6-L10

正常系のコールバック

以下の部分において、コールバック部分に違和感を覚えた読者の方もいらっしゃるのではないでしょうか。

assert.ok(input, |i| {
   assert_eq!(i.to_string(), "mock".to_string());
});

ここはある種の妥協でこうなっています。筆者も実装した際、当然ながらassert.okはResultに対するunwrapのような挙動が望ましいと思っていました。
しかしいざ書いてみると、synクレートの構造体は入力のTokenStreamに対して参照を持っているようで、値を返すことで上手く所有権を渡すことができなかったのです。
無論入力の所有権を持っているのはassert.okをコールする関数ですので、理論的には可能なはずですが、そこまで高度なライフタイム操作をして、コールバックでアサートする以上に利便性が向上するか、という疑問に衝突し、ひとまず妥協する運びとなったのです。
ここはいつか改善したい場所でもあります。

利点

このクレートを用いることで受けられる恩恵はいくつかあります。
その最たるものはボイラーテンプレートを省略できる部分です。けれども筆者はそれを目的としてこのクレートを実装したのではありません。
ところで、筆者は以下の記事にて感動を覚えたことがあります。

https://qiita.com/daishi/items/a002904f320976235ec4

筆者は基本フロントエンドに興味を持っていますので、それに関する調査をしているうちにこの記事を見つけました。特に、以下の引用部分に心を動かされました。

このライブラリは、その機能より、コーディングパターンを提案することに意味があります。

筆者にとってライブラリの作成とは新たな機能、機構の提案でしかないと、それまでは考えていました。けれども、ある種のコード規約やテンプレート、決まった書き方の提唱という意味でのライブラリの利用という観点は私にとって新しく、視野が広がったと自覚できた瞬間でもありました。
よって、その指向性を持って、当クレートを実装しました。
assertに対して型チェックの要素を持たせ、より思考コストを削減し、様々なテストにおいて似たコードの形をとりやすくすることで、コードから難解さを除去しようという取り組みにおいて実装したとも言えます。

モチベーション

以下では当クレートを実装するにあたってのモチベーションを述べます。

何も考えたくない

見出しにあることは、コーディングをしているとき誰しも思うことではないでしょうか。筆者もそうです。
ところで、筆者は冗談ではなく実際に、コーディングではあまり考えないようにしています。流石に設計段階では考えますが、それ以外で悩むことはほぼありません。
それと似た話として、筆者が幼少期に聞きかじったことなのですが、将棋のプロは指すとき割と考えないそうです。長年将棋を指すことで、戦略の流れが直観的に理解できるようになり、考えるのではなく感じられるようになるとか。
あるいは、学生時代に恩師に教わった、テストで直観的に答えが分からないなら思い出せないから諦めて次の問題に行け、という言葉にも近しいでしょうか。
とにかく、下手に考えるより感じるままに書いたほうがよいコードがかける、というのを筆者は信条にしていまして、それがまず前提にあります。
ここで、上記の項目である利点で語った、コーダーに対して作用する、コーディングパターンの提唱という側面について考えます。
テンプレートがあれば、それにそって書くだけなので、何も考える必要がないのです。
よって、筆者はコーディングする際に少しでも思考コストを削るために当ライブラリを作ろうと思い立ちました。

値の埋め込みの回避

元来Rustのマクロにおいて、最も単純なエラーハンドリングはpanicマクロをコールすることです。あるいは、それに準じるライブラリが存在しており、そちらを使えばもっとシンプルに書くことができます。
しかし、その場合はエラーの扱いが宣言的ではなく手続き的になり、どこでどのエラーが発生しているのか判然としません。また、panic時のメッセージはコードに埋め込むこととなり、単体テスト等値を使い回したいシチュエーションにおいてコピー&ペーストが発生してしまいます。
よってエラーメッセージを含むエラー列挙型を定義し、それらをreturnするのが最も適切なハンドリングと言えるでしょう。
そこで、宣言的にエラー列挙型を定義し、エラーメッセージを単体テストする際のユーティリィとしてこのクレートが存在します。

ボイラーテンプレート

上記の通り、マクロで単体テストを堅実に行うためにはエラーを別に定義する必要があります。
しかし、synクレートを用いてパーサーの定義をする場合、パース部分が返すことのできるエラー型はstringのみです。
このままではエラーバリアントをmatch式等でテストすることができません。
そこの解決策の一つが、エラー列挙型に対してDisplayトレイトが実装されていることを期待して、パース語の値とエラー列挙型のバリアントに対してto_stringメソッドをコールし、値を比較することです。
しかし、各テストでto_stringを各部でコールするのは冗長すぎるきらいがあり、また各部のmatch式の構成も非常に似通ってきます。
その部分を関数に纏めるのはプログラミングにおける常道だと思われますが、そここそがボイラープレートと呼ばれる、毎回定義しなければならない面倒な部分でもあります。
ここで、こういったコピー&ペーストの発生しそうな部分をクレートに纏めたのがassert-parseとなります。

あとがき

当記事では制作時、構想時の備忘録も兼ねて、いくつかの観点から当クレートについて解説しました。
前書きでも述べましたが、当記事及び当クレートに対する指摘等は歓迎します。
なお、assert-parseと併せて使うとより便利なクレート、to-syn-errorの記事も投稿予定です。よろしくお願いします。

GitHubで編集を提案

Discussion