Dela via


Enhetstestning av kontrollerlogik i ASP.NET Core

Av Steve Smith

Enhetstester omfattar testning av en del av en app isolerat från dess infrastruktur och beroenden. När logiken hos en kontrollant enhetstestas testas endast den specifika åtgärdens innehåll, inte beroendenas eller ramverkets eget beteende.

Enhetstestkontrollanter

Konfigurera enhetstester av kontrollantåtgärder för att fokusera på kontrollantens beteende. Ett kontrollenhetstest undviker scenarier som filter, routning ochmodellbindning. Tester som omfattar interaktioner mellan komponenter som gemensamt svarar på en begäran hanteras av integrationstester. Mer information om integreringstester finns i Integreringstester i ASP.NET Core.

Om du skriver anpassade filter och vägar testar enheten dem isolerat, inte som en del av tester på en viss kontrollantåtgärd.

För att demonstrera enhetstester för kontrollanter, granska följande kontroller i exempelappen.

Visa eller ladda ned exempelkod (hur du laddar ned)

Kontrollanten Home visar en lista över brainstormingsessioner och gör det möjligt att skapa nya brainstormingsessioner med en POST-begäran:

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

Föregående kontrollant:

Metoden HTTP GET Index har ingen looping eller förgrening och anropar bara en metod. Enhetstestet för den här åtgärden:

  • Simulerar IBrainstormSessionRepository-tjänsten med hjälp av GetTestSessions-metoden. GetTestSessions skapar två falska brainstorm-sessioner med datum och sessionsnamn.
  • Utför Index-metoden.
  • Gör försäkran om resultatet som returneras av metoden:
[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;
}

Kontrollantens HomeHTTP POST Index metodtester verifierar att:

Ett ogiltigt modelltillstånd testas genom att lägga till fel med hjälp av AddModelError som visas i det första testet nedan:

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

När ModelState inte är giltigt returneras samma ViewResult som vid ett GET-anrop. Testet försöker inte skicka in en ogiltig modell. Att skicka en ogiltig modell är inte en giltig metod eftersom modellbindningen inte körs (även om ett integrationstest använder modellbindning). I det här fallet testas inte modellbindning. De här enhetstesterna testar bara koden i åtgärdsmetoden.

Det andra testet verifierar att när ModelState är giltigt:

  • En ny BrainstormSession läggs till (via lagringsplatsen).
  • Metoden returnerar en RedirectToActionResult med de förväntade egenskaperna.

Mock-anrop som inte körs ignoreras normalt, men genom att anropa Verifiable i slutet av setup-anropet tillåter mock-validering i testet. Detta utförs med anropet till mockRepo.Verify, som misslyckas med testet om den förväntade metoden inte anropades.

Note

Moq-biblioteket som används i det här exemplet gör det möjligt att blanda verifierbara, eller "strikta", mock-objekt med icke-verifierbara mock-objekt (även kallade "lösa" mock-objekt eller stubbar). Läs mer om hur du anpassar mock-beteende med Moq.

SessionController i exempelappen visar information som rör en viss brainstormingsession. Kontrollanten innehåller logik för att hantera ogiltiga id värden (det finns två return scenarier i följande exempel för att täcka dessa scenarier). Den slutliga return instruktionen returnerar en ny StormSessionViewModel till vyn (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);
    }
}

Enhetstesterna innehåller ett test för varje return scenario i åtgärden i Sessionscontrollern Index:

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

Genom att gå vidare till Ideas-kontrollen exponeras appens funktionalitet som ett webb-API på api/ideas-rutan.

  • En lista över idéer (IdeaDTO) som är associerade med en brainstormingsession returneras av ForSession metoden.
  • Metoden Create lägger till nya idéer i en 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);
}

Undvik att returnera affärsdomänentiteter direkt via API-anrop. Domänentiteter:

  • Inkludera ofta mer data än vad klienten kräver.
  • Koppla i onödan ihop appens interna domänmodell med det offentligt exponerade API:et.

