Logica unità test controller in ASP.NET CoreUnit test controller logic in ASP.NET Core

Steve SmithBy Steve Smith

Gli unit test implicano l'esecuzione di test su una parte di un'app isolandola dall'infrastruttura e dalle dipendenze.Unit tests involve testing a part of an app in isolation from its infrastructure and dependencies. Quando si sottopone a unit test la logica del controller, si verificano solo i contenuti di una singola azione e non il comportamento delle relative dipendenze o del framework.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.

Controller di unit testUnit testing controllers

Configurare unit test delle azioni del controller per concentrare l'attenzione sul comportamento del controller.Set up unit tests of controller actions to focus on the controller's behavior. Uno unit test del controller evita scenari come filtri, routing e associazione di modelli.A controller unit test avoids scenarios such as filters, routing, and model binding. I test che verificano le interazioni tra i componenti che collettivamente rispondono a una richiesta vengono gestiti dai test di integrazione.Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. Per altre informazioni sui test di integrazione, vedere Test di integrazione in ASP.NET Core.For more information on integration tests, see Test di integrazione in ASP.NET Core.

Se si scrivono route e filtri personalizzati, sottoporli a unit test in isolamento e non durante i test relativi a una determinata azione del controller.If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.

Per una dimostrazione degli unit test del controller, esaminare il controller seguente nell'app di esempio.To demonstrate controller unit tests, review the following controller in the sample app.

Visualizzare o scaricare il codice di esempio (procedura per il download)View or download sample code (how to download)

Il controller Home visualizza un elenco di sessioni di brainstorming e consente di creare nuove sessioni con una richiesta 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));
    }
}

Il controller precedente:The preceding controller:

Il metodo HTTP GET Index non dispone di cicli o rami e chiama un solo metodo.The HTTP GET Index method has no looping or branching and only calls one method. Lo unit test per questa azione:The unit test for this action:

  • Simula il servizio IBrainstormSessionRepository usando il metodo GetTestSessions.Mocks the IBrainstormSessionRepository service using the GetTestSessions method. GetTestSessions crea due sessioni di brainstorming fittizie con date e nomi di sessione.GetTestSessions creates two mock brainstorm sessions with dates and session names.
  • Esegue il metodo Index.Executes the Index method.
  • Crea asserzioni sul risultato restituito dal metodo:Makes assertions on the result returned by the method:
[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;
}

Il metodo HTTP POST Index del controller Home verifica che:The Home controller's HTTP POST Index method tests verifies that:

  • Quando ModelState.IsValid è false, il metodo di azione restituisca un elemento di tipo400 - Richiesta non validaViewResult con i dati appropriati.When ModelState.IsValid is false, the action method returns a 400 Bad Request ViewResult with the appropriate data.
  • Quando ModelState.IsValid è true:When ModelState.IsValid is true:
    • Venga chiamato il metodo Add sul repository.The Add method on the repository is called.
    • Venga restituito un RedirectToActionResult con gli argomenti corretti.A RedirectToActionResult is returned with the correct arguments.

È possibile eseguire il test di uno stato del modello non valido aggiungendo gli errori con AddModelError come illustrato nel primo test riportato di seguito: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();
}

Quando ModelState non è valido, viene restituito lo stesso ViewResult come per una richiesta GET.When ModelState isn't valid, the same ViewResult is returned as for a GET request. Il test non prova a passare un modello non valido.The test doesn't attempt to pass in an invalid model. Il passaggio di un modello non valido non è un approccio corretto, in quanto l'associazione di modelli non è in esecuzione (benché un test di integrazione usi invece l'associazione di modelli).Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). In questo caso l'associazione di modelli non viene testata.In this case, model binding isn't tested. Questi unit test testano solo il codice del metodo di azione.These unit tests are only testing the code in the action method.

