bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] EF Core の更新と削除をテストする

[C#] EF Core の更新と削除をテストする

Aug 17, 2026 C# , .NET , Entity Framework Core , テスト bucket-sort

前回は、EF Core の追加処理をテストしました。

今回は、更新と削除をテストします。
ここには、データアクセス層で特に壊れやすい要素が集まっています。

  • 追跡中のエンティティを更新する
  • 追跡していないエンティティを更新する
  • 一部の列だけ更新する
  • 関連データを持つ行を削除する
  • 削除制約の失敗を扱う
  • 同時実行の競合を検出する

問い合わせや追加よりも、データの破壊につながりやすい処理です。
テストで動作を固定しておく価値があります。

追跡ありの更新をテストする

まず、コンテキストで取得したエンティティを変更して保存する例です。

public async Task RenameAsync(int id, string color, string petName)
{
    var car = await _context.Cars
        .SingleAsync(car => car.Id == id);

    car.Rename(car.Make, color, petName);

    await _context.SaveChangesAsync();
}

テストです。

public sealed class CarUpdateTests : DatabaseTestBase
{
    [Fact]
    public async Task 追跡中の車を更新できる()
    {
        var car = await Context.Cars.FirstAsync();
        var repository = new CarRepository(Context);

        await repository.RenameAsync(car.Id, "Purple", "Violet");

        await using var verificationContext =
            new AutoLotContext(TestDbContextOptions.Create());

        var saved = await verificationContext.Cars
            .AsNoTracking()
            .SingleAsync(item => item.Id == car.Id);

        Assert.Equal("Purple", saved.Color);
        Assert.Equal("Violet", saved.PetName);
    }
}

別の DbContext で読み直しているため、追跡中の値ではなく、保存された値を確認できます。

追跡なしの更新をテストする

画面や API から更新内容だけが返ってくる場合、エンティティを追跡していない状態で更新することがあります。

public async Task UpdateDetachedAsync(UpdateCarRequest request)
{
    var car = new Car(
        request.Make,
        request.Color,
        request.PetName);

    _context.Attach(car);
    _context.Entry(car).Property("Id").CurrentValue = request.Id;
    _context.Entry(car).State = EntityState.Modified;

    await _context.SaveChangesAsync();
}

この例は概念を示すためのものです。
実際には、主キーを設定できるコンストラクターや内部メソッドを用意するなど、エンティティ設計に合わせて調整します。

追跡なし更新では、意図しない列まで更新しないようにすることが大切です。
一部の列だけを更新するなら、次のように明示します。

public async Task ChangePetNameAsync(int id, string petName)
{
    var car = new CarPatch { Id = id, PetName = petName };

    _context.Attach(car);
    _context.Entry(car).Property(item => item.PetName).IsModified = true;

    await _context.SaveChangesAsync();
}

テストでは、変更した列と変更していない列の両方を確認します。

[Fact]
public async Task 愛称だけを更新できる()
{
    var car = await Context.Cars.AsNoTracking().FirstAsync();
    var repository = new CarRepository(Context);

    await repository.ChangePetNameAsync(car.Id, "NewName");

    await using var verificationContext =
        new AutoLotContext(TestDbContextOptions.Create());

    var saved = await verificationContext.Cars
        .AsNoTracking()
        .SingleAsync(item => item.Id == car.Id);

    Assert.Equal("NewName", saved.PetName);
    Assert.Equal(car.Make, saved.Make);
    Assert.Equal(car.Color, saved.Color);
}

部分更新のテストでは、「変わるべきもの」と「変わってはいけないもの」を両方見るのがコツです。

削除をテストする

基本的な削除メソッドです。

public async Task DeleteAsync(int id)
{
    var car = await _context.Cars
        .SingleAsync(car => car.Id == id);

    _context.Cars.Remove(car);
    await _context.SaveChangesAsync();
}

テストです。

[Fact]
public async Task 車を削除できる()
{
    var car = await Context.Cars
        .Where(car => !car.Orders.Any())
        .FirstAsync();

    var repository = new CarRepository(Context);

    await repository.DeleteAsync(car.Id);

    var exists = await Context.Cars.AnyAsync(item => item.Id == car.Id);

    Assert.False(exists);
}

注文を持たない車を選んでいるのは、外部キー制約による失敗を避けるためです。
削除テストでは、関連データの有無を意識する必要があります。

削除制約の失敗をテストする

注文を持つ車を削除しようとすると、外部キー制約で失敗する設計にしている場合があります。

[Fact]
public async Task 注文を持つ車を削除すると失敗する()
{
    var car = await Context.Cars
        .Include(car => car.Orders)
        .FirstAsync(car => car.Orders.Any());

    var repository = new CarRepository(Context);

    await Assert.ThrowsAsync<DataStoreException>(() =>
        repository.DeleteAsync(car.Id));
}

リポジトリ側では、EF Core の例外を独自例外に変換します。

public async Task DeleteAsync(int id)
{
    try
    {
        var car = await _context.Cars.SingleAsync(car => car.Id == id);
        _context.Cars.Remove(car);
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateException ex)
    {
        throw new DataStoreException(
            "関連データが存在するため削除できません。", ex);
    }
}

テストで確認したいのは、データベースの細かいエラー番号ではありません。
アプリケーションとして、削除できない状態を適切な例外に変換できているかです。

論理削除をテストする

物理削除ではなく、削除済みフラグを立てる設計もあります。

public async Task SoftDeleteAsync(int id)
{
    var car = await _context.Cars.SingleAsync(car => car.Id == id);

    car.MarkAsDeleted();

    await _context.SaveChangesAsync();
}

テストでは、通常の一覧から消えることと、データ自体は残っていることを確認します。

[Fact]
public async Task 論理削除した車は通常の問い合わせから除外される()
{
    var car = await Context.Cars.FirstAsync();
    var repository = new CarRepository(Context);

    await repository.SoftDeleteAsync(car.Id);

    var visible = await Context.Cars.AnyAsync(item => item.Id == car.Id);

    var existsIgnoringFilter = await Context.Cars
        .IgnoreQueryFilters()
        .AnyAsync(item => item.Id == car.Id);

    Assert.False(visible);
    Assert.True(existsIgnoringFilter);
}

グローバルフィルターを使っている場合、IgnoreQueryFilters() で実データの存在を確認できます。

更新時の同時実行をテストする

同時実行制御では、2 つの DbContext を使って競合を再現します。

[Fact]
public async Task 他の利用者が更新した場合は競合として扱う()
{
    var carId = await Context.Cars
        .Select(car => car.Id)
        .FirstAsync();

    await using var firstContext =
        new AutoLotContext(TestDbContextOptions.Create());

    await using var secondContext =
        new AutoLotContext(TestDbContextOptions.Create());

    var firstCar = await firstContext.Cars.SingleAsync(car => car.Id == carId);
    var secondCar = await secondContext.Cars.SingleAsync(car => car.Id == carId);

    firstCar.Rename(firstCar.Make, "Red", firstCar.PetName);
    await firstContext.SaveChangesAsync();

    secondCar.Rename(secondCar.Make, "Blue", secondCar.PetName);

    await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() =>
        secondContext.SaveChangesAsync());
}

