EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

Rustでテストでのみ必要なアトリビュートをプロダクトコードに含めない設定

はじめに

こんにちは、バックエンドエンジニアのおおたわらです。

Rustで開発をする中で、以下のようなケースがあります。

  • テストコードで使いたいアトリビュートがある
    • #[derive] アトリビュートによるトレイト実装(assert! に指定するメッセージに構造体の中身を出力するため、Debug トレイトを実装したい、テストで entity の比較を行うため、PartialEq を実装したい etc.)
    • mockall クレートの automock アトリビュートによるモック実装
  • プロダクトコードのコンパイルには含めたくない
  • が、定義はプロダクトコード側にする必要がある

アトリビュート(特に #[derive] アトリビュート)は気軽に実装を追加できる分、意外とリリースビルド成果物のサイズを引き上げてしまうかもしれません。

また、テストコード以外から呼ばれると安全でない実装も時にはあります。例えば、DDDにおけるエンティティはプロダクトコードでは同値性は id で担保したいが、テストコードでは比較を容易にするため、PartialEq を実装したいなどです。

このように、プロダクトコードのコンパイルにテストに関連したコードを含めたくないときの設定方法を紹介します。

この記事はエモーションテックアドベントカレンダー 2023の5日目の記事です。

cfg_attr

Rustには条件付きコンパイルの仕組みがあり、ソースコードコンパイルに含める条件の設定が可能です。

条件付きコンパイルの1つにcfg_attrがあります。これは条件に応じて、アトリビュートコンパイルに含めるかどうかを選択できます。

文法は以下のようになっています。

CfgAttrAttribute :
   cfg_attr ( ConfigurationPredicate , CfgAttrs )

ConfigurationPredicateは条件式です。 指定できるオプションはドキュメントに記載があります。

この中に、test というオプションがあります。これは、テストコードをコンパイルした時、という条件です。

余談ですが、他のオプションとして target_os などの環境を指定できるのは、Rustならではですね。

all, any, not を使って複雑な条件も指定可能です。例えば、以下はターゲットOSがLinuxかつ featuremultithreaded の場合だけ some_other_attributeコンパイルに含めるという設定です。

#[cfg_attr(all(target_os = "linux", feature ="multithreaded"), some_other_attribute)]

CfgAttrsには、アトリビュートを複数指定可能です。

条件がtrueなら、指定したアトリビュートが適用されるという設定です。

設定サンプル

cfg_attr を活用することで、テストでのみ必要なアトリビュートをプロダクションビルドに含めない設定が可能です。

条件付きのderive

以下のような構造体があるとします。

pub struct Person {
  pub name: String,
  pub age: i32,
}

テストのコンパイル時に限定して Debug を実装するための設定は以下のようになります。

#[cfg_attr(test, derive(Debug))]
pub struct Person {
  pub name: String,
  pub age: i32,
}

プロダクトコードから Debug が前提の実装を行うと以下のような具合でコンパイルエラーとなります。

error[E0277]: `Person` doesn't implement `std::fmt::Debug`
  --> domain/src/entity/person.rs:32:26
   |
32 |         println!("{:?}", person);
   |                          ^^^^^^ `Person` cannot be formatted using `{:?}`
   |
   = help: the trait `std::fmt::Debug` is not implemented for `Person`
   = note: add `#[derive(Debug)]` to `Person` or manually `impl std::fmt::Debug for Person`
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Person` with `#[derive(Debug)]`
   |
7  + #[derive(Debug)]
8  | pub struct Person {
   |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `domain` (lib) due to previous error

弊社では、以前の記事(Rustバックエンドのテスト構成何もわからない - EmotionTechテックブログ)でも紹介した通り、テストコードをプロダクトコードと別のクレートに配置しています。

このような場合、Cargoのfeatureを使うことでうまく設定が可能です。

prod クレート(プロダクトコードを配置) の Cargo.toml

[features]
# testcase という feature を定義しておく
testcase = []

条件付きコンパイルの設定を行う

#[cfg_attr(feature = “testcase”, derive(Debug))]
pub struct Person {
  pub name: String,
  pub age: i32,
}

testcase クレート(テストコードを配置)の Cargo.toml

# testcase feature を読み込む
prod = {path = "../prod", features = ["testcase"]}

条件付きのモック実装生成

mockall などを使っている場合に有用な方法です。

以下のような trait があるとします。

pub trait ISampleRepository {
  fn save(entity: SampleEntity);
}

これにモック実装を与えたいが、プロダクションビルドに含めない設定は以下のようにできます。

#[cfg_attr(test, mock::automock)]
pub trait ISampleRepository {
  fn save(entity: SampleEntity);
}

おわりに

いかがでしたでしょうか?このように、Rustではきめ細やかにコンパイルのオプションを設定することができます。

エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、Rustでワクワクするプロダクト開発をしたい方がいらっしゃいましたら、ぜひ採用ページからご応募をお願いいたします。