Il secondo test verifica che quando ModelState è valido:The second test verifies that when the ModelState is valid:

  • Venga aggiunto un nuovo elemento BrainstormSession (tramite il repository).A new BrainstormSession is added (via the repository).
  • Il metodo restituisca un RedirectToActionResult con le proprietà previste.The method returns a RedirectToActionResult with the expected properties.

Le chiamate fittizie che non vengono eseguite sono in genere ignorate, ma la chiamata di Verifiable al termine della chiamata setup consente la convalida fittizia nel test.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. Questa operazione viene eseguita con la chiamata a mockRepo.Verify, che non supera il test se non è stato chiamato il metodo previsto.This is performed with the call to mockRepo.Verify, which fails the test if the expected method wasn't called.

Nota

La libreria Moq usata in questo esempio consente la combinazione di simulazioni verificabili o "rigide" con simulazioni non verificabili (dette anche simulazioni "generiche" o stub "generici").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). Altre informazioni sulla personalizzazione del comportamento di simulazione con Moq.Learn more about customizing Mock behavior with Moq.

SessionController nell'app di esempio visualizza informazioni correlate a una sessione di brainstorming specifica.SessionController in the sample app displays information related to a particular brainstorming session. Il controller include la logica per gestire i valori id non validi (nell'esempio seguente sono riportati due scenari return in proposito).The controller includes logic to deal with invalid id values (there are two return scenarios in the following example to cover these scenarios). L'istruzione finale return restituisce un nuovo elemento StormSessionViewModel alla vista (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);
    }
}

Gli unit test includono un test per ogni scenario return nell'azione Index del controller Session: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);
}

Passando al controller Ideas, l'app espone la funzionalità come API Web sulla route api/ideas:Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:

  • Il metodo ForSession restituisce un elenco di idee (IdeaDTO) associate a una sessione di brainstorming.A list of ideas (IdeaDTO) associated with a brainstorming session is returned by the ForSession method.
  • Il metodo Create aggiunge nuove idee a una sessione.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);
}

Evitare di restituire direttamente le entità di dominio aziendali tramite chiamate API.Avoid returning business domain entities directly via API calls. Le entità di dominio infatti:Domain entities:

  • Spesso includono più dati di quelli richiesti dal client.Often include more data than the client requires.
  • Associano senza necessità il modello di dominio interno dell'app all'API esposta pubblicamente.Unnecessarily couple the app's internal domain model with the publicly exposed API.

Il mapping tra le entità di dominio e i tipi restituiti al client può essere eseguito:Mapping between domain entities and the types returned to the client can be performed:

L'app di esempio esegue quindi una dimostrazione degli unit test per i metodi API Create e ForSession del controller Ideas.Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas controller.

L' app di esempio contiene due test ForSession.The sample app contains two ForSession tests. Il primo determina se ForSession restituisce un NotFoundObjectResult (HTTP non trovato) per una sessione non valida: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);
}

Il secondo test ForSession determina se ForSession restituisce un elenco di idee (<List<IdeaDTO>>) per una sessione valida.The second ForSession test determines if ForSession returns a list of session ideas (<List<IdeaDTO>>) for a valid session. I controlli esaminano anche la prima idea per verificare che la relativa proprietà Name sia corretta: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);
}

Per testare il comportamento del metodo Create quando ModelState non è valido, l'app di esempio aggiunge un errore del modello al controller come parte del test.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. Non provare a testare la convalida o l'associazione di modelli negli unit test. Testare solo il comportamento del metodo di azione rispetto a un ModelState non valido: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);
}

Il secondo test di Create dipende dal fatto che il repository restituisca null, pertanto il repository fittizio è configurato per restituire null.The second test of Create depends on the repository returning null, so the mock repository is configured to return null. Non è necessario creare un database di test (in memoria o con un altro approccio) e creare una query che restituisca questo risultato.There's no need to create a test database (in memory or otherwise) and construct a query that returns this result. Il test può essere eseguito in una singola istruzione, come illustrato dal codice di esempio: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);
}

