ASP.NET Core でコントローラーのロジックの単体テストを行う

作成者: Steve Smith

単体テストでは、アプリの一部をインフラストラクチャや依存関係から切り離してテストすることが必要とされます。 コントローラー ロジックの単体テストを行うと、単一のアクションの内容のみがテストされ、その依存関係やフレームワーク自体の動作はテストされません。

コントローラーの単体テスト

コントローラーのアクションの単体テストは、コントローラーの動作に注目するように設定します。 コントローラーの単体テストでは、フィルタールーティングモデル バインドなどのシナリオは除外します。 まとまって要求に応答するコンポーネント間のインタラクションをカバーするテストは、統合テストによって処理されます。 統合サイトの詳細情報については、ASP.NET Core での総合テスト を参照してください。

カスタム フィルターやルートを作成している場合は、コントローラーの特定のアクションに対するテストの一部としてではなく、単体テストを切り離して実行します。

コントローラーの単体テストを理解するために、次のサンプル アプリ内のコントローラーを確認してください。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

Home コントローラーは、ブレーンストーミング セッションの一覧を表示し、POST 要求で新しいブレーンストーミング セッションを作成できるようにします。

public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public HomeController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index()
    {
        var sessionList = await _sessionRepository.ListAsync();

        var model = sessionList.Select(session => new StormSessionViewModel()
        {
            Id = session.Id,
            DateCreated = session.DateCreated,
            Name = session.Name,
            IdeaCount = session.Ideas.Count
        });

        return View(model);
    }

    public class NewSessionModel
    {
        [Required]
        public string SessionName { get; set; }
    }

    [HttpPost]
    public async Task<IActionResult> Index(NewSessionModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            await _sessionRepository.AddAsync(new BrainstormSession()
            {
                DateCreated = DateTimeOffset.Now,
                Name = model.SessionName
            });
        }

        return RedirectToAction(actionName: nameof(Index));
    }
}

上記のコントローラーの説明:

  • 明示的な依存関係の原則に従います。
  • 依存関係の注入 (DI) を予期して、IBrainstormSessionRepository のインスタンスを提供します。
  • モック オブジェクト フレームワークを使用するモックされた IBrainstormSessionRepository サービスでテストできます (Moq サービスなど)。 モック オブジェクトは、テストで使用されるプロパティとメソッドの動作が事前定義されている加工オブジェクトです。 詳細については、「Introduction to integration tests」(統合テストの概要) を参照してください。

HTTP GET Index メソッドにはループや分岐はなく、1 つのメソッドを呼び出すだけです。 このアクションに対する単体テストでは、以下を行います。

  • IBrainstormSessionRepository メソッドを使用する GetTestSessions サービスをモックする。 GetTestSessions が日付とセッション名を持つ 2 つのモック ブレーンストーミング セッションを作成する。
  • Index メソッドを実行する。
  • メソッドによって返された結果に関するアサーションを行う。
    • ViewResult が返される。
    • ViewDataDictionary.ModelStormSessionViewModel
    • ViewDataDictionary.Modelに格納された 2 つのブレーンストーミング セッションがある。
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
    var sessions = new List<BrainstormSession>();
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 2),
        Id = 1,
        Name = "Test One"
    });
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 1),
        Id = 2,
        Name = "Test Two"
    });
    return sessions;
}

Home コントローラーの HTTP POST Index メソッドのテストでは、以下が検証されます。

  • ModelState.IsValidfalse の場合、アクション メソッドは、400 Bad RequestViewResult と適切なデータを返す。
  • ModelState.IsValidtrue の場合:
    • リポジトリの Add メソッドが呼び出される。
    • RedirectToActionResult と適切な引数が返される。

下の最初のテストに示すように、AddModelError を使用してエラーを追加することで、モデルの無効状態をテストできます。

[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);
    controller.ModelState.AddModelError("SessionName", "Required");
    var newSession = new HomeController.NewSessionModel();

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
    Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
        .Returns(Task.CompletedTask)
        .Verifiable();
    var controller = new HomeController(mockRepo.Object);
    var newSession = new HomeController.NewSessionModel()
    {
        SessionName = "Test Name"
    };

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
    Assert.Null(redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
    mockRepo.Verify();
}

