ASP.NET Core のコントローラーのロジックをテストするTest controller logic in ASP.NET Core

作成者: Steve SmithBy Steve Smith

コントローラーは、すべての ASP.NET Core MVC アプリで中心的な役割を担います。Controllers play a central role in any ASP.NET Core MVC app. そのため、コントローラーが意図するとおりに動作するという信頼が必要です。As such, you should have confidence that controllers behave as intended. 自動テストによって、アプリが運用環境にデプロイされる前にエラーを検出できます。Automated tests can detect errors before the app is deployed to a production environment.

サンプル コードを表示またはダウンロードします (ダウンロード方法)。View or download sample code (how to download)

コントローラー ロジックの単体テストUnit tests of controller logic

単体テストでは、アプリの一部をインフラストラクチャや依存関係から切り離してテストすることが必要とされます。Unit tests involve testing a part of an app in isolation from its infrastructure and dependencies. コントローラー ロジックの単体テストを行うと、単一のアクションの内容のみがテストされ、その依存関係やフレームワーク自体の動作はテストされません。When unit testing controller logic, only the contents of a single action are tested, not the behavior of its dependencies or of the framework itself.

コントローラーのアクションの単体テストは、コントローラーの動作に注目するように設定します。Set up unit tests of controller actions to focus on the controller's behavior. コントローラーの単体テストでは、フィルタールーティングモデル バインドなどのシナリオは除外します。A controller unit test avoids scenarios such as filters, routing, and model binding. まとまって要求に応答するコンポーネント間のインタラクションをカバーするテストは、統合テストによって処理されます。Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. 統合テストの詳細については、「ASP.NET Core で統合テスト」を参照してください。For more information on integration tests, see ASP.NET Core で統合テスト.

カスタム フィルターやルートを作成している場合は、コントローラーの特定のアクションに対するテストの一部としてではなく、単体テストを切り離して実行します。If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.

コントローラーの単体テストを理解するために、次のサンプル アプリ内のコントローラーを確認してください。To demonstrate controller unit tests, review the following controller in the sample app. Home コントローラーは、ブレーンストーミング セッションの一覧を表示し、POST 要求で新しいブレーンストーミング セッションを作成できるようにします。The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:

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));
    }
}

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

  • 明示的な依存関係の原則に従います。Follows the Explicit Dependencies Principle.
  • 依存関係の注入 (DI) を予期して、IBrainstormSessionRepository のインスタンスを提供します。Expects dependency injection (DI) to provide an instance of IBrainstormSessionRepository.
  • モック オブジェクト フレームワークを使用するモックされた IBrainstormSessionRepository サービスでテストできます (Moq サービスなど)。Can be tested with a mocked IBrainstormSessionRepository service using a mock object framework, such as Moq. モック オブジェクトは、テストで使用されるプロパティとメソッドの動作が事前定義されている加工オブジェクトです。A mocked object is a fabricated object with a predetermined set of property and method behaviors used for testing. 詳細については、「Introduction to integration tests」(統合テストの概要) を参照してください。For more information, see Introduction to integration tests.

HTTP GET Index メソッドにはループや分岐はなく、1 つのメソッドを呼び出すだけです。The HTTP GET Index method has no looping or branching and only calls one method. このアクションに対する単体テストでは、以下を行います。The unit test for this action:

  • IBrainstormSessionRepository メソッドを使用する GetTestSessions サービスをモックする。Mocks the IBrainstormSessionRepository service using the GetTestSessions method. GetTestSessions が日付とセッション名を持つ 2 つのモック ブレーンストーミング セッションを作成する。GetTestSessions creates two mock brainstorm sessions with dates and session names.
  • Index メソッドを実行する。Executes the Index method.
  • メソッドによって返された結果に関するアサーションを行う。Makes assertions on the result returned by the method:
    • ViewResult が返される。A ViewResult is returned.
    • ViewDataDictionary.ModelStormSessionViewModelThe ViewDataDictionary.Model is a StormSessionViewModel.
    • ViewDataDictionary.Modelに格納された 2 つのブレーンストーミング セッションがある。There are two brainstorming sessions stored in the ViewDataDictionary.Model.