Il terzo test di Create, Create_ReturnsNewlyCreatedIdeaForSession, verifica che venga chiamato il metodo UpdateAsync del repository.The third Create test, Create_ReturnsNewlyCreatedIdeaForSession, verifies that the repository's UpdateAsync method is called. La simulazione viene chiamata con Verifiable e quindi viene chiamato il metodo Verify del repository fittizio per confermare che il metodo verificabile sia stato eseguito.The mock is called with Verifiable, and the mocked repository's Verify method is called to confirm the verifiable method is executed. Non spetta allo unit test verificare che il metodo UpdateAsync abbia salvato i dati. Questa operazione può essere eseguita con un test di integrazione.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);
}

Test ActionResult<T >Test ActionResult<T>

In ASP.NET Core 2,1 o versioni successive, ActionResult<t > (ActionResult<TValue>) consente di restituire un tipo derivante da ActionResult o restituire un tipo specifico.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.

L'app di esempio include un metodo che restituisce un List<IdeaDTO> per un determinato id di sessione.The sample app includes a method that returns a List<IdeaDTO> for a given session id. Se l'id di sessione non esiste, il controller restituisce 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;
}

In ApiIdeasControllerTests sono inclusi due test del controller ForSessionActionResult.Two tests of the ForSessionActionResult controller are included in the ApiIdeasControllerTests.

Il primo test verifica che il controller restituisca un ActionResult ma non un elenco non esistente di idee per un id di sessione inesistente: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);
}

Per un id di sessione valido, il secondo test verifica che il metodo restituisca:For a valid session id, the second test confirms that the method returns:

  • Il metodo restituisca un ActionResult con un tipo List<IdeaDTO>.An ActionResult with a List<IdeaDTO> type.
  • <t > ActionResult. Il valore è un tipo di List<IdeaDTO>.The ActionResult<T>.Value is a List<IdeaDTO> type.
  • Il primo elemento nell'elenco sia un'idea valida corrispondente all'idea archiviata nella sessione fittizia (ottenuta chiamando 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);
}

L'app di esempio include anche un metodo per creare una nuova Idea per una determinata sessione.The sample app also includes a method to create a new Idea for a given session. Il controller restituisce: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);
}

In ApiIdeasControllerTests sono inclusi tre test di CreateActionResult.Three tests of CreateActionResult are included in the ApiIdeasControllerTests.

Il primo test verifica che per un modello non valido venga restituito un elemento 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);
}

Il secondo test verifica che venga restituito un elemento NotFound se la sessione non esiste.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);
}

Per un id di sessione valido, il test finale verifica che:For a valid session id, the final test confirms that:

  • Il metodo restituisca un ActionResult di tipo BrainstormSession.The method returns an ActionResult with a BrainstormSession type.
  • <t > ActionResult. Il risultato è un CreatedAtActionResult.The ActionResult<T>.Result is a CreatedAtActionResult. CreatedAtActionResult è analogo a una risposta 201 - Creato con un'intestazione Location.CreatedAtActionResult is analogous to a 201 Created response with a Location header.
  • <t > ActionResult. Il valore è un tipo di BrainstormSession.The ActionResult<T>.Value is a BrainstormSession type.
  • La chiamata fittizia per aggiornare la sessione, UpdateAsync(testSession), sia stata eseguita.The mock call to update the session, UpdateAsync(testSession), was invoked. La chiamata del metodo Verifiable viene controllata eseguendo mockRepo.Verify() nelle asserzioni.The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.
  • Siano stati restituiti due oggetti Idea per la sessione.Two Idea objects are returned for the session.
  • L'ultimo elemento (Idea aggiunta dalla chiamata fittizia a UpdateAsync) corrisponda alla newIdea aggiunta alla sessione nel test.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);
}