Mappning mellan domänentiteter och de typer som returneras till klienten kan utföras:

Därefter demonstrerar exempelappen enhetstester för API-metoderna Create och ForSession i idéhanteringskontrollern.

Exempelappen innehåller två ForSession tester. Det första testet avgör om ForSession returnerar en NotFoundObjectResult (HTTP hittades inte) för en ogiltig 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);
}

Det andra ForSession testet avgör om ForSession returnerar en lista med sessionsidéer (<List<IdeaDTO>>) för en giltig session. Kontrollerna undersöker också den första idén för att bekräfta att dess Name egenskap är korrekt:

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

Om du vill testa beteendet för Create metoden när den ModelState är ogiltig lägger exempelappen till ett modellfel till kontrollanten som en del av testet. Försök inte testa modellverifiering eller modellbindning i enhetstester – testa bara åtgärdsmetodens beteende när du konfronteras med ett ogiltigt 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);
}

Det andra testet av Create beror på vilken lagringsplats som returnerar null, så den falska lagringsplatsen är konfigurerad för att returnera null. Du behöver inte skapa en testdatabas (i minnet eller på annat sätt) och skapa en fråga som returnerar det här resultatet. Testet kan utföras i en enda instruktion, som exempelkoden illustrerar:

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

Det tredje Create testet, Create_ReturnsNewlyCreatedIdeaForSession, verifierar att metod UpdateAsync i lagringsplatsen anropas. Mock-anropet görs med Verifiable, och den simulerade lagringsplatsens metod Verify kallas för att bekräfta att den verifierbara metoden utförs. Det är inte enhetstestets ansvar att se till att UpdateAsync metoden sparade data – som kan utföras med ett integrationstest.

[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>

ActionResult<T> (ActionResult<TValue>) kan returnera en typ som härleds från ActionResult eller returnera en viss typ.

Exempelappen innehåller en metod som returnerar en List<IdeaDTO> för en viss session id. Om sessionen id inte finns returnerar NotFoundkontrollanten :

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

Två tester av kontrollanten ForSessionActionResult ingår i ApiIdeasControllerTests.

Det första testet bekräftar att kontrollanten returnerar en ActionResult men inte en obefintlig lista med idéer för en obefintlig 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);
}

För en giltig session idbekräftar det andra testet att metoden returnerar:

  • En ActionResult av typen List<IdeaDTO>.
  • ActionResult<T>.Value är en List<IdeaDTO> typ.
  • Det första objektet i listan är en giltig idé som matchar den idé som lagras i den falska sessionen (hämtas genom att anropa 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);
}

Exempelappen innehåller också en metod för att skapa en ny Idea för en viss session. Kontrollanten returnerar:

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

Tre tester av CreateActionResult ingår i ApiIdeasControllerTests.

Den första texten bekräftar att en BadRequest returneras för en ogiltig modell.

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

Det andra testet kontrollerar att en NotFound returneras om sessionen inte finns.

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

För en giltig session idbekräftar det slutliga testet att:

  • Metoden returnerar en ActionResult med en BrainstormSession typ.
  • ActionResult<T>.Result är en CreatedAtActionResult. CreatedAtActionResult är analogt med ett 201-skapat svar med en Location rubrik.
  • ActionResult<T>.Value är en BrainstormSession typ.
  • Det falska anropet för att uppdatera sessionen, UpdateAsync(testSession), anropades. Metodanropet Verifiable kontrolleras genom att mockRepo.Verify() exekveras i assertioner.
  • Två Idea objekt returneras för sessionen.
  • Det sista objektet (som Idea lagts till av mock-anropet till UpdateAsync) matchar det newIdea som lagts till i sessionen i testet.
[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);
}