ModelState が有効でない場合は、GET 要求と同じ ViewResult が返されます。 このテストでは、無効なモデルを渡すことは試していません。 モデル バインドが実行されていないため、無効なモデルを渡すことは効果的なアプローチではありません (ただし、統合テストではモデル バインドが使用されます)。 ここでは、モデル バインドはテストされていません。 これらの単体テストでは、アクション メソッドのコードだけをテストしています。

2 番目のテストでは、ModelState が有効な場合の検証を行います。

  • 新しい BrainstormSession が追加される (リポジトリ経由)。
  • メソッドが RedirectToActionResult と予期されるプロパティを返す。

呼び出されないモックの呼び出しは通常は無視されますが、Setup 呼び出しの最後で Verifiable を呼び出すと、テスト内でのモック検証が許可されます。 これは mockRepo.Verify の呼び出しで実行され、予期されるメソッドが呼び出されないとテストは失敗します。

Note

このサンプルで使われている Moq ライブラリにより、検証可能な ("厳密な") モックと検証不可能なモック ("厳密でない" モックまたはスタブとも呼ばれます) を混在させることができます。 詳しくは、Moq の「Customizing Mock Behavior」(モックの動作のカスタマイズ) をご覧ください。

サンプル アプリの SessionController は、特定のブレーンストーミング セッションに関する情報を表示します。 このコントローラーには、無効な id 値を処理するロジックが含まれています (これらのシナリオをカバーするために、次のサンプルには 2 つの return シナリオがあります)。 最後の return ステートメントは、新しい StormSessionViewModel をビュー (Controllers/SessionController.cs) に返します。

public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public SessionController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index(int? id)
    {
        if (!id.HasValue)
        {
            return RedirectToAction(actionName: nameof(Index), 
                controllerName: "Home");
        }

        var session = await _sessionRepository.GetByIdAsync(id.Value);
        if (session == null)
        {
            return Content("Session not found.");
        }

        var viewModel = new StormSessionViewModel()
        {
            DateCreated = session.DateCreated,
            Name = session.Name,
            Id = session.Id
        };

        return View(viewModel);
    }
}

Session コントローラーの Indexアクション内に各 return シナリオ用の 1 つのテストを含む単体テスト:

[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
    // Arrange
    var controller = new SessionController(sessionRepository: null);

    // Act
    var result = await controller.Index(id: null);

    // Assert
    var redirectToActionResult = 
        Assert.IsType<RedirectToActionResult>(result);
    Assert.Equal("Home", redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var contentResult = Assert.IsType<ContentResult>(result);
    Assert.Equal("Session not found.", contentResult.Content);
}

[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSessions().FirstOrDefault(
            s => s.Id == testSessionId));
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsType<StormSessionViewModel>(
        viewResult.ViewData.Model);
    Assert.Equal("Test One", model.Name);
    Assert.Equal(2, model.DateCreated.Day);
    Assert.Equal(testSessionId, model.Id);
}

Ideas コントローラーに移ると、アプリは、機能を Web API として api/ideas ルートで公開します。

  • ブレーンストーミング セッションに関連付けられたアイデアの一覧 (IdeaDTO) が、ForSession メソッドによって返される。
  • Create メソッドが新しいアイデアをセッションに追加する。
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);
    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);
    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return Ok(session);
}

API の呼び出しでビジネス ドメイン エンティティを直接返すことは避けてください。 ドメイン エンティティ:

  • 多くの場合、クライアントが必要とするもの以上のデータを含みます。
  • アプリの内部ドメイン モデルと一般公開されている API が不必要に結合します。

ドメイン エンティティとクライアントに返される型の間のマッピングは、以下のように実行できます。

  • サンプル アプリで使用しているように、LINQ Select を使用して手動で実行します。 詳細については、「LINQ (統合言語クエリ)」を参照してください。
  • AutoMapper などのライブラリを使用して自動的に実行します。

次のサンプル アプリは、Ideas コントローラーの CreateForSession API メソッドの単体テストを示します。

このサンプル アプリには、2 つの ForSession テストが含まれています。 最初のテストでは、無効なセッションに対して ForSessionNotFoundObjectResult (HTTP Not Found) を返すかどうかを判断します。

[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
    Assert.Equal(testSessionId, notFoundObjectResult.Value);
}

