このドキュメントの内容
データレコード形式のテストデータを生成するライブラリ mxProject.Devs.DataGenerator
を紹介します。
ライブラリの概要
-
データレコード形式のテストデータを生成するための .NET Standard 2.0 ライブラリです。
-
IDataReader インターフェースを介してデータレコードにアクセスできます。
Dapper
を用いたO/RマッピングやReactive Extensions
を用いた Observable モデルの実装も容易だと思います。 -
IDataReader.Read メソッドが呼び出された時点で次のデータレコードを生成します。データジェネレーター内部ではなるべくリストを保持しないように設計しています。生成するデータレコードの数が多い場合でも、データ生成に必要となるリソースはそれほど大きくなりません。
公開先
GitHub
https://github.com/mxProject/DataGenerator/tree/main/mxProject.Devs.DataGenerator
Nuget
https://www.nuget.org/packages/mxProject.Devs.DataGenerator/
サンプルコード
ビルダーを用いた実装例
// ビルダーを定義します。
DataGeneratorBuilder builder = new DataGeneratorBuilder()
// 1 から 100 までの連番を返すフィールド
.AddField(factory => factory.SequenceInt32(
"ID",
1,
100
))
// 今日から一か月後までの間のランダムな日付を返すフィールド
.AddField(factory => factory.RandomDateTime(
"SalesDate",
DateTime.Today,
DateTime.Today.AddMonths(1),
x => x.Date
))
// 1, 2, 3 を順番に返すフィールド
.AddField(factory => factory.Each(
"ShopCode",
new int[] { 1, 2, 3 }
))
// 0 から 100000 までの間のランダムな数値を返すフィールド
.AddField(factory => factory.RandomInt64(
"Sales",
minValue: 0,
maxValue: 100000
))
// "SalesDate" フィールドの値に対応する曜日を返すフィールド
.AddAdditionalField(
"DayOfWeek",
typeof(DayOfWeek),
rec => rec.GetDateTime("SalesDate").DayOfWeek
)
// "ShopCode" フィールドの値に対応する複数の付加列の値を返すフィールド
.AddJoinField(factory => factory.WithDictionary(
"ShopCode",
additionalFieldNames: new[]
{
"ShopName",
"OpeningTime"
},
additionalValues: new Dictionary<int, (StringValue, TimeSpan)>()
{
{ 1, ("SHOP1", new TimeSpan(10, 0, 0)) },
{ 2, ("SHOP2", new TimeSpan(18, 0, 0)) }
}
))
;
// 10件のデータレコードを生成するデータジェネレーターを生成し、IDataReader インスタンスとして返します。
using IDataReader reader = await builder.BuildAsDataReaderAsync(generateCount: 10);
// IDataReader からデータレコードを取得して出力します。
for (int i = 0; i < reader.FieldCount; ++i)
{
if (i > 0) { Debug.Write('\t'); }
Debug.Write(reader.GetName(i));
}
Debug.WriteLine("");
while (reader.Read())
{
for (int i = 0; i < reader.FieldCount; ++i)
{
if (i > 0) { Debug.Write('\t'); }
Debug.Write(reader.GetValue(i));
}
Debug.WriteLine("");
}
次のようなデータが出力されます。
ID SalesDate ShopCode Sales DayOfWeek ShopName OpeningTime
1 2021/10/05 0:00:00 1 15702 Tuesday SHOP1 10:00:00
2 2021/09/20 0:00:00 2 99983 Monday SHOP2 18:00:00
3 2021/09/27 0:00:00 3 16640 Monday
4 2021/09/24 0:00:00 1 12485 Friday SHOP1 10:00:00
5 2021/10/02 0:00:00 2 18911 Saturday SHOP2 18:00:00
6 2021/10/17 0:00:00 3 79244 Sunday
7 2021/10/17 0:00:00 1 38010 Sunday SHOP1 10:00:00
8 2021/09/28 0:00:00 2 49279 Tuesday SHOP2 18:00:00
9 2021/09/24 0:00:00 3 43640 Friday
10 2021/10/15 0:00:00 1 71709 Friday SHOP1 10:00:00
設定値クラスを用いた実装例
設定値クラスを用いてデータジェネレーターを定義することもできます。これらの設定値クラスはJsonシリアライズをサポートしています。
次のコードは前述のサンプルコードとほぼ同じ内容を実装しています。
// ジェネレーターを定義します。
DataGeneratorSettings generatorSettings = new DataGeneratorSettings()
{
Fields = new DataGeneratorFieldSettings[]
{
// 1 から 100 までの連番を返すフィールド
new SequenceInt32FieldSettings()
{
FieldName = "ID",
InitialValue = 1,
MaximumValue = 100
},
// 今日から一か月後までの間のランダムな日付を返すフィールド
new RandomDateTimeFieldSettings()
{
FieldName = "SalesDate",
MinimumValue = DateTime.Today,
MaximumValue = DateTime.Today.AddMonths(1),
SelectorExpression = "x => x.Date"
},
// 1, 2, 3 を順番に返すフィールド
new EachFieldSettings<int>()
{
FieldName = "ShopCode",
Values = new int?[]{ 1, 2, 3 }
},
// 0 から 100000 までの間のランダムな数値を返すフィールド
new RandomDecimalFieldSettings()
{
FieldName = "Sales",
MinimumValue = 1,
MaximumValue = 100000
}
},
AdditionalFields = new DataGeneratorAdditionalFieldSettings[]
{
// "SalesDate" フィールドの値に対応する曜日を返すフィールド
new ExpressionFieldSettings()
{
FieldName = "DayOfWeek",
ValueType = typeof(DayOfWeek).FullName,
Expression = "rec => rec.GetDateTime(\"SalesDate\").DayOfWeek"
}
},
AdditionalTupleFields = new DataGeneratorAdditionalTupleFieldSettings[]
{
// "ShopCode" フィールドの値に対応する複数の付加列の値を返すフィールド
new JoinFieldSettings()
{
KeyFields = new DataGeneratorFieldInfo[]
{
new DataGeneratorFieldInfo("ShopCode", typeof(int))
},
AdditionalFields = new DataGeneratorFieldInfo[]
{
new DataGeneratorFieldInfo("ShopName", typeof(string)),
new DataGeneratorFieldInfo("OpeningTime", typeof(TimeSpan))
},
AdditionalValues = new Dictionary<string?[], string?[]>()
{
{
new[] { "1" },
new[] { "SHOP1", "10:00:00" }
},
{
new[] { "2" },
new[] { "SHOP2", "18:00:00" }
}
}
}
}
};
// ビルダーを生成します。
DataGeneratorContext context = new DataGeneratorContext();
DataGeneratorBuilder builder = generatorSettings.CreateBuilder(context);
// 10件のデータレコードを生成するデータジェネレーターを生成し、IDataReader インスタンスとして返します。
using IDataReader reader = await builder.BuildAsDataReaderAsync(generateCount: 10);
// IDataReader からデータレコードを取得して出力します。
for (int i = 0; i < reader.FieldCount; ++i)
{
if (i > 0) { Debug.Write('\t'); }
Debug.Write(reader.GetName(i));
}
Debug.WriteLine("");
while (reader.Read())
{
for (int i = 0; i < reader.FieldCount; ++i)
{
if (i > 0) { Debug.Write('\t'); }
Debug.Write(reader.GetValue(i));
}
Debug.WriteLine("");
}
// 設定値インスタンスをJSON文字列にシリアライズします。
var converterBuilder = DataGeneratorFieldTypeConverterBuilder.CreateDefault();
var jsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented
};
foreach (var converter in converterBuilder.Build())
{
jsonSettings.Converters.Add(converter);
}
string json = JsonConvert.SerializeObject(generatorSettings, jsonSettings);
Debug.WriteLine(json);
次のJSON文字列は、前述の generatorSettings をシリアライズした結果です。
{
"Fields": [
{
"Name": "ID",
"NullProb": 0.0,
"Initial": 1,
"Max": 100,
"Increment": null,
"FieldType": "SequenceInt32"
},
{
"Name": "SalesDate",
"NullProb": 0.0,
"Min": "2021-09-23T00:00:00+09:00",
"Max": "2021-10-23T00:00:00+09:00",
"Selector": "x => x.Date",
"FieldType": "RandomDateTime"
},
{
"Name": "ShopCode",
"NullProb": 0.0,
"Values": [
1,
2,
3
],
"FieldType": "EachInt32"
},
{
"Name": "Sales",
"NullProb": 0.0,
"Min": 1.0,
"Max": 100000.0,
"Selector": null,
"FieldType": "RandomDecimal"
}
],
"TupleFields": null,
"AdditionalFields": [
{
"FieldName": "DayOfWeek",
"ValueType": "System.DayOfWeek",
"Expression": "rec => rec.GetDateTime(\"SalesDate\").DayOfWeek",
"FieldType": "Expression"
}
],
"AdditionalTupleFields": [
{
"AdditionalFields": [
{
"FieldName": "ShopName",
"ValueType": "System.String"
},
{
"FieldName": "OpeningTime",
"ValueType": "System.TimeSpan"
}
],
"KeyFields": [
{
"FieldName": "ShopCode",
"ValueType": "System.Int32"
}
],
"AdditionalValues": [
{
"Key": [
"1"
],
"Value": [
"SHOP1",
"10:00:00"
]
},
{
"Key": [
"2"
],
"Value": [
"SHOP2",
"18:00:00"
]
}
],
"FieldType": "Join"
}
]
}
機能説明
-
次の手順に従ってデータを生成します。
- データレコードを構成するフィールドの名前とデータ生成アルゴリズムを定義します。
- 生成するデータ数を指定して DataGenerator クラスのインスタンスを生成します。IDataReader インターフェースのインスタンスを生成することもできます。
- DataGenerator インスタンスの GenerateNext メソッドを呼び出します。データレコードはこのとき生成されます。(IDataReader インスタンスの場合は Read メソッド)
- GenerateNext メソッドの戻り値が false になるまで繰り返します。(IDataReader インスタンスの場合は Read メソッド)
データフィールド
- DataGenerator に定義できるフィールドは、次の四つの種類に分類することができます。
Field Type | Summary |
---|---|
DataGeneratorField | 一つのフィールドの値を生成します。 |
DataGeneratorTupleField | 複数のフィールドの値の組み合わせを生成します。 |
DataGeneratorAdditionalField | 生成された値をもとに追加の一つのフィールドの値を返します。 |
DataGeneratorAdditionalTupleField | 生成された値をもとに追加の複数のフィールドの値を返します。 |
DataGeneratorField
Field Type | Summary |
---|---|
Any | 指定された値の中から何れかの値を返します。 |
Computing | 指定された式の計算結果を返します。 |
DbQuery | 指定されたデータベースクエリーから一つのフィールドの値を読み取って返します。 |
Each | 指定された値を順番に返します。 |
FormattedString | 指定された書式文字列でフォーマットされた文字列値を返します。 |
Random | 指定された範囲内のランダムな値を生成します。 |
Sequence | 指定された範囲内のシーケンシャルな値を生成します。 |
DataGeneratorTupleField
Field Type | Summary |
---|---|
DbQuery | 指定されたデータベースクエリーから複数のフィールドの値を読み取って返します。 |
DirectProduct | 指定された複数のフィールドの値を生成し、それらの直積を返します。 |
EachTuple | 指定された複数のフィールドの値の組み合わせを順番に返します。 |
DataGeneratorAdditionalField
Field Type | Summary |
---|---|
Expression | 生成されたデータレコードを引数として受け取り、指定された式の戻り値を返します。 |
DataGeneratorAdditionalTupleField
Field Type | Summary |
---|---|
Join | 生成されたデータレコードを引数として受け取り、キーとするフィールドの値に対応する値をディクショナリまたはルックアップから取得して返します。 |
JoinDbQuery | 生成されたデータレコードを引数として受け取り、キーとするフィールドの値に対応する値をデータクエリーから取得して返します。 |
まとめ
テストデータを作成したり、ユニットテストに対応するテストデータの内容が不意に変更されないように維持したり、テストデータにまつわる苦労は少しでも減らしたいものです。テストデータを効率的に作成できるかどうかは、テストの精度を大きく左右すると考えています。
今後はこのライブラリを利用したモックやテストデータ生成ツールを考えてみたいと思います。