I controller hanno un ruolo centrale in qualsiasi app ASP.NET Core MVC.Controllers play a central role in any ASP.NET Core MVC app. Di conseguenza, è importante essere certi che funzionino nel modo previsto.As such, you should have confidence that controllers behave as intended. I test automatizzati possono rilevare eventuali errori prima che l'app venga distribuita in un ambiente di produzione.Automated tests can detect errors before the app is deployed to a production environment.

Visualizzare o scaricare il codice di esempio (procedura per il download)View or download sample code (how to download)

Unit test della logica dei controllerUnit tests of controller logic

Gli unit test implicano l'esecuzione di test su una parte di un'app isolandola dall'infrastruttura e dalle dipendenze.Unit tests involve testing a part of an app in isolation from its infrastructure and dependencies. Quando si sottopone a unit test la logica del controller, si verificano solo i contenuti di una singola azione e non il comportamento delle relative dipendenze o del framework.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.

Configurare unit test delle azioni del controller per concentrare l'attenzione sul comportamento del controller.Set up unit tests of controller actions to focus on the controller's behavior. Uno unit test del controller evita scenari come filtri, routing e associazione di modelli.A controller unit test avoids scenarios such as filters, routing, and model binding. I test che verificano le interazioni tra i componenti che collettivamente rispondono a una richiesta vengono gestiti dai test di integrazione.Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. Per altre informazioni sui test di integrazione, vedere Test di integrazione in ASP.NET Core.For more information on integration tests, see Test di integrazione in ASP.NET Core.

Se si scrivono route e filtri personalizzati, sottoporli a unit test in isolamento e non durante i test relativi a una determinata azione del controller.If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.

Per una dimostrazione degli unit test del controller, esaminare il controller seguente nell'app di esempio.To demonstrate controller unit tests, review the following controller in the sample app. Il controller Home visualizza un elenco di sessioni di brainstorming e consente di creare nuove sessioni con una richiesta 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));
    }
}

Il controller precedente:The preceding controller:

Il metodo HTTP GET Index non dispone di cicli o rami e chiama un solo metodo.The HTTP GET Index method has no looping or branching and only calls one method. Lo unit test per questa azione:The unit test for this action:

  • Simula il servizio IBrainstormSessionRepository usando il metodo GetTestSessions.Mocks the IBrainstormSessionRepository service using the GetTestSessions method. GetTestSessions crea due sessioni di brainstorming fittizie con date e nomi di sessione.GetTestSessions creates two mock brainstorm sessions with dates and session names.
  • Esegue il metodo Index.Executes the Index method.
  • Crea asserzioni sul risultato restituito dal metodo:Makes assertions on the result returned by the method:
[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;
}

Il metodo HTTP POST Index del controller Home verifica che:The Home controller's HTTP POST Index method tests verifies that:

  • Quando ModelState.IsValid è false, il metodo di azione restituisca un elemento di tipo400 - Richiesta non validaViewResult con i dati appropriati.When ModelState.IsValid is false, the action method returns a 400 Bad Request ViewResult with the appropriate data.
  • Quando ModelState.IsValid è true:When ModelState.IsValid is true:
    • Venga chiamato il metodo Add sul repository.The Add method on the repository is called.
    • Venga restituito un RedirectToActionResult con gli argomenti corretti.A RedirectToActionResult is returned with the correct arguments.

È possibile eseguire il test di uno stato del modello non valido aggiungendo gli errori con AddModelError come illustrato nel primo test riportato di seguito: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();
}

Quando ModelState non è valido, viene restituito lo stesso ViewResult come per una richiesta GET.When ModelState isn't valid, the same ViewResult is returned as for a GET request. Il test non prova a passare un modello non valido.The test doesn't attempt to pass in an invalid model. Il passaggio di un modello non valido non è un approccio corretto, in quanto l'associazione di modelli non è in esecuzione (benché un test di integrazione usi invece l'associazione di modelli).Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). In questo caso l'associazione di modelli non viene testata.In this case, model binding isn't tested. Questi unit test testano solo il codice del metodo di azione.These unit tests are only testing the code in the action method.

