前回は、EF Core の問い合わせ処理をテストしました。
今回は、追加処理をテストします。
追加処理は一見単純です。
エンティティを作り、Add() して、SaveChangesAsync() を呼ぶだけに見えます。
しかし実務では、次のような違いが重要になります。
- 1 件だけ追加する
- 複数件をまとめて追加する
- 関連データも一緒に追加する
- 既存データと関連付けて追加する
- 自動採番された主キーを使う
- 保存後の状態を別の
DbContextで確認する
テストでは、これらを分けて確認します。
1 件追加をテストする
まず、車を 1 件追加するメソッドです。
public async Task<int> CreateAsync(CreateCarRequest request)
{
var car = new Car(
request.Make,
request.Color,
request.PetName);
_context.Cars.Add(car);
await _context.SaveChangesAsync();
return car.Id;
}
テストです。
public sealed class CarCreateTests : DatabaseTestBase
{
[Fact]
public async Task 車を1件追加できる()
{
var repository = new CarRepository(Context);
var id = await repository.CreateAsync(new CreateCarRequest
{
Make = "Nissan",
Color = "Silver",
PetName = "Leaf"
});
var saved = await Context.Cars.SingleAsync(car => car.Id == id);
Assert.Equal("Nissan", saved.Make);
Assert.Equal("Silver", saved.Color);
Assert.Equal("Leaf", saved.PetName);
}
}
戻り値の ID を使って、保存されたデータを取り直しています。
追加処理では、採番された主キーが正しく返ることも重要です。
別の DbContext で確認する
同じ DbContext で確認すると、追跡中のエンティティを見ているだけになる場合があります。
保存後、本当にデータベースに入ったことを確認したいなら、別の DbContext で読み直すと安心です。
[Fact]
public async Task 追加した車を別のコンテキストから読み取れる()
{
var repository = new CarRepository(Context);
var id = await repository.CreateAsync(new CreateCarRequest
{
Make = "Mazda",
Color = "Red",
PetName = "Roadster"
});
await using var verificationContext =
new AutoLotContext(TestDbContextOptions.Create());
var saved = await verificationContext.Cars
.AsNoTracking()
.SingleAsync(car => car.Id == id);
Assert.Equal("Mazda", saved.Make);
}
統合テストでは、同じコンテキストの追跡状態に助けられてしまうことがあります。
別コンテキストで確認すると、データベースに保存された事実をより直接確認できます。
複数件追加をテストする
複数件を追加するメソッドです。
public async Task AddRangeAsync(IEnumerable<CreateCarRequest> requests)
{
var cars = requests.Select(request => new Car(
request.Make,
request.Color,
request.PetName));
await _context.Cars.AddRangeAsync(cars);
await _context.SaveChangesAsync();
}
テストです。
[Fact]
public async Task 複数の車をまとめて追加できる()
{
var before = await Context.Cars.CountAsync();
var repository = new CarRepository(Context);
await repository.AddRangeAsync(new[]
{
new CreateCarRequest
{
Make = "Subaru",
Color = "Blue",
PetName = "Impreza"
},
new CreateCarRequest
{
Make = "Suzuki",
Color = "Yellow",
PetName = "Swift"
}
});
var after = await Context.Cars.CountAsync();
Assert.Equal(before + 2, after);
}
件数だけでなく、必要なら追加された内容も確認します。
var names = await Context.Cars
.Where(car => car.Make == "Subaru" || car.Make == "Suzuki")
.Select(car => car.Make)
.ToListAsync();
Assert.Contains("Subaru", names);
Assert.Contains("Suzuki", names);
関連データも一緒に追加する
車と注文を同時に追加するメソッドです。
public async Task<int> CreateWithOrderAsync(
CreateCarRequest carRequest,
string customerName)
{
var car = new Car(
carRequest.Make,
carRequest.Color,
carRequest.PetName);
car.Orders.Add(new Order
{
CustomerName = customerName,
OrderDate = DateTime.UtcNow
});
_context.Cars.Add(car);
await _context.SaveChangesAsync();
return car.Id;
}
テストです。
[Fact]
public async Task 車と注文をまとめて追加できる()
{
var repository = new CarRepository(Context);
var id = await repository.CreateWithOrderAsync(
new CreateCarRequest
{
Make = "Daihatsu",
Color = "Green",
PetName = "Copen"
},
"山田");
var saved = await Context.Cars
.Include(car => car.Orders)
.SingleAsync(car => car.Id == id);
Assert.Single(saved.Orders);
Assert.Equal("山田", saved.Orders[0].CustomerName);
}
EF Core は、追加されたオブジェクトグラフを見て、関連するエンティティも追加対象にします。
テストでは、親だけでなく子のデータも保存されたことを確認します。
既存データに関連付けて追加する
既存の車に注文を追加する場合を考えます。
public async Task<int> AddOrderAsync(int carId, string customerName)
{
var order = new Order
{
CarId = carId,
CustomerName = customerName,
OrderDate = DateTime.UtcNow
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return order.Id;
}
テストです。
[Fact]
public async Task 既存の車に注文を追加できる()
{
var car = await Context.Cars.FirstAsync();
var before = await Context.Orders.CountAsync(order => order.CarId == car.Id);
var repository = new CarRepository(Context);
await repository.AddOrderAsync(car.Id, "高橋");
var after = await Context.Orders.CountAsync(order => order.CarId == car.Id);
Assert.Equal(before + 1, after);
}
この方法では、車を読み込まずに外部キーだけで関連付けています。
単純な追加なら、この方が効率的です。
Attach を使った関連付けをテストする
既存エンティティを「すでにあるもの」としてコンテキストに認識させたい場合、Attach() を使います。
public async Task<int> AddOrderUsingAttachAsync(int carId, string customerName)
{
var car = new CarReference { Id = carId };
_context.Attach(car);
var order = new Order
{
CarId = carId,
CustomerName = customerName,
OrderDate = DateTime.UtcNow
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return order.Id;
}
ここでは説明のために、主キーだけを持つ参照用の考え方を示しています。
実際のエンティティのコンストラクターやプロパティ設計によっては、この書き方を調整します。
テストでは、既存の車が重複追加されないことを確認します。
[Fact]
public async Task 既存の車を重複させずに注文を追加できる()
{
var car = await Context.Cars.FirstAsync();
var carCountBefore = await Context.Cars.CountAsync();
var repository = new CarRepository(Context);
await repository.AddOrderAsync(car.Id, "伊藤");
var carCountAfter = await Context.Cars.CountAsync();
var orderExists = await Context.Orders.AnyAsync(order =>
order.CarId == car.Id && order.CustomerName == "伊藤");
Assert.Equal(carCountBefore, carCountAfter);
Assert.True(orderExists);
}
追加処理では、関連先まで新規追加してしまう誤りが起きることがあります。
既存データとの関連付けでは、親の件数が増えていないことも確認すると安全です。
入力値の検証をテストする
エンティティやリポジトリで入力値を検証しているなら、異常系もテストします。
[Fact]
public async Task メーカー名が空なら追加できない()
{
var repository = new CarRepository(Context);
await Assert.ThrowsAsync<ArgumentException>(() =>
repository.CreateAsync(new CreateCarRequest
{
Make = "",
Color = "Gray",
PetName = "NoName"
}));
}
このテストは、データベース制約ではなく、アプリケーション側のルールを確認しています。
どの層で検証するかは設計によります。
ただ、重要なルールはテストで明示しておくと、後から変更したときに気づきやすくなります。
追加処理のテストで見ること
追加処理では、次の観点を確認します。
- 保存後に主キーが採番される
- 別の
DbContextから読み取れる - 追加件数が正しい
- 関連データも保存される
- 既存データが重複追加されない
- 入力値の検証が働く
- 保存時の共通処理が動く
保存時に作成日時や更新日時を自動設定しているなら、それも確認できます。
Assert.NotEqual(default, saved.CreatedAt);
Assert.NotEqual(default, saved.UpdatedAt);
追加処理は、データの入口です。
ここをテストしておくと、後続の更新や削除のテストも組み立てやすくなります。
次回は、更新、削除、同時実行制御のテストを見ていきます。