EmotionTechテックブログ

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

Rustバックエンドのテスト構成何もわからない

はじめに

こんにちは、テックリードのかどたみです。 前回の記事でRustでマイクロサービス開発をすることを宣言してから一つのサービスの開発が終盤に差し掛かっています。現在開発しているサービスはとても重要なドメインであるためテストケースの充足にはとても力を入れて取り組んでいます。

その最中、短い間ですがテストファイルの構成について変遷があり、悩み続けているので、本記事ではその過程と自信が無いながらも落ち着いている現状についてさらけだそうと思います。

※本記事での"プロダクトコード"はサービス提供時にデプロイされて動作するプログラムのコードを指します。

※この記事はEmotion Tech Advent Calendar 2021 7日目の記事です

採用しているディレクトリ構成

プロダクトコードのディレクトリ構成

実際にコンパイルされて動作環境にデプロイされるコードのフォルダ構成を以下に示します。4層レイヤードアーキテクチャを採用しています。

project/
 |- api/
 |- domain/
 |- infrastructure/
 |- usecase/
 |- Cargo.toml

Cargo.tomlは以下のようになっており、それぞれのディレクトリをcrateとして置いてあります。各層をバラバラのcrateにして、必要なものを依存させることによって不必要にレイヤーが相互依存することを防いでいます。

[workspace]

members = [
  "api",
  "usecase",
  "domain",
  "infrastructure",
]

現状のテストコード配置

Rustではプロダクトコードと同じファイルの中にユニットテストコードを書き、結合テストはcrateの直下にtestsディレクトリを作成し、その配下に配置するのが一般的です。 しかし、弊社では以下のようにテストコードもcrateとして存在させています。(ディレクトリ名はtestにするとコンパイラに怒られます。。。)

project/
 |- api/
 |- domain/
 |- infrastructure/
 |- testcase/  # ここにテストを配置している
 |- usecase/
 |- Cargo.toml

Cargo.tomlにもこのように含めています。

[workspace]

members = [
  "api",
  "usecase",
  "domain",
  "infrastructure",
  "testcase",
]

では、なぜこのような構成に至ったのか経緯を紹介します。

困っていたこと

webサービス開発のテストに置いて構造体やDBの永続化データを作成することは必須です。 はじめはドメイン層や永続層をユニットテストをするときにそれらを作成するファクトリをtestsディレクトリに作成し、テストコードではそれを使い回すようにしていました。 しかし、当然ユースケース層やインターフェース層でもそれらのデータは必要になるため、ファクトリを他の層でも使い回したくなりました。

思考停止で対応するとなると、各層のsrc以下にファクトリを格納し、外部クレートから利用できるようするということが挙げられると思います。しかし、このようにしてしまうとプロダクトコードとしてファクトリがビルドされてしまいます。これによって

  • 実際に利用しないものにビルド時間がかかる
  • 他層、同層のプロダクトコードから作成者の意図と無関係に呼び出されてしまう

という問題が発生します。

解決案

前章の問題を解決するためにテスト用のcrateを用意するという方法が取れるのでは無いかと思い構成を変えてみましたが、そのtest用crateをどのように扱うか以下の2つの選択肢が出てきます。

  1. プロジェクトの1crateとしてworkspaceに含めてしまう
  2. プロダクトコードとは別に独立したcrateとして定義して、外部crateとしてプロダクトコードを読み込む

結論としては1を選んだわけですが、テストコードを実行するだけであれば上記2つに差はありませんでした。 では、なぜ1を選んだかというと次章で紹介する実験の結果、CIの観点で1の方が勝っていたからです。

実験

開発環境のCIではテストを実行し、問題がなければビルドするので同じマシンでcargo test, cargo build2つのコマンドを実行することを前提としています。ただし、本番環境にデプロイされるものは--releaseオプションを付け最適化したものをビルドします。 よって、ビルド時に生成されるtargetディレクトリが無い状態で

  • cargo test
  • cargo build
  • cargo build --release

を実行した場合の実行時間を比較することにしました。 比較する構成は以下の3つです。

  1. プロダクトコードにファクトリが含まれているもの
  2. プロジェクトの1crateとしてworkspaceに含めたもの
  3. プロダクトコードとは別に独立したcrateとして定義したもの

10回実行時の平均結果は以下のようになりました。

コマンド 1.プロダクトコードにファクトリを含む 2.テストをworkspaceに含める 3.テストを独立したcrateとして定義
cargo test 1:38.21 1:41.57 1:37.36
cargo build 0.346 0.319 1:26.56
cargo build --release 2:28.49 2:30.12 2:27.12

これを見ると、独立したcrateとしてビルドすると開発環境のCI時には3の場合はプロダクトコードを2回ビルドすることになり、時間がかかることがわかります。それ以外の結果は変わらずだったので2を採用しています。 時間の他にビルドされたバイナリサイズも調べてみました。当たり前ですが--releaseオプションを付けたときはすべてのサイズが同じ、付けていないときはファクトリの分だけ1のバイナリが大きくなります。

ただ、開発環境のビルドでも最適化したものを利用する、というのであれば2も3も変わりないかなという印象です。

おわりに

いかがでしたでしょうか? 今回はRustプロジェクトのテストに関するディレクトリ構成についてお話しました。まだ開発をはじめて数ヶ月で開発をしながら環境を整えている状態なので、これがベストプラクティスだとは思っていませんが、参考になれば幸いです。 もっと良い方法や便利なcrateを知っている方がいらっしゃればコメントなどで教えていただけると幸いです。

Emotion Techでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、Rustを使ってバリバリ開発したい方、イキイキと働けるチームでプロダクトをより良くしていきたい方がいらっしゃいましたら、ぜひ採用ページからご応募をお願いいたします。

hrmos.co

hrmos.co

hrmos.co