Il secondo test verifica che quando ModelState è valido:The second test verifies that when the ModelState is valid:

  • Venga aggiunto un nuovo elemento BrainstormSession (tramite il repository).A new BrainstormSession is added (via the repository).
  • Il metodo restituisca un RedirectToActionResult con le proprietà previste.The method returns a RedirectToActionResult with the expected properties.

Le chiamate fittizie che non vengono eseguite sono in genere ignorate, ma la chiamata di Verifiable al termine della chiamata setup consente la convalida fittizia nel test.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. Questa operazione viene eseguita con la chiamata a mockRepo.Verify, che non supera il test se non è stato chiamato il metodo previsto.This is performed with the call to mockRepo.Verify, which fails the test if the expected method wasn't called.

Nota

La libreria Moq usata in questo esempio consente la combinazione di simulazioni verificabili o "rigide" con simulazioni non verificabili (dette anche simulazioni "generiche" o stub "generici").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). Altre informazioni sulla personalizzazione del comportamento di simulazione con Moq.Learn more about customizing Mock behavior with Moq.

SessionController nell'app di esempio visualizza informazioni correlate a una sessione di brainstorming specifica.SessionController in the sample app displays information related to a particular brainstorming session. Il controller include la logica per gestire i valori id non validi (nell'esempio seguente sono riportati due scenari return in proposito).The controller includes logic to deal with invalid id values (there are two return scenarios in the following example to cover these scenarios). L'istruzione finale return restituisce un nuovo elemento StormSessionViewModel alla vista (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);
    }
}

Gli unit test includono un test per ogni scenario return nell'azione Index del controller Session: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);
}

Passando al controller Ideas, l'app espone la funzionalità come API Web sulla route api/ideas:Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:

  • Il metodo ForSession restituisce un elenco di idee (IdeaDTO) associate a una sessione di brainstorming.A list of ideas (IdeaDTO) associated with a brainstorming session is returned by the ForSession method.
  • Il metodo Create aggiunge nuove idee a una sessione.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);
}

Evitare di restituire direttamente le entità di dominio aziendali tramite chiamate API.Avoid returning business domain entities directly via API calls. Le entità di dominio infatti:Domain entities:

  • Spesso includono più dati di quelli richiesti dal client.Often include more data than the client requires.
  • Associano senza necessità il modello di dominio interno dell'app all'API esposta pubblicamente.Unnecessarily couple the app's internal domain model with the publicly exposed API.

Il mapping tra le entità di dominio e i tipi restituiti al client può essere eseguito:Mapping between domain entities and the types returned to the client can be performed:

L'app di esempio esegue quindi una dimostrazione degli unit test per i metodi API Create e ForSession del controller Ideas.Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas controller.

L' app di esempio contiene due test ForSession.The sample app contains two ForSession tests. Il primo determina se ForSession restituisce un NotFoundObjectResult (HTTP non trovato) per una sessione non valida: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);
}

Il secondo test ForSession determina se ForSession restituisce un elenco di idee (<List<IdeaDTO>>) per una sessione valida.The second ForSession test determines if ForSession returns a list of session ideas (<List<IdeaDTO>>) for a valid session. I controlli esaminano anche la prima idea per verificare che la relativa proprietà Name sia corretta: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);
}

Per testare il comportamento del metodo Create quando ModelState non è valido, l'app di esempio aggiunge un errore del modello al controller come parte del test.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. Non provare a testare la convalida o l'associazione di modelli negli unit test. Testare solo il comportamento del metodo di azione rispetto a un ModelState non valido: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);
}

Il secondo test di Create dipende dal fatto che il repository restituisca null, pertanto il repository fittizio è configurato per restituire null.The second test of Create depends on the repository returning null, so the mock repository is configured to return null. Non è necessario creare un database di test (in memoria o con un altro approccio) e creare una query che restituisca questo risultato.There's no need to create a test database (in memory or otherwise) and construct a query that returns this result. Il test può essere eseguito in una singola istruzione, come illustrato dal codice di esempio: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);
}