[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 メソッドのテストでは、以下が検証されます。The Home controller's HTTP POST Index method tests verifies that:

  • ModelState.IsValidfalse の場合、アクション メソッドは、400 Bad Request ViewResult と適切なデータを返す。When ModelState.IsValid is false, the action method returns a 400 Bad Request ViewResult with the appropriate data.
  • ModelState.IsValidtrue の場合:When ModelState.IsValid is true:
    • リポジトリの Add メソッドが呼び出される。The Add method on the repository is called.
    • RedirectToActionResult と適切な引数が返される。A RedirectToActionResult is returned with the correct arguments.

下の最初のテストに示すように、AddModelError を使用してエラーを追加することで、モデルの無効状態をテストできます。An invalid model state is tested by adding errors using AddModelError as shown in the first test below:

[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 が返されます。When ModelState isn't valid, the same ViewResult is returned as for a GET request. このテストでは、無効なモデルを渡すことは試していません。The test doesn't attempt to pass in an invalid model. モデル バインドが実行されていないため、無効なモデルを渡すことは効果的なアプローチではありません (ただし、統合テストではモデル バインドが使用されます)。Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). ここでは、モデル バインドはテストされていません。In this case, model binding isn't tested. これらの単体テストでは、アクション メソッドのコードだけをテストしています。These unit tests are only testing the code in the action method.

2 番目のテストでは、ModelState が有効な場合の検証を行います。The second test verifies that when the ModelState is valid:

  • 新しい BrainstormSession が追加される (リポジトリ経由)。A new BrainstormSession is added (via the repository).
  • メソッドが RedirectToActionResult と予期されるプロパティを返す。The method returns a RedirectToActionResult with the expected properties.

呼び出されないモックの呼び出しは通常は無視されますが、Setup 呼び出しの最後で Verifiable を呼び出すと、テスト内でのモック検証が許可されます。Mocked calls that aren't called are normally ignored, but calling Verifiable at the end of the setup call allows mock validation in the test. これは mockRepo.Verify の呼び出しで実行され、予期されるメソッドが呼び出されないとテストは失敗します。This is performed with the call to mockRepo.Verify, which fails the test if the expected method wasn't called.

注意

このサンプルで使われている Moq ライブラリにより、検証可能な ("厳密な") モックと検証不可能なモック ("厳密でない" モックまたはスタブとも呼ばれます) を混在させることができます。The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). 詳しくは、Moq の「Customizing Mock Behavior」(モックの動作のカスタマイズ) をご覧ください。Learn more about customizing Mock behavior with Moq.