このテストが動くには、エンティティに同時実行トークンが設定されている必要があります。

builder.Property(car => car.RowVersion)
    .IsRowVersion();

競合は、同じデータを別々のコンテキストで読み、それぞれが保存しようとしたときに起きます。
テストでも、その状況を作る必要があります。

例外変換までテストする

リポジトリで同時実行例外を独自例外へ変換しているなら、そこまで確認します。

public async Task SaveChangesAsync()
{
    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        throw new RecordChangedException(
            "他の利用者によってデータが変更されています。", ex);
    }
}

テストでは、独自例外が投げられることを確認します。

[Fact]
public async Task 同時実行の競合は独自例外に変換される()
{
    var carId = await Context.Cars
        .Select(car => car.Id)
        .FirstAsync();

    await using var firstContext =
        new AutoLotContext(TestDbContextOptions.Create());

    await using var secondContext =
        new AutoLotContext(TestDbContextOptions.Create());

    var firstRepository = new CarRepository(firstContext);
    var secondRepository = new CarRepository(secondContext);

    var firstCar = await firstContext.Cars.SingleAsync(car => car.Id == carId);
    var secondCar = await secondContext.Cars.SingleAsync(car => car.Id == carId);

    firstCar.Rename(firstCar.Make, "Red", firstCar.PetName);
    await firstRepository.SaveChangesAsync();

    secondCar.Rename(secondCar.Make, "Blue", secondCar.PetName);

    await Assert.ThrowsAsync<RecordChangedException>(() =>
        secondRepository.SaveChangesAsync());
}