Il terzo test di Create, Create_ReturnsNewlyCreatedIdeaForSession, verifica che venga chiamato il metodo UpdateAsync del repository.The third Create test, Create_ReturnsNewlyCreatedIdeaForSession, verifies that the repository's UpdateAsync method is called. La simulazione viene chiamata con Verifiable e quindi viene chiamato il metodo Verify del repository fittizio per confermare che il metodo verificabile sia stato eseguito.The mock is called with Verifiable, and the mocked repository's Verify method is called to confirm the verifiable method is executed. Non spetta allo unit test verificare che il metodo UpdateAsync abbia salvato i dati. Questa operazione può essere eseguita con un test di integrazione.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);
}

Test ActionResult<T >Test ActionResult<T>

In ASP.NET Core 2,1 o versioni successive, ActionResult<t > (ActionResult<TValue>) consente di restituire un tipo derivante da ActionResult o restituire un tipo specifico.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.

L'app di esempio include un metodo che restituisce un List<IdeaDTO> per un determinato id di sessione.The sample app includes a method that returns a List<IdeaDTO> for a given session id. Se l'id di sessione non esiste, il controller restituisce 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;
}

In ApiIdeasControllerTests sono inclusi due test del controller ForSessionActionResult.Two tests of the ForSessionActionResult controller are included in the ApiIdeasControllerTests.

Il primo test verifica che il controller restituisca un ActionResult ma non un elenco non esistente di idee per un id di sessione inesistente: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);
}

Per un id di sessione valido, il secondo test verifica che il metodo restituisca:For a valid session id, the second test confirms that the method returns:

  • Il metodo restituisca un ActionResult con un tipo List<IdeaDTO>.An ActionResult with a List<IdeaDTO> type.
  • <t > ActionResult. Il valore è un tipo di List<IdeaDTO>.The ActionResult<T>.Value is a List<IdeaDTO> type.
  • Il primo elemento nell'elenco sia un'idea valida corrispondente all'idea archiviata nella sessione fittizia (ottenuta chiamando 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);
}

L'app di esempio include anche un metodo per creare una nuova Idea per una determinata sessione.The sample app also includes a method to create a new Idea for a given session. Il controller restituisce: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);
}

In ApiIdeasControllerTests sono inclusi tre test di CreateActionResult.Three tests of CreateActionResult are included in the ApiIdeasControllerTests.

Il primo test verifica che per un modello non valido venga restituito un elemento 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);
}

Il secondo test verifica che venga restituito un elemento NotFound se la sessione non esiste.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);
}

Per un id di sessione valido, il test finale verifica che:For a valid session id, the final test confirms that:

  • Il metodo restituisca un ActionResult di tipo BrainstormSession.The method returns an ActionResult with a BrainstormSession type.
  • <t > ActionResult. Il risultato è un CreatedAtActionResult.The ActionResult<T>.Result is a CreatedAtActionResult. CreatedAtActionResult è analogo a una risposta 201 - Creato con un'intestazione Location.CreatedAtActionResult is analogous to a 201 Created response with a Location header.
  • <t > ActionResult. Il valore è un tipo di BrainstormSession.The ActionResult<T>.Value is a BrainstormSession type.
  • La chiamata fittizia per aggiornare la sessione, UpdateAsync(testSession), sia stata eseguita.The mock call to update the session, UpdateAsync(testSession), was invoked. La chiamata del metodo Verifiable viene controllata eseguendo mockRepo.Verify() nelle asserzioni.The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.
  • Siano stati restituiti due oggetti Idea per la sessione.Two Idea objects are returned for the session.
  • L'ultimo elemento (Idea aggiunta dalla chiamata fittizia a UpdateAsync) corrisponda alla newIdea aggiunta alla sessione nel test.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);
}

Risorse aggiuntiveAdditional resources