背景
先日、.NET環境で開発しているWebアプリについて、テストデータを作成するということで、Bogusというパッケージを使用しました。Bogusは、様々な内容を自動生成してくれる機能(例、人の名前、国、地名)を有しており、世界中で利用されているパッケージです。(2023年11月4日 : 約1万7千人が使用)
Readmeファイルにも使用例が親切に載っておりますが、多分次に使う時は色々と忘れていて、戸惑う可能性がある。というわけで、まとめてみました。
Bogusの概要
Bogusは.NET系の言語向け(C#やF#、VB.NET)のシンプルなフェイクデータ生成プログラムです。Bogusは基本的にfaker.jsのC#移植であり、FluentValidationのsyntax sugarに基づいています。
Bogus, https://github.com/bchavez/Bogus. 拙訳
人の名前やメールアドレスのように適当な値を挿入したい項目はもちろん、外部キーのカラムには、他のテーブルの値を用いて挿入することも可能です。また、そのための構文も、この記事を読むことによって大枠を抑えるこができるくらいには、わかりやすいです。
さらに、世界中のユーザーが利用していることからもその利便性は明らかであり、この記事以外にも様々な記事で解説されています。
Bogusの使い方
Fakerクラスをインスタンス化する際は、対象となるモデルクラスをジェネリッククラスに指定します。例えば、Playerというクラスについて、ダミーデータを作成したい場合は、以下のようになります。
Faker<Player> dammyData = new Faker<Player>(locale : "ja")
引数は作成する言語を示しており、”ja”は日本語です。日本語の他に英語、中国語、ドイツ語など、複数の言語に対応しております。
また、インスタンス化する際に、それぞれのプロパティの値について、ルールを決めます。下のコードでは、RuleForメソッドを用いてFirstNameプロパティにランダムな値を与えています。
後に紹介しますが、RuleSetメソッドで条件を保存し、ダミーデータ生成時にその条件を指定することによって、コードの重複を避けることができます。
Faker<Player> dammyData = new Faker<Player>(locale : "ja")
.RuleFor(p => p.FirstName, f => f.Name.FirstName());
1でルール決めを行ったダミーデータを生成します。生成した後は、通常のデータの保存と同様の処理を行うことによって保存することができます。
dammyData.Generate(3) // 3つのダミーデータを生成する
// データベースに保存する
_db.Players.AddRange(dammyData)
_db.SaveChanges()
以上で、ダミーデータを生成することができます。おそらく、多くの人が簡単に感じたと思います。そのため、Bogusを使用することによって、技術的なことではなく、仕様の理解に焦点を当ててダミーデータを生成することができます。
ダミーデータ作成例
注意点
上の例で紹介した手順は、あくまで概要であり、ここでは実際のアプリに対して、ダミーデータを生成していきます。そうは言っても、僕が勉強用に作成したアプリなのですが・・・。
テーブル間の関係性がわかりやすいように、ER図を用意しました。
※スポーツチームのスタッフや選手の情報を管理するアプリを想定しており、Userクラスは、アプリの利用者がログインするために使用する情報を保存するテーブルだと想定しています。
初めてダミーデータを生成する際に、僕が苦労したことは、リレーションの把握です。
リレーションがあるということは、テーブル間に親と子の関係性があるということを示しております。そのため、子のテーブルを生成した後に親のテーブルを生成すると、実際に使用するデータベースとは異なった内容になってしまいます。
そのため、テストデータを使用する際の注意点にはなりますが、仕様をよく読み、どの順番でテストデータを作成するかは気をつける必要があります。
サンプルコード
以下に示すのは、PlayerテーブルとUserテーブルを作成するメソッドです。CoachテーブルとStaffテーブルはPlayerテーブルとほぼ同様ですし、UserCodeテーブルとStatusテーブルは内容をある程度決めており、SQL文で挿入したため、2つのメソッドを載せます。
public static void CreatePlayer(ApplicationDbContext _db)
{
// StatusIdをリストで受け取る
List<int> statusIds = _db.statuses.Select(s => s.Id).ToList();
var player = new Faker<Player>()
.RuleFor(s => s.FirstName, f => f.Name.FirstName())
.RuleFor(s => s.LastName, f => f.Name.LastName())
.RuleFor(s => s.Height, f => f.Random.Float()*2.0)
.RuleFor(s => s.Weight, f => f.Random.Float()*130.0)
.RuleFor(s => s.StatusId, f => f.PickRandom(statusIds))
;
var players = player.Generate(number);
_db.players.AddRange(players);
_db.SaveChanges();
}
public static void CreateUser(ApplicationDbContext _db)
{
// userCodeIdを定義する
const int staffUserCodeId = 1;
const int coachUserCodeId = 2;
const int playerUserCodeId = 3;
List<int> numbers = _db.players.Select(p => p.Number).ToList();
List<int> coachIds = _db.coaches.Select(c => c.Id).ToList();
List<int> staffIds = _db.staffs.Select(s => s.StaffId).ToList();
// Coachテーブルに紐づいたユーザーを作成する
foreach(int id in coachIds){
var user = new Faker<User>()
.RuleFor(u => u.Mail, f => f.Internet.ExampleEmail())
.RuleFor(u => u.Tel, f => f.Random.Replace("###-####-####"))
.RuleFor(u => u.CoachId, f => id)
.RuleFor(u => u.UserCodeId, f => coachUserCodeId);
_db.users.AddRange(user.Generate());
}
// Staffテーブルに紐づいたユーザーを作成する
foreach (int id in staffIds)
{
var user = new Faker<User>()
.RuleFor(u => u.Mail, f => f.Internet.ExampleEmail())
.RuleFor(u => u.Tel, f => f.Random.Replace("###-####-####"))
.RuleFor(u => u.StaffId, f => id)
.RuleFor(u => u.UserCodeId, f => staffUserCodeId)
.RuleFor(u => u.CoachId, f => null);
_db.users.AddRange(user.Generate());
}
// Playerテーブルに紐づいたユーザーを作成する
foreach (int id in numbers)
{
var user = new Faker<User>()
.RuleFor(u => u.Mail, f => f.Internet.ExampleEmail())
.RuleFor(u => u.Tel, f => f.Random.Replace("###-####-####"))
.RuleFor(u => u.Number, f => id)
.RuleFor(u => u.UserCodeId, f => playerUserCodeId);
_db.users.AddRange(user.Generate());
}
_db.SaveChanges();
}
名前や体重、メールアドレス等はFakerクラスのメソッドを使用し、任意の値を与えます。一方、Player ClassのStatusId(健康状態)のような外部キーについては、Dbに保存されている値を取り出してから与えます。
また、Userクラスは、コーチや球団スタッフ、選手がログインするためのデータを保存しているテーブルのため、CoachIdやStaffId、Numberは一意である必要があります。そのため、foreach文でlistから一つずつ取り出して挿入するような処理を書いています。
挿入されたUserテーブルの値
UserId | UserCodeId | Number | CoachId | StaffId | Tel | |
20 | 2 | NULL | 13 | NULL | Mona56@example.org | 135-3573-9734 |
21 | 2 | NULL | 14 | NULL | Agnes84@example.org | 322-8034-8264 |
22 | 2 | NULL | 15 | NULL | Joana_VonRueden@example.com | 573-8730-9057 |
23 | 1 | NULL | NULL | 13 | Heaven.Parker@example.com | 516-2473-6328 |
24 | 1 | NULL | NULL | 14 | Soledad.Jast@example.net | 128-3267-1546 |
25 | 1 | NULL | NULL | 15 | Hadley.Hamill@example.com | 081-4620-2966 |
26 | 3 | 10 | NULL | NULL | Bartholome67@example.org | 574-0260-9617 |
27 | 3 | 12 | NULL | NULL | Bernice_Cremin16@example.net | 960-4146-5776 |
28 | 3 | 11 | NULL | NULL | Cedrick.Lang@example.com | 041-0623-9521 |
その他の便利なメソッド
言語設定
日本語のデータを作成したい場合は、インスタンス化時に引数を与えます。ただし、日本語対応がされていない項目があるため、一部英語が入ってしまう場合があります。
また、日本語以外にも、中国語や韓国語など、様々な言語に対応しております。
var coach = new Faker<Coach>(locale : "ja")
.RuleFor(c => c.FirstName, f => f.Name.FirstName())
.RuleFor(c => c.LastName, f => f.Name.LastName());
RuleSet : RuleForを保存する
この章の内容は、dunno blogさんの”久しぶりに C# での開発現場に戻ってからテストコードを書く時にやっていること – Bogus –“を自前のコードで行ったような内容になります。読んでもあんまりよくわからない、という方はこちらの記事を読むとよりわかりやすいかもしれません。
RuleSet() メソッドはそれぞれのプロパティに対する条件を保存することができるメソッドになります。例えば、上述のCreateUserメソッドでは、3種類(Coach, Staff, Player)のテーブルに紐づいたUserをそれぞれ作成していますが、携帯番号やメールアドレスに関する条件は同一です。
同一の条件・それぞれ(Coach, Staff, Player)変更したい条件に分け、データ生成(Generate)時にその組み合わせを指定することによって、ある程度の条件を満たしたテストデータを作成することができます。
以下は、上述のコードを変更したものを一部抜粋しています。
~~~
// <一部抜粋>
var user = new Faker<User>()
.RuleFor(u => u.Mail, f => f.Internet.ExampleEmail())
.RuleFor(u => u.Tel, f => f.Random.Replace("###-####-####"));
// Coachテーブルに紐づいたユーザーを作成する
foreach (int id in coachIds)
{
user.RuleSet("Coach", set => set.Rules((f, u) =>
{
u.CoachId = id;
u.UserCodeId = coachUserCodeId;
}));
_db.users.AddRange(user.Generate("default, Coach"));
}
// <一部抜粋>
~~~
Coach, Staff, Userのいずれにおいても使用するメールアドレスや電話番号をデフォルトとして指定し、Coachテーブルに紐づくレコードにのみ必要なルールを”Coach”として指定、最後にGenerate()に引数としてそれらを渡すことによって、最初に示したコードと同様の機能を果たします。
Seed
「いくらランダムに作成されるとはいえ、毎回異なる値がでてくるのはなぁ」という方のために、Seedメソッドが用意されています。使い方は以下の通り。
var coach = new Faker<Coach>()
.RuleFor(c => c.FirstName, f => f.Name.FirstName())
.RuleFor(c => c.LastName, f => f.Name.LastName());
List<Coach> MakeCoach(int seed)
{
return coach.UseSeed(seed).Generate(number);
}
// 毎回同じ値を作成する
var coachs = MakeCoach(1330);
Fakerクラスのインスタンスに対してUseSeed()を用います。引数に同じ値を与えると、同じデータを作成することができます。
OrNull() :
「値が入っているときもあるし、入っていない時もあるよなぁ」というカラムの値を挿入する時に役立つのが、OrNullです。今回はありませんでしたが、”備考”とかある場合はOrNullを使うと、より現実に近づきますね。
使うとすると、以下のような形になります。
var coach = new Faker<Coach>()
.RuleFor(c => c.FirstName, f => f.Name.FirstName())
.RuleFor(c => c.LastName, f => f.Name.LastName())
.RuleFor(c => c.Descripation, f => f.Lorem.Sentence().OrNull());
まとめ
Bogusは、現実に近いテストデータを作成することができる .NET系の開発向けのパッケージです。
Bogusを利用することによって、テストデータを作成するための技術的な知識ではなく、仕様の理解に焦点を当てて、テストデータを作成することができます。
また、Bogusの利用者は多く、かつ、いくつかのブログで紹介されているため、信頼度の高さや情報量も魅力的な点です。そのため、.NETを利用したアプリでテストデータを作成する際には、Bogusの利用をお勧めします。
最後に、こちらの記事が役に立った際は、記事冒頭の👍ボタンを押していただきたいです。
Stackflowで、0や-300のQ&Aは読みませんよね?
一目で役に立つ記事かどうかを伝えたいのです。
以上になります。今回も読んでいただきありがとうございました。
参考文献
- bchavez, “Bogus”, https://github.com/bchavez/Bogus
- cmatskas, “Creating .NET fakes using Bogus”, https://cmatskas.com/creating-net-fakes-using-bogus-2/
- dunno logs, “久しぶりに C# での開発現場に戻ってからテストコードを書く時にやっていること – Bogus -“, https://dany1468.hatenablog.com/entry/2019/01/06/233540
コメント