呼び出し側が依存するのは、EF Core の例外ではなく、アプリケーションの例外です。
その境界をテストしておくと、内部実装を変えても外側の振る舞いを保ちやすくなります。

更新と削除のテストで見ること

更新と削除では、次の観点を確認します。

  • 変更した値が保存される
  • 変更していない値が壊れない
  • 別の DbContext から保存結果を確認できる
  • 削除できるデータは削除できる
  • 削除できないデータは適切に失敗する
  • 論理削除なら通常問い合わせから除外される
  • 同時実行の競合を再現できる
  • EF Core の例外をアプリケーションの例外へ変換できる

データアクセス層のテストは、少し準備が重いです。
しかし、保存や削除のような重要処理をテストしておくと、後からリファクタリングするときの安心感が大きくなります。

ここまでで、データアクセス層を作るだけでなく、実際に動くことを確認する流れまで見てきました。
テストは、設計の付録ではありません。設計を長く保つための支えです。

C# .NET Entity Framework Core XUnit 更新 削除 同時実行制御
← [C#] EF Core の追加処理をテストする [WPF] WPF が生まれた背景 →

Related Posts

  • [C#] EF Core の追加処理をテストする Aug 16, 2026
  • [C#] EF Core の問い合わせ処理をテストする Aug 15, 2026
  • [C#] EF Core のデータアクセス層をテストする準備 Aug 14, 2026
  • [C#] EF Core の実務向け機能を理解する Aug 3, 2026

Table of Contents

  • 追跡ありの更新をテストする
  • 追跡なしの更新をテストする
  • 削除をテストする
  • 削除制約の失敗をテストする
  • 論理削除をテストする
  • 更新時の同時実行をテストする
  • 例外変換までテストする
  • 更新と削除のテストで見ること

Recent Posts

  • [WPF] XAML の構文を理解する: 名前空間とキーワード Aug 23, 2026
  • [WPF] Window クラスを支える基底クラス Aug 22, 2026
  • [WPF] Window クラスの役割と上位クラス Aug 21, 2026
  • [WPF] Application クラスの役割 Aug 20, 2026
  • [WPF] WPF の主要アセンブリと名前空間 Aug 19, 2026

Categories

  • C#165
  • .NET164
  • AWS27
  • Entity Framework Core24
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • Data Access6
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • テスト4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1

Tags

  • C#
  • .NET
  • Entity Framework Core
  • AWS
  • Laravel
  • コレクション
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • LINQ
  • WPF
  • Apache
  • SQL Server
  • System.Collections.Generic
  • デリゲート
  • リフレクション
  • ADO.NET
  • Code Snippet
  • DbContext
  • DynamoDB
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メタデータ
  • メモリ管理
  • CIL
  • DoS
  • Nginx
  • WordPress
  • アセンブリ
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • Migration
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
Powered by Hugo & Explore Theme.