C# でテスト時のオブジェクト生成を楽にする Plant を試してみる

テストコード書いてると、テストで使う value object の生成が繰り返しになって、かつその値がケースによって違うのだけど、差分がわかりにくかったりする事が多い。
そうなると、「結局このテストで確認したい項目って何?」とテストの焦点がボケてしまって、pull request が無駄に行ったり来たり。。
GOOS 本でも生成に関しては触れられてたけど、自前でゴリゴリ書くのはめんどいなーと思ってら、一年ぐらいまえに社内に来てた ruby のすごい人に教えてもらった factory_girl について思い出した。

C# での factory_girl っぽいライブラリ

たぶん全部じゃないけどさらっと見つけられたのは以下。

  • FactoryGirl.NET
    • まんまの名前のライブラリ、でもそれほど実装は進んでいない。単なるインスタンス名前付けして生成できないのが痛い。
  • Machinist.Net
    • factory girl じゃなくて、ruby でも factory_girl より後発の machinist の移植版。
    • EntityFramework との接続もやるようになってるし、まあまあ良い感じ。でも 2 年前から更新無し。
  • Plant
    • 名前的には関連なさげだけど、一番 factory_girl に近い形で API も整備されている気がする。

ruby の Fabrication の移植はさらっとは見つけられなかった。Fabricator っていう近い感じのはあったが、データ生成のライブラリなので関係はなさそう。

てなわけで、個人的には Plant を使ってテスト書いてみよかなと。

Plant の使い方

インストール

おなじみの nuget で。

PM> install-package plant
'Plant 0.1.0.0' が正常にインストールされました。
'Plant 0.1.0.0' が SampleProject に正常に追加されました。

基本

これから説明するのは以下の公式のテストケースに全部書いてある事と同じなので、以下見てもらった方が早いです。
https://github.com/jbrechtel/plant/blob/master/Plant.Tests/BasePlantTest.cs

ここからは以下の2つのクラスをサンプルに進めます。ついでにサンプルコードは全て NUnit と ChainingAssertion で書いたコードです。

public class Company
{
    public string Name { get; private set; }
    public string CompanyStatus { get; private set; }
    
    public Company(string name, string companyStatus)
    {
        Name = name;
        CompanyStatus = companyStatus;
    }
}

public class Employee
{
    public string Name { get; set; }
    public string DepartmentName { get; set; }
    public bool IsManager { get; set; }
    public Company BelongedCompany { get; set; }
}

定義して作る

コンストラクタから

var plant = new BasePlant();
plant.DefineConstructionOf<Company>(new {Name = "テスト", CompanyStatus = "株式会社"});

var company = plant.Create<Company>();
company.Name.Is("テスト");
company.CompanyStatus.Is("株式会社");

生成時に一部上書きする

こちらはプロパティを入れる形で。

var plant = new BasePlant();
plant.DefinePropertiesOf<Employee>(new { Name = "山田太郎", DepartmentName = "開発部" });

var employee = plant.Create<Employee>();
employee.Name.Is("山田太郎");
employee.DepartmentName.Is("開発部");
employee.IsManager.IsFalse();

var employee2 = plant.Create<Employee>(new { DepartmentName ="サポート部"});
employee2.Name.Is("山田太郎");
employee2.DepartmentName.Is("サポート部"); // 上書きした値が入る
employee2.IsManager.IsFalse();

ここまで、定義時には無名クラスを使ってきましたが、普通にインスタンスとして定義を作る事も可能です。

名前付きで定義して生成する

まず、ベースとなるものを同じ plant に定義してから、その variation という形で名前付きのインスタンス生成を可能にするのが特徴です。

var plant = new BasePlant();
plant.DefinePropertiesOf<Employee>(new { Name = "山田太郎", DepartmentName = "開発部" });
plant.DefineVariationOf<Employee>("Manager", new { IsManager = true });

var manager = plant.Create<Employee>("Manager");
manager.Name.Is("山田太郎");
manager.DepartmentName.Is("開発部");
manager.IsManager.IsTrue();

var employee = plant.Create<Employee>();
employee.Name.Is("山田太郎");
employee.DepartmentName.Is("開発部");
employee.IsManager.IsFalse();

関連を生成する

Employee の方にある、BelongedCompany に Company が入ります。

var plant = new BasePlant();
plant.DefineConstructionOf<Company>(new { Name = "テスト", CompanyStatus = "株式会社" });
plant.DefinePropertiesOf<Employee>(new { Name = "山田太郎", DepartmentName = "開発部" });

var employee = plant.Create<Employee>();

employee.BelongedCompany.Name.Is("テスト");

逆に Company の方に Employee のコレクションを追加しても、自動で関連として登録はされません。サンプルを読む限りだと定義時に生成後イベントを設定できるため、そこで登録するようにしているようです。

生成後のイベントは、プロパティ同士の結合した値を入れる等にも使われるみたいです。(姓と名を定義して姓名の値を作ったりとか)

シーケンスを使って生成する

var plant = new BasePlant();
plant.DefinePropertiesOf<Employee>(new { Name = new Sequence<string>((i) => "従業員_" + i), DepartmentName = "開発部" });

plant.Create<Employee>().Name.Is("従業員_0");
plant.Create<Employee>().Name.Is("従業員_1");

Lazy Property を使って生成する

使いドコロはまだ想像できないのですが、定義後にも環境にある変数の変更に対応できます。

var today = DateTime.Today;

var plant = new BasePlant();
plant.DefinePropertiesOf<Employee>(new { Name = "山田太郎", DepartmentName = "開発部", StartDate = today });
plant.DefineVariationOf<Employee>("lazy", new { Name = "山田太郎", DepartmentName = "開発部", StartDate = new LazyProperty<DateTime>(() => today) });

today = DateTime.Today.AddDays(-1); // 定義後に値を変える

plant.Create<Employee>().StartDate.IsNot(today); // こっちは固定されているため一致しない
plant.Create<Employee>("lazy").StartDate.Is(today);

駆け足で説明してきました。今回は全て毎回 BasePlant を生成してしますが、IBlueprint を継承したクラスを作って、一気に定義を構築する事もできます。

また、今回省きましたが、生成には Build と Create があって、単にインスタンスを生成するだけの Build と異なり Create は生成時のイベントを書くことができるので、そのまま生成した値を DB に反映なんて事もできます、次はその辺りを書けたら。