久しぶりに C# での開発現場に戻ってからテストコードを書く時にやっていること - Bogus -
上記の記事の続きです。
factory_bot みたいなのが欲しい
Ruby の開発現場ではテストデータの生成に factory_bot を利用していました。 factory_bot の利用自体には賛否あると思いますし、メンテナンスが大変になるケースもありましたが、それでも Rails の Model のデータからさくっと Factory を定義できて、インスタンス化やデータの投入も簡単にできて重宝しました。
5 年以上前に Plant という factory_bot like なパッケージを使っていることをこのブログに書いたことがあります。
ただ、もう何年もメンテナンスされておらず、メンテナンスしてまで使いたいとも思えず別のものを探すことにしました。
Bogus
README より引用 Hello. I'm your host Brian Chavez (twitter). Bogus is a simple and sane fake data generator for .NET languages like C#, F# and VB.NET. Bogus is fundamentally a C# port of faker.js and inspired by FluentValidation's syntax sugar.
上記の通り factory_bot というよりは Faker です。
Factory としても結構使える
Bogus には基本は Usage を見てもらうように、ある class に対して RuleFor
を設定していくことで、生成するデータのルールを設定していき、 Generate
で生成するという形です。
また、 FinishWith
というオプションを使うと、 Generate
でのインスタンス生成後に実行するルールを追加できます。 factory_bot での after(:build)
callback のように使えます。
加えて RuleSet
というオプションが用意されていて、これを利用すると複数のルールに名前をつけて登録することができ、個別にそのルールを適用した上で呼び出すことができます。
var testUsers = new Faker<User>() .RuleSet("Male", set => set.Rules((f, fa) => { fa.FirstName = f.Name.FirstName(Gendar.Male); fa.LastName = f.Name.LastName(Gendar.Male); } )) .RuleSet("Female", set => set.Rules((f, fa) => { fa.FirstName = f.Name.FirstName(Gendar.Female); fa.LastName = f.Name.LastName(Gendar.Female); } )) .RuleFor(u => u.UserName, (f, u) => f.Internet.UserName(u.FirstName, u.LastName))
上記のような定義を作った場合には以下のように生成します。
testUsers.Generate("Male"); // UserName も生成するためには default のルールも含める testUsers.Generate("default,Male");
私は以下のような拡張メソッドを作って default
を含めるケースを書いています。
public static class FakerExtensions { public static T GenerateWithDefault<T>(this Faker<T> obj, string ruleSets) where T : class => obj.Generate($"default,{ruleSets}"); }
関連するモデルの生成
この辺はまだ試行錯誤中ですが、 RuleFor
に書けるところは書いたり、 FinishWith
にまとめて書いたりしています。
こういうちょっとした部分はやはり factory_bot はよくできているなと思います。というか Bogus がそういう用途ではないというのもあると思いますが。
Rule のデータを生成時に書き換えたい時
factory_bot なら特に苦労なく build(:user, first_name: 'test')
のように上書きできたのですが、 Bogus の場合はそうはいきません。
今の所は変更することの多いプロパティのみを FinishWith
で上書きする Build
メソッドを用意したり、イレギュラーな場合は普通に生成後に上書きしたりで対応しています。
例ですが、上記の例を私が作成している Factory class として書くと以下のような感じです。
public static class UserFactory { private static Faker<User> Default = new Faker<User>() .RuleSet("Male", set => set.Rules((f, fa) => { fa.FirstName = f.Name.FirstName(Gendar.Male); fa.LastName = f.Name.LastName(Gendar.Male); })) .RuleSet("Female", set => set.Rules((f, fa) => { fa.FirstName = f.Name.FirstName(Gendar.Female); fa.LastName = f.Name.LastName(Gendar.Female); })) .RuleFor(u => u.UserName, (f, u) => f.Internet.UserName(u.FirstName, u.LastName)); public static Faker<User> Build(string userName) => Default.FinishWith((f, u) => { { u.UserName = userName; }); } // 利用側 var user = UserFactory.Build("exampleUserName").GenerateWithDefault("Male");
と、いろいろ試行錯誤はしているのですが、今の現場だと NUnit の作法通りにデータセットを淡々と記述しているケースがほとんどなので、「だいぶ邪道なやり方を自分は選んでしまったのではないか。。」という感じで、これからどうしていこうかなぁと悩んでいる最中です。