2 番目の ForSession テストでは、有効なセッションに対して ForSession がセッションのアイデアの一覧 (<List<IdeaDTO>>) を返すかどうかを判断します。 これらのチェックでは、最初のアイデアを調べて、その Name プロパティが正しいことも確認します。

[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}

ModelState が無効の場合の Create メソッドの動作をテストするため、サンプル アプリでは、テストの一部としてコントローラーにモデル エラーを追加します。 単体テストでは、モデルの検証またはモデル バインドのテストを試さないでください。無効な ModelState に遭遇したときのアクション メソッドの動作だけをテストしてください:

[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.Create(model: null);

    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
}

Create の 2 番目のテストでは、リポジトリが null を返すことに依存しているため、null を返すようにモック リポジトリが構成されます。 テスト データベース (メモリ内またはそれ以外の場所) を作成し、この結果を返すクエリを作成する必要はありません。 サンプル コードに示すように、このテストは単一のステートメントで実行できます。

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.Create(new NewIdeaModel());

    // Assert
    Assert.IsType<NotFoundObjectResult>(result);
}

3 番目の Create テスト (Create_ReturnsNewlyCreatedIdeaForSession) では、リポジトリの UpdateAsync メソッドが呼び出されることを検証します。 モックが Verifiable で呼び出され、モック リポジトリの Verify メソッドが呼び出されて、検証可能なメソッドが実行されることを確認します。 UpdateAsync メソッドがデータを保存したことを確認するのは、単体テストの役割ではありません。それは統合テストで実行できます。

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.Create(newIdea);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnSession.Ideas.Count());
    Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

ActionResult<T> をテストする

ActionResult<T> (ActionResult<TValue>) は、ActionResult から派生した型を返したり、特定の型を返したりできます。

サンプル アプリには、特定のセッション id に対して List<IdeaDTO> を返すメソッドが含まれています。 セッションに id が存在しない場合、コントローラーは NotFound を返します。

[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);

    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return result;
}

ApiIdeasControllerTestsには、ForSessionActionResult コントローラーの 2 つのテストが含まれています。

最初のテストでは、コントローラーは ActionResult を返すが、存在しないセッション id の存在しないアイデアの一覧は返さないことを確認します。

[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    var nonExistentSessionId = 999;

    // Act
    var result = await controller.ForSessionActionResult(nonExistentSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

有効なセッション id に対する 2 番目のテストでは、メソッドが以下を返すことを確認します。

  • List<IdeaDTO> 型の ActionResult
  • ActionResult<T>.ValueList<IdeaDTO> 型。
  • 一覧の最初の項目は、モック セッションに格納されているアイデアと一致する有効なアイデア (GetTestSession の呼び出しによって取得)。
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSessionActionResult(testSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}

このサンプル アプリには、特定のセッションで新しい Idea を作成するメソッドも含まれています。 コントローラーは以下を返します。

  • 無効なモデルに対する BadRequest
  • セッションが存在しない場合は NotFound
  • セッションが新しいアイデアで更新された場合は CreatedAtAction
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);

    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}

ApiIdeasControllerTests には、CreateActionResult の 3 つのテストが含まれます。

最初のテストでは、無効なモデルに対して BadRequest が返されることを確認します。

[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.CreateActionResult(model: null);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

2 番目のテストでは、セッションが存在しない場合は NotFound が返されることを確認します。

[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var nonExistentSessionId = 999;
    string testName = "test name";
    string testDescription = "test description";
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = nonExistentSessionId
    };

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

有効なセッション id に対しては、最後のテストで以下を確認します。

  • メソッドが BrainstormSession 型の ActionResult を返す。
  • ActionResult<T>.ResultCreatedAtActionResultCreatedAtActionResult201 Created 応答に類似した Location ヘッダー付きの応答である。
  • ActionResult<T>.ValueBrainstormSession 型。
  • セッション UpdateAsync(testSession) を更新するモック 呼び出しが呼び出された。 Verifiable メソッド呼び出しは、アサーション内で mockRepo.Verify() を実行することでチェックされます。
  • セッションに対して 2 つの Idea オブジェクトが返された。
  • 最後の項目 (UpdateAsync へのモック呼び出しによって追加された Idea) が、テスト中にセッションに追加された newIdea と一致する。
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
    var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnValue.Ideas.Count());
    Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}