サンプル アプリの SessionController は、特定のブレーンストーミング セッションに関する情報を表示します。SessionController in the sample app displays information related to a particular brainstorming session. このコントローラーには、無効な id 値を処理するロジックが含まれています (これらのシナリオをカバーするために、次のサンプルには 2 つの return シナリオがあります)。The controller includes logic to deal with invalid id values (there are two return scenarios in the following example to cover these scenarios). 最終的な return ステートメントにより、新しい StormSessionViewModel がビュー (Controllers/SessionController.cs) に返されます。The final return statement returns a new StormSessionViewModel to the view (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 つのテストを含む単体テスト:The unit tests include one test for each return scenario in the Session controller Index action:

[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 ルートで公開します。Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:

  • ブレーンストーミング セッションに関連付けられたアイデアの一覧 (IdeaDTO) が、ForSession メソッドによって返される。A list of ideas (IdeaDTO) associated with a brainstorming session is returned by the ForSession method.
  • Create メソッドが新しいアイデアをセッションに追加する。The Create method adds new ideas to a session.
[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 の呼び出しでビジネス ドメイン エンティティを直接返すことは避けてください。Avoid returning business domain entities directly via API calls. ドメイン エンティティ:Domain entities:

  • 多くの場合、クライアントが必要とするもの以上のデータを含みます。Often include more data than the client requires.
  • アプリの内部ドメイン モデルと一般公開されている API が不必要に結合します。Unnecessarily couple the app's internal domain model with the publicly exposed API.

ドメイン エンティティとクライアントに返される型の間のマッピングは、以下のように実行できます。Mapping between domain entities and the types returned to the client can be performed:

  • サンプル アプリで使用しているように、LINQ Select を使用して手動で実行します。Manually with a LINQ Select, as the sample app uses. 詳細については、「LINQ (統合言語クエリ)」を参照してください。For more information, see LINQ (Language Integrated Query).
  • AutoMapper などのライブラリを使用して自動的に実行します。Automatically with a library, such as AutoMapper.

次のサンプル アプリは、Ideas コントローラーの CreateForSession API メソッドの単体テストを示します。Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas controller.

このサンプル アプリには、2 つの ForSession テストが含まれています。The sample app contains two ForSession tests. 最初のテストでは、無効なセッションに対して ForSessionNotFoundObjectResult (HTTP Not Found) を返すかどうかを判断します。The first test determines if ForSession returns a NotFoundObjectResult (HTTP Not Found) for an invalid session:

[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>>) を返すかどうかを判断します。The second ForSession test determines if ForSession returns a list of session ideas (<List<IdeaDTO>>) for a valid session. これらのチェックでは、最初のアイデアを調べて、その Name プロパティが正しいことも確認します。The checks also examine the first idea to confirm its Name property is correct:

[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 メソッドの動作をテストするため、サンプル アプリでは、テストの一部としてコントローラーにモデル エラーを追加します。To test the behavior of the Create method when the ModelState is invalid, the sample app adds a model error to the controller as part of the test. 単体テストでは、モデルの検証またはモデル バインドのテストを試さないでください。無効な ModelState に遭遇したときのアクション メソッドの動作だけをテストしてください。Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid 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 を返すようにモック リポジトリが構成されます。The second test of Create depends on the repository returning null, so the mock repository is configured to return null. テスト データベース (メモリ内またはそれ以外の場所) を作成し、この結果を返すクエリを作成する必要はありません。There's no need to create a test database (in memory or otherwise) and construct a query that returns this result. サンプル コードに示すように、このテストは単一のステートメントで実行できます。The test can be accomplished in a single statement, as the sample code illustrates:

[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 メソッドが呼び出されることを検証します。The third Create test, Create_ReturnsNewlyCreatedIdeaForSession, verifies that the repository's UpdateAsync method is called. モックが Verifiable で呼び出され、モック リポジトリの Verify メソッドが呼び出されて、検証可能なメソッドが実行されることを確認します。The mock is called with Verifiable, and the mocked repository's Verify method is called to confirm the verifiable method is executed. UpdateAsync メソッドがデータを保存したことを確認するのは、単体テストの役割ではありません。それは統合テストで実行できます。It's not the unit test's responsibility to ensure that the UpdateAsync method saved the data—that can be performed with an integration test.

[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> をテストするTest ActionResult<T>

ASP.NET Core 2.1 以降、ActionResult<T> (ActionResult<TValue>) で、ActionResult から派生する型または特定の型を返すことができます。In ASP.NET Core 2.1 or later, ActionResult<T> (ActionResult<TValue>) enables you to return a type deriving from ActionResult or return a specific type.

サンプル アプリには、特定のセッション id に対して List<IdeaDTO> を返すメソッドが含まれています。The sample app includes a method that returns a List<IdeaDTO> for a given session id. セッションに id が存在しない場合、コントローラーは NotFound を返します。If the session id doesn't exist, the controller returns 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 つのテストが含まれています。Two tests of the ForSessionActionResult controller are included in the ApiIdeasControllerTests.

最初のテストでは、コントローラーは ActionResult を返すが、存在しないセッション id の存在しないアイデアの一覧は返さないことを確認します。The first test confirms that the controller returns an ActionResult but not a nonexistent list of ideas for a nonexistent session 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 番目のテストでは、メソッドが以下を返すことを確認します。For a valid session id, the second test confirms that the method returns:

  • List<IdeaDTO> 型の ActionResultAn ActionResult with a List<IdeaDTO> type.
  • ActionResult<T>.ValueList<IdeaDTO> 型。The ActionResult<T>.Value is a List<IdeaDTO> type.
  • 一覧の最初の項目は、モック セッションに格納されているアイデアと一致する有効なアイデア (GetTestSession の呼び出しによって取得)。The first item in the list is a valid idea matching the idea stored in the mock session (obtained by calling 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 を作成するメソッドも含まれています。The sample app also includes a method to create a new Idea for a given session. コントローラーは以下を返します。The controller returns:

[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 つのテストが含まれます。Three tests of CreateActionResult are included in the ApiIdeasControllerTests.

最初のテストでは、無効なモデルに対して BadRequest が返されることを確認します。The first text confirms that a BadRequest is returned for an invalid model.

[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 が返されることを確認します。The second test checks that a NotFound is returned if the session doesn't exist.

[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 に対しては、最後のテストで以下を確認します。For a valid session id, the final test confirms that:

  • メソッドが BrainstormSession 型の ActionResult を返す。The method returns an ActionResult with a BrainstormSession type.
  • ActionResult<T>.ResultCreatedAtActionResultThe ActionResult<T>.Result is a CreatedAtActionResult. CreatedAtActionResult201 Created 応答に類似した Location ヘッダー付きの応答である。CreatedAtActionResult is analogous to a 201 Created response with a Location header.
  • ActionResult<T>.ValueBrainstormSession 型。The ActionResult<T>.Value is a BrainstormSession type.
  • セッション UpdateAsync(testSession) を更新するモック 呼び出しが呼び出された。The mock call to update the session, UpdateAsync(testSession), was invoked. Verifiable メソッド呼び出しは、アサーション内で mockRepo.Verify() を実行することでチェックされます。The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.
  • セッションに対して 2 つの Idea オブジェクトが返された。Two Idea objects are returned for the session.
  • 最後の項目 (UpdateAsync へのモック呼び出しによって追加された Idea) が、テスト中にセッションに追加された newIdea と一致する。The last item (the Idea added by the mock call to UpdateAsync) matches the newIdea added to the session in the test.
[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);
}

その他の技術情報Additional resources