Kontrollanter spelar en central roll i alla ASP.NET Core MVC-appar. Därför bör du ha förtroende för att kontrollanter beter sig som avsett. Automatiserade tester kan identifiera fel innan appen distribueras till en produktionsmiljö.

Visa eller ladda ned exempelkod (hur du laddar ned)

Enhetstester av styrenhetslogik

Enhetstester omfattar testning av en del av en app isolerat från dess infrastruktur och beroenden. När logiken hos en kontrollant enhetstestas testas endast den specifika åtgärdens innehåll, inte beroendenas eller ramverkets eget beteende.

Konfigurera enhetstester av kontrollantåtgärder för att fokusera på kontrollantens beteende. Ett kontrollenhetstest undviker scenarier som filter, routning ochmodellbindning. Tester som omfattar interaktioner mellan komponenter som gemensamt svarar på en begäran hanteras av integrationstester. Mer information om integreringstester finns i Integreringstester i ASP.NET Core.

Om du skriver anpassade filter och vägar testar enheten dem isolerat, inte som en del av tester på en viss kontrollantåtgärd.

För att demonstrera enhetstester för kontrollanter, granska följande kontroller i exempelappen. Kontrollanten Home visar en lista över brainstormingsessioner och gör det möjligt att skapa nya brainstormingsessioner med en POST-begäran:

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

Föregående kontrollant:

Metoden HTTP GET Index har ingen looping eller förgrening och anropar bara en metod. Enhetstestet för den här åtgärden:

  • Simulerar IBrainstormSessionRepository-tjänsten med hjälp av GetTestSessions-metoden. GetTestSessions skapar två falska brainstorm-sessioner med datum och sessionsnamn.
  • Utför Index-metoden.
  • Gör försäkran om resultatet som returneras av metoden:
[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;
}

Kontrollantens HomeHTTP POST Index metodtester verifierar att:

Ett ogiltigt modelltillstånd testas genom att lägga till fel med hjälp av AddModelError som visas i det första testet nedan:

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

När ModelState inte är giltigt returneras samma ViewResult som vid ett GET-anrop. Testet försöker inte skicka in en ogiltig modell. Att skicka en ogiltig modell är inte en giltig metod eftersom modellbindningen inte körs (även om ett integrationstest använder modellbindning). I det här fallet testas inte modellbindning. De här enhetstesterna testar bara koden i åtgärdsmetoden.

Det andra testet verifierar att när ModelState är giltigt:

  • En ny BrainstormSession läggs till (via lagringsplatsen).
  • Metoden returnerar en RedirectToActionResult med de förväntade egenskaperna.

Mock-anrop som inte körs ignoreras normalt, men genom att anropa Verifiable i slutet av setup-anropet tillåter mock-validering i testet. Detta utförs med anropet till mockRepo.Verify, som misslyckas med testet om den förväntade metoden inte anropades.

Note

Moq-biblioteket som används i det här exemplet gör det möjligt att blanda verifierbara, eller "strikta", mock-objekt med icke-verifierbara mock-objekt (även kallade "lösa" mock-objekt eller stubbar). Läs mer om hur du anpassar mock-beteende med Moq.

SessionController i exempelappen visar information som rör en viss brainstormingsession. Kontrollanten innehåller logik för att hantera ogiltiga id värden (det finns två return scenarier i följande exempel för att täcka dessa scenarier). Den slutliga return instruktionen returnerar en ny StormSessionViewModel till vyn (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);
    }
}

Enhetstesterna innehåller ett test för varje return scenario i åtgärden i Sessionscontrollern Index:

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

Genom att gå vidare till Ideas-kontrollen exponeras appens funktionalitet som ett webb-API på api/ideas-rutan.

  • En lista över idéer (IdeaDTO) som är associerade med en brainstormingsession returneras av ForSession metoden.
  • Metoden Create lägger till nya idéer i en 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);
}

Undvik att returnera affärsdomänentiteter direkt via API-anrop. Domänentiteter:

  • Inkludera ofta mer data än vad klienten kräver.
  • Koppla i onödan ihop appens interna domänmodell med det offentligt exponerade API:et.

Mappning mellan domänentiteter och de typer som returneras till klienten kan utföras:

Därefter demonstrerar exempelappen enhetstester för API-metoderna Create och ForSession i idéhanteringskontrollern.

Exempelappen innehåller två ForSession tester. Det första testet avgör om ForSession returnerar en NotFoundObjectResult (HTTP hittades inte) för en ogiltig 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);
}

Det andra ForSession testet avgör om ForSession returnerar en lista med sessionsidéer (<List<IdeaDTO>>) för en giltig session. Kontrollerna undersöker också den första idén för att bekräfta att dess Name egenskap är korrekt:

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

Om du vill testa beteendet för Create metoden när den ModelState är ogiltig lägger exempelappen till ett modellfel till kontrollanten som en del av testet. Försök inte testa modellverifiering eller modellbindning i enhetstester – testa bara åtgärdsmetodens beteende när du konfronteras med ett ogiltigt 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);
}

Det andra testet av Create beror på vilken lagringsplats som returnerar null, så den falska lagringsplatsen är konfigurerad för att returnera null. Du behöver inte skapa en testdatabas (i minnet eller på annat sätt) och skapa en fråga som returnerar det här resultatet. Testet kan utföras i en enda instruktion, som exempelkoden illustrerar:

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

Det tredje Create testet, Create_ReturnsNewlyCreatedIdeaForSession, verifierar att metod UpdateAsync i lagringsplatsen anropas. Mock-anropet görs med Verifiable, och den simulerade lagringsplatsens metod Verify kallas för att bekräfta att den verifierbara metoden utförs. Det är inte enhetstestets ansvar att se till att UpdateAsync metoden sparade data – som kan utföras med ett integrationstest.

[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>

I ASP.NET Core 2.1 eller senare gör ActionResult<T> (ActionResult<TValue>) att du kan returnera en typ som härleds från ActionResult eller returnera en viss typ.

Exempelappen innehåller en metod som returnerar en List<IdeaDTO> för en viss session id. Om sessionen id inte finns returnerar NotFoundkontrollanten :

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

Två tester av kontrollanten ForSessionActionResult ingår i ApiIdeasControllerTests.

Det första testet bekräftar att kontrollanten returnerar en ActionResult men inte en obefintlig lista med idéer för en obefintlig 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);
}

För en giltig session idbekräftar det andra testet att metoden returnerar:

  • En ActionResult av typen List<IdeaDTO>.
  • ActionResult<T>.Value är en List<IdeaDTO> typ.
  • Det första objektet i listan är en giltig idé som matchar den idé som lagras i den falska sessionen (hämtas genom att anropa 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);
}

Exempelappen innehåller också en metod för att skapa en ny Idea för en viss session. Kontrollanten returnerar:

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

Tre tester av CreateActionResult ingår i ApiIdeasControllerTests.

Den första texten bekräftar att en BadRequest returneras för en ogiltig modell.

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

Det andra testet kontrollerar att en NotFound returneras om sessionen inte finns.

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

För en giltig session idbekräftar det slutliga testet att:

  • Metoden returnerar en ActionResult med en BrainstormSession typ.
  • ActionResult<T>.Result är en CreatedAtActionResult. CreatedAtActionResult är analogt med ett 201-skapat svar med en Location rubrik.
  • ActionResult<T>.Value är en BrainstormSession typ.
  • Det falska anropet för att uppdatera sessionen, UpdateAsync(testSession), anropades. Metodanropet Verifiable kontrolleras genom att mockRepo.Verify() exekveras i assertioner.
  • Två Idea objekt returneras för sessionen.
  • Det sista objektet (som Idea lagts till av mock-anropet till UpdateAsync) matchar det newIdea som lagts till i sessionen i testet.
[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);
}

Ytterligare resurser