コントローラーは、すべての ASP.NET Core MVC アプリで中心的な役割を担います。 そのため、コントローラーが意図するとおりに動作するという信頼が必要です。 自動テストによって、アプリが運用環境にデプロイされる前にエラーを検出できます。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

コントローラー ロジックの単体テスト

単体テストでは、アプリの一部をインフラストラクチャや依存関係から切り離してテストすることが必要とされます。 コントローラー ロジックの単体テストを行うと、単一のアクションの内容のみがテストされ、その依存関係やフレームワーク自体の動作はテストされません。

コントローラーのアクションの単体テストは、コントローラーの動作に注目するように設定します。 コントローラーの単体テストでは、フィルタールーティングモデル バインドなどのシナリオは除外します。 まとまって要求に応答するコンポーネント間のインタラクションをカバーするテストは、統合テストによって処理されます。 統合サイトの詳細情報については、ASP.NET Core での総合テスト を参照してください。

カスタム フィルターやルートを作成している場合は、コントローラーの特定のアクションに対するテストの一部としてではなく、単体テストを切り離して実行します。

コントローラーの単体テストを理解するために、次のサンプル アプリ内のコントローラーを確認してください。 Home コントローラーは、ブレーンストーミング セッションの一覧を表示し、POST 要求で新しいブレーンストーミング セッションを作成できるようにします。

public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public HomeController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index()
    {
        var sessionList = await _sessionRepository.ListAsync();

        var model = sessionList.Select(session => new StormSessionViewModel()
        {
            Id = session.Id,
            DateCreated = session.DateCreated,
            Name = session.Name,
            IdeaCount = session.Ideas.Count
        });

        return View(model);
    }

    public class NewSessionModel
    {
        [Required]
        public string SessionName { get; set; }
    }

    [HttpPost]
    public async Task<IActionResult> Index(NewSessionModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            await _sessionRepository.AddAsync(new BrainstormSession()
            {
                DateCreated = DateTimeOffset.Now,
                Name = model.SessionName
            });
        }

        return RedirectToAction(actionName: nameof(Index));
    }
}

上記のコントローラーの説明:

  • 明示的な依存関係の原則に従います。
  • 依存関係の注入 (DI) を予期して、IBrainstormSessionRepository のインスタンスを提供します。
  • モック オブジェクト フレームワークを使用するモックされた IBrainstormSessionRepository サービスでテストできます (Moq サービスなど)。 モック オブジェクトは、テストで使用されるプロパティとメソッドの動作が事前定義されている加工オブジェクトです。 詳細については、「Introduction to integration tests」(統合テストの概要) を参照してください。

HTTP GET Index メソッドにはループや分岐はなく、1 つのメソッドを呼び出すだけです。 このアクションに対する単体テストでは、以下を行います。

  • IBrainstormSessionRepository メソッドを使用する GetTestSessions サービスをモックする。 GetTestSessions が日付とセッション名を持つ 2 つのモック ブレーンストーミング セッションを作成する。
  • Index メソッドを実行する。
  • メソッドによって返された結果に関するアサーションを行う。
    • ViewResult が返される。
    • ViewDataDictionary.ModelStormSessionViewModel
    • ViewDataDictionary.Modelに格納された 2 つのブレーンストーミング セッションがある。
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
    var sessions = new List<BrainstormSession>();
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 2),
        Id = 1,
        Name = "Test One"
    });
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 1),
        Id = 2,
        Name = "Test Two"
    });
    return sessions;
}

Home コントローラーの HTTP POST Index メソッドのテストでは、以下が検証されます。

  • ModelState.IsValidfalse の場合、アクション メソッドは、400 Bad RequestViewResult と適切なデータを返す。
  • ModelState.IsValidtrue の場合:
    • リポジトリの Add メソッドが呼び出される。
    • RedirectToActionResult と適切な引数が返される。

下の最初のテストに示すように、AddModelError を使用してエラーを追加することで、モデルの無効状態をテストできます。

[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);
    controller.ModelState.AddModelError("SessionName", "Required");
    var newSession = new HomeController.NewSessionModel();

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
    Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
        .Returns(Task.CompletedTask)
        .Verifiable();
    var controller = new HomeController(mockRepo.Object);
    var newSession = new HomeController.NewSessionModel()
    {
        SessionName = "Test Name"
    };

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
    Assert.Null(redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
    mockRepo.Verify();
}

ModelState が有効でない場合は、GET 要求と同じ ViewResult が返されます。 このテストでは、無効なモデルを渡すことは試していません。 モデル バインドが実行されていないため、無効なモデルを渡すことは効果的なアプローチではありません (ただし、統合テストではモデル バインドが使用されます)。 ここでは、モデル バインドはテストされていません。 これらの単体テストでは、アクション メソッドのコードだけをテストしています。

2 番目のテストでは、ModelState が有効な場合の検証を行います。

  • 新しい BrainstormSession が追加される (リポジトリ経由)。
  • メソッドが RedirectToActionResult と予期されるプロパティを返す。

呼び出されないモックの呼び出しは通常は無視されますが、Setup 呼び出しの最後で Verifiable を呼び出すと、テスト内でのモック検証が許可されます。 これは mockRepo.Verify の呼び出しで実行され、予期されるメソッドが呼び出されないとテストは失敗します。

Note

このサンプルで使われている Moq ライブラリにより、検証可能な ("厳密な") モックと検証不可能なモック ("厳密でない" モックまたはスタブとも呼ばれます) を混在させることができます。 詳しくは、Moq の「Customizing Mock Behavior」(モックの動作のカスタマイズ) をご覧ください。

サンプル アプリの SessionController は、特定のブレーンストーミング セッションに関する情報を表示します。 このコントローラーには、無効な id 値を処理するロジックが含まれています (これらのシナリオをカバーするために、次のサンプルには 2 つの return シナリオがあります)。 最後の return ステートメントは、新しい StormSessionViewModel をビュー (Controllers/SessionController.cs) に返します。

public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public SessionController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index(int? id)
    {
        if (!id.HasValue)
        {
            return RedirectToAction(actionName: nameof(Index), 
                controllerName: "Home");
        }

        var session = await _sessionRepository.GetByIdAsync(id.Value);
        if (session == null)
        {
            return Content("Session not found.");
        }

        var viewModel = new StormSessionViewModel()
        {
            DateCreated = session.DateCreated,
            Name = session.Name,
            Id = session.Id
        };

        return View(viewModel);
    }
}

Session コントローラーの Indexアクション内に各 return シナリオ用の 1 つのテストを含む単体テスト:

[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
    // Arrange
    var controller = new SessionController(sessionRepository: null);

    // Act
    var result = await controller.Index(id: null);

    // Assert
    var redirectToActionResult = 
        Assert.IsType<RedirectToActionResult>(result);
    Assert.Equal("Home", redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var contentResult = Assert.IsType<ContentResult>(result);
    Assert.Equal("Session not found.", contentResult.Content);
}

[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSessions().FirstOrDefault(
            s => s.Id == testSessionId));
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsType<StormSessionViewModel>(
        viewResult.ViewData.Model);
    Assert.Equal("Test One", model.Name);
    Assert.Equal(2, model.DateCreated.Day);
    Assert.Equal(testSessionId, model.Id);
}

Ideas コントローラーに移ると、アプリは、機能を Web API として api/ideas ルートで公開します。

  • ブレーンストーミング セッションに関連付けられたアイデアの一覧 (IdeaDTO) が、ForSession メソッドによって返される。
  • Create メソッドが新しいアイデアをセッションに追加する。
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);
    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);
    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return Ok(session);
}

API の呼び出しでビジネス ドメイン エンティティを直接返すことは避けてください。 ドメイン エンティティ:

  • 多くの場合、クライアントが必要とするもの以上のデータを含みます。
  • アプリの内部ドメイン モデルと一般公開されている API が不必要に結合します。

ドメイン エンティティとクライアントに返される型の間のマッピングは、以下のように実行できます。

  • サンプル アプリで使用しているように、LINQ Select を使用して手動で実行します。 詳細については、「LINQ (統合言語クエリ)」を参照してください。
  • AutoMapper などのライブラリを使用して自動的に実行します。

次のサンプル アプリは、Ideas コントローラーの CreateForSession API メソッドの単体テストを示します。

このサンプル アプリには、2 つの ForSession テストが含まれています。 最初のテストでは、無効なセッションに対して ForSessionNotFoundObjectResult (HTTP Not Found) を返すかどうかを判断します。

[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
    Assert.Equal(testSessionId, notFoundObjectResult.Value);
}

2 番目の ForSession テストでは、有効なセッションに対して ForSession がセッションのアイデアの一覧 (<List<IdeaDTO>>) を返すかどうかを判断します。 これらのチェックでは、最初のアイデアを調べて、その Name プロパティが正しいことも確認します。

[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}

ModelState が無効の場合の Create メソッドの動作をテストするため、サンプル アプリでは、テストの一部としてコントローラーにモデル エラーを追加します。 単体テストでは、モデルの検証またはモデル バインドのテストを試さないでください。無効な ModelState に遭遇したときのアクション メソッドの動作だけをテストしてください:

[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.Create(model: null);

    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
}

Create の 2 番目のテストでは、リポジトリが null を返すことに依存しているため、null を返すようにモック リポジトリが構成されます。 テスト データベース (メモリ内またはそれ以外の場所) を作成し、この結果を返すクエリを作成する必要はありません。 サンプル コードに示すように、このテストは単一のステートメントで実行できます。

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.Create(new NewIdeaModel());

    // Assert
    Assert.IsType<NotFoundObjectResult>(result);
}

3 番目の Create テスト (Create_ReturnsNewlyCreatedIdeaForSession) では、リポジトリの UpdateAsync メソッドが呼び出されることを検証します。 モックが Verifiable で呼び出され、モック リポジトリの Verify メソッドが呼び出されて、検証可能なメソッドが実行されることを確認します。 UpdateAsync メソッドがデータを保存したことを確認するのは、単体テストの役割ではありません。それは統合テストで実行できます。

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.Create(newIdea);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnSession.Ideas.Count());
    Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

ActionResult<T> をテストする

ASP.NET Core 2.1 以降、ActionResult<T> (ActionResult<TValue>) で、ActionResult から派生する型または特定の型を返すことができます。

サンプル アプリには、特定のセッション id に対して List<IdeaDTO> を返すメソッドが含まれています。 セッションに id が存在しない場合、コントローラーは NotFound を返します。

[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);

    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return result;
}

ApiIdeasControllerTestsには、ForSessionActionResult コントローラーの 2 つのテストが含まれています。

最初のテストでは、コントローラーは ActionResult を返すが、存在しないセッション id の存在しないアイデアの一覧は返さないことを確認します。

[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    var nonExistentSessionId = 999;

    // Act
    var result = await controller.ForSessionActionResult(nonExistentSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

有効なセッション id に対する 2 番目のテストでは、メソッドが以下を返すことを確認します。

  • List<IdeaDTO> 型の ActionResult
  • ActionResult<T>.ValueList<IdeaDTO> 型。
  • 一覧の最初の項目は、モック セッションに格納されているアイデアと一致する有効なアイデア (GetTestSession の呼び出しによって取得)。
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSessionActionResult(testSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}

このサンプル アプリには、特定のセッションで新しい Idea を作成するメソッドも含まれています。 コントローラーは以下を返します。

  • 無効なモデルに対する BadRequest
  • セッションが存在しない場合は NotFound
  • セッションが新しいアイデアで更新された場合は CreatedAtAction
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);

    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}

ApiIdeasControllerTests には、CreateActionResult の 3 つのテストが含まれます。

最初のテストでは、無効なモデルに対して BadRequest が返されることを確認します。

[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.CreateActionResult(model: null);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

2 番目のテストでは、セッションが存在しない場合は NotFound が返されることを確認します。

[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var nonExistentSessionId = 999;
    string testName = "test name";
    string testDescription = "test description";
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = nonExistentSessionId
    };

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

有効なセッション id に対しては、最後のテストで以下を確認します。

  • メソッドが BrainstormSession 型の ActionResult を返す。
  • ActionResult<T>.ResultCreatedAtActionResultCreatedAtActionResult201 Created 応答に類似した Location ヘッダー付きの応答である。
  • ActionResult<T>.ValueBrainstormSession 型。
  • セッション UpdateAsync(testSession) を更新するモック 呼び出しが呼び出された。 Verifiable メソッド呼び出しは、アサーション内で mockRepo.Verify() を実行することでチェックされます。
  • セッションに対して 2 つの Idea オブジェクトが返された。
  • 最後の項目 (UpdateAsync へのモック呼び出しによって追加された Idea) が、テスト中にセッションに追加された newIdea と一致する。
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
    var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnValue.Ideas.Count());
    Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}

その他のリソース