Logika kontroleru testů jednotek v ASP.NET Core

Autor: Steve Smith

Testy jednotek zahrnují testování části aplikace izolovaně od infrastruktury a závislostí. Pokud je logika kontroleru testování jednotek, testují se pouze obsah jedné akce, nikoli chování jejích závislostí nebo samotné architektury.

Kontrolery testování jednotek

Nastavte testy jednotek akcí kontroleru, abyste se mohli zaměřit na chování kontroleru. Test jednotek kontroleru zabraňuje scénářům, jako jsou filtry, směrování a vazby modelu. Testy, které pokrývají interakce mezi komponentami, které souhrnně reagují na požadavek, se zpracovávají integračními testy. Další informace o integračníchtestch ASP.NET ch

Pokud píšete vlastní filtry a trasy, otestujte je izolovaně, ne jako součást testů na konkrétní akci kontroleru.

Pokud chcete předvést testy jednotek kontroleru, projděte si následující kontroler v ukázkové aplikaci.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Kontroler Home zobrazí seznam debat a umožňuje vytváření nových debat s požadavkem POST:

public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

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

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

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

        return View(model);
    }

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

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

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

Předchozí kontroler:

  • Řídí se principem explicitních závislostí.
  • Očekává, že injektáž závislostí (DI) poskytne instanci IBrainstormSessionRepository.
  • Můžete testovat pomocí napodobené služby pomocí napodobené IBrainstormSessionRepository architektury objektů, jako je Například Moq. Napodobený objekt je objekt s předem určenou sadou vlastností a chování metod používaných k testování. Další informace naleznete v tématu Úvod do integračních testů.

Metoda HTTP GET Index nemá žádnou smyčku ani větvení a volá pouze jednu metodu. Test jednotek pro tuto akci:

  • Napodobí IBrainstormSessionRepository službu pomocí GetTestSessions metody. GetTestSessions vytvoří dvě napodobené debaty s daty a názvy relací.
  • Spustí metodu Index .
  • Vytvoří kontrolní výrazy ve výsledku vrácené metodou:
    • Vrátí se hodnota A ViewResult .
    • ViewDataDictionary.Model je .StormSessionViewModel
    • Existují dvě debaty uložené v ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

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

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

Metoda Home kontroleru HTTP POST Index ověřuje, že:

Neplatný stav modelu se testuje přidáním chyb, AddModelError jak je znázorněno v prvním testu níže:

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

Pokud modelState není platný, vrátí se totéž ViewResult jako u požadavku GET. Test se nepokouší předat neplatný model. Předání neplatného modelu není platný přístup, protože vazba modelu není spuštěná (i když integrační test používá vazbu modelu). V tomto případě se vazba modelu neotestuje. Tyto testy jednotek testují pouze kód v metodě akce.

Druhý test ověří, že pokud ModelState je platný:

  • Přidá se nový BrainstormSession (prostřednictvím úložiště).
  • Metoda vrátí RedirectToActionResult hodnotu s očekávanými vlastnostmi.

Volání napodobená volání, která nejsou volána, se obvykle ignorují, ale volání Verifiable na konci volání nastavení umožňuje ověření napodobení v testu. To se provádí s voláním mockRepo.Verify, což selže test, pokud očekávaná metoda nebyla volána.

Poznámka

Knihovna Moq použitá v této ukázce umožňuje kombinovat ověřitelné nebo "striktní", napodobení s neověřitelnými napodobením (označovanými také jako "volné" napodobení nebo zástupné procedury). Přečtěte si další informace o přizpůsobení chování napodobení pomocí Moq.

SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní relací debaty. Kontroler obsahuje logiku pro řešení neplatných id hodnot (v následujícím příkladu existují dva return scénáře pro pokrytí těchto scénářů). Konečný return příkaz vrátí nové StormSessionViewModel zobrazení (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);
    }
}

Testy jednotek zahrnují jeden test pro každý return scénář v akci kontroleru Index relace:

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

Přechod na kontroler Návrhy zpřístupňuje funkce jako webové rozhraní API na api/ideas trase:

  • Metoda vrátí ForSession seznam nápadů (IdeaDTO) přidružených k relaci debaty.
  • Metoda Create přidá do relace nové nápady.
[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);
}

Vyhněte se vracení entit obchodní domény přímo prostřednictvím volání rozhraní API. Entity domény:

  • Často obsahují více dat, než vyžaduje klient.
  • Zbytečně propojte interní doménový model aplikace s veřejně vystaveným rozhraním API.

Mapování mezi entitami domény a typy vrácené klientovi je možné provést:

Dále ukázková aplikace předvádí testy jednotek pro CreateForSession metody rozhraní API kontroleru Ideas.

Ukázková aplikace obsahuje dva ForSession testy. První test určuje, jestli ForSession vrátí NotFoundObjectResult hodnotu (HTTP Nenalezena) pro neplatnou relaci:

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

Druhý ForSession test určuje, jestli ForSession vrátí seznam nápadů relace (<List<IdeaDTO>>) pro platnou relaci. Kontroly také prověřují první myšlenku na ověření správnosti jeho Name vlastnosti:

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

Pokud chcete otestovat chování Create metody, když ModelState je neplatná, ukázková aplikace přidá do kontroleru chybu modelu jako součást testu. Nepokoušejte se testovat ověření modelu nebo vazbu modelu v testech jednotek – pouze otestujte chování metody akce, když je konfrontováno s neplatným ModelStatekódem:

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

Druhý test Create závisí na vrácení nullúložiště , takže je nakonfigurované úložiště na vrácení null. Není potřeba vytvořit testovací databázi (v paměti nebo jinak) a vytvořit dotaz, který vrátí tento výsledek. Test lze provést v jednom příkazu, jak ukazuje ukázkový kód:

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

Třetí Create test , ověřuje, Create_ReturnsNewlyCreatedIdeaForSessionže je volána metoda úložiště UpdateAsync . Napodobení se volá pomocí Verifiablea metoda napodobeného úložiště Verify je volána k potvrzení, že je ověřená metoda provedena. Není zodpovědností testu jednotek zajistit, aby UpdateAsync metoda uložila data – která se dá provést s integračním testem.

[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>) může vrátit typ odvozený nebo ActionResult vrátit určitý typ.

Ukázková aplikace obsahuje metodu, která vrací pro danou List<IdeaDTO> relaci id. Pokud relace id neexistuje, vrátí kontroler 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;
}

Dva testy ForSessionActionResult kontroleru jsou zahrnuty v sadě ApiIdeasControllerTests.

První test potvrdí, že kontroler vrátí ActionResult neexistující seznam nápadů pro neexistující relaci 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);
}

Pro platnou relaci iddruhý test potvrdí, že metoda vrátí:

  • List<IdeaDTO> TypActionResult.
  • ActionResult <T>. Hodnota je List<IdeaDTO> typ.
  • První položkou v seznamu je platný nápad odpovídající myšlence uložené v relaci napodobení (získané voláním 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);
}

Ukázková aplikace obsahuje také metodu vytvoření nové Idea pro danou relaci. Kontroler vrátí:

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

Do souboru ApiIdeasControllerTestsjsou zahrnuty tři testy CreateActionResult .

První text potvrzuje, že BadRequest je vrácen pro neplatný 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);
}

Druhý test zkontroluje, jestli NotFound se vrátí, pokud relace neexistuje.

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

Pro platnou relaci idkonečný test potvrzuje, že:

  • Metoda vrátí typ ActionResultBrainstormSession .
  • ActionResult <T>. Výsledek je .CreatedAtActionResult CreatedAtActionResult je podobná odpovědi vytvořené v roce 201 s hlavičkou Location .
  • ActionResult <T>. Hodnota je BrainstormSession typ.
  • Volání napodobení aktualizace relace , UpdateAsync(testSession)bylo vyvoláno. Volání Verifiable metody je kontrolováno spuštěním mockRepo.Verify() v kontrolních výrazech.
  • Pro relaci se vrátí dva Idea objekty.
  • Poslední položka ( Idea přidaná voláním UpdateAsyncnapodobení) odpovídá newIdea přidané relaci v testu.
[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);
}

Kontrolery hrají ústřední roli v libovolné aplikaci ASP.NET Core MVC. Proto byste měli mít jistotu, že se kontrolery chovají tak, jak mají. Automatizované testy můžou detekovat chyby před nasazením aplikace do produkčního prostředí.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Testy jednotek logiky kontroleru

Testy jednotek zahrnují testování části aplikace izolovaně od infrastruktury a závislostí. Pokud je logika kontroleru testování jednotek, testují se pouze obsah jedné akce, nikoli chování jejích závislostí nebo samotné architektury.

Nastavte testy jednotek akcí kontroleru, abyste se mohli zaměřit na chování kontroleru. Test jednotek kontroleru zabraňuje scénářům, jako jsou filtry, směrování a vazby modelu. Testy, které pokrývají interakce mezi komponentami, které souhrnně reagují na požadavek, se zpracovávají integračními testy. Další informace o integračníchtestch ASP.NET ch

Pokud píšete vlastní filtry a trasy, otestujte je izolovaně, ne jako součást testů na konkrétní akci kontroleru.

Pokud chcete předvést testy jednotek kontroleru, projděte si následující kontroler v ukázkové aplikaci. Kontroler Home zobrazí seznam debat a umožňuje vytváření nových debat s požadavkem POST:

public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

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

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

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

        return View(model);
    }

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

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

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

Předchozí kontroler:

  • Řídí se principem explicitních závislostí.
  • Očekává, že injektáž závislostí (DI) poskytne instanci IBrainstormSessionRepository.
  • Můžete testovat pomocí napodobené služby pomocí napodobené IBrainstormSessionRepository architektury objektů, jako je Například Moq. Napodobený objekt je objekt s předem určenou sadou vlastností a chování metod používaných k testování. Další informace naleznete v tématu Úvod do integračních testů.

Metoda HTTP GET Index nemá žádnou smyčku ani větvení a volá pouze jednu metodu. Test jednotek pro tuto akci:

  • Napodobí IBrainstormSessionRepository službu pomocí GetTestSessions metody. GetTestSessions vytvoří dvě napodobené debaty s daty a názvy relací.
  • Spustí metodu Index .
  • Vytvoří kontrolní výrazy ve výsledku vrácené metodou:
    • Vrátí se hodnota A ViewResult .
    • ViewDataDictionary.Model je .StormSessionViewModel
    • Existují dvě debaty uložené v ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

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

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

Metoda Home kontroleru HTTP POST Index ověřuje, že:

Neplatný stav modelu se testuje přidáním chyb, AddModelError jak je znázorněno v prvním testu níže:

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

Pokud modelState není platný, vrátí se totéž ViewResult jako u požadavku GET. Test se nepokouší předat neplatný model. Předání neplatného modelu není platný přístup, protože vazba modelu není spuštěná (i když integrační test používá vazbu modelu). V tomto případě se vazba modelu neotestuje. Tyto testy jednotek testují pouze kód v metodě akce.

Druhý test ověří, že pokud ModelState je platný:

  • Přidá se nový BrainstormSession (prostřednictvím úložiště).
  • Metoda vrátí RedirectToActionResult hodnotu s očekávanými vlastnostmi.

Volání napodobená volání, která nejsou volána, se obvykle ignorují, ale volání Verifiable na konci volání nastavení umožňuje ověření napodobení v testu. To se provádí s voláním mockRepo.Verify, což selže test, pokud očekávaná metoda nebyla volána.

Poznámka

Knihovna Moq použitá v této ukázce umožňuje kombinovat ověřitelné nebo "striktní", napodobení s neověřitelnými napodobením (označovanými také jako "volné" napodobení nebo zástupné procedury). Přečtěte si další informace o přizpůsobení chování napodobení pomocí Moq.

SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní relací debaty. Kontroler obsahuje logiku pro řešení neplatných id hodnot (v následujícím příkladu existují dva return scénáře pro pokrytí těchto scénářů). Konečný return příkaz vrátí nové StormSessionViewModel zobrazení (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);
    }
}

Testy jednotek zahrnují jeden test pro každý return scénář v akci kontroleru Index relace:

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

Přechod na kontroler Návrhy zpřístupňuje funkce jako webové rozhraní API na api/ideas trase:

  • Metoda vrátí ForSession seznam nápadů (IdeaDTO) přidružených k relaci debaty.
  • Metoda Create přidá do relace nové nápady.
[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);
}

Vyhněte se vracení entit obchodní domény přímo prostřednictvím volání rozhraní API. Entity domény:

  • Často obsahují více dat, než vyžaduje klient.
  • Zbytečně propojte interní doménový model aplikace s veřejně vystaveným rozhraním API.

Mapování mezi entitami domény a typy vrácené klientovi je možné provést:

Dále ukázková aplikace předvádí testy jednotek pro CreateForSession metody rozhraní API kontroleru Ideas.

Ukázková aplikace obsahuje dva ForSession testy. První test určuje, jestli ForSession vrátí NotFoundObjectResult hodnotu (HTTP Nenalezena) pro neplatnou relaci:

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

Druhý ForSession test určuje, jestli ForSession vrátí seznam nápadů relace (<List<IdeaDTO>>) pro platnou relaci. Kontroly také prověřují první myšlenku na ověření správnosti jeho Name vlastnosti:

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

Pokud chcete otestovat chování Create metody, když ModelState je neplatná, ukázková aplikace přidá do kontroleru chybu modelu jako součást testu. Nepokoušejte se testovat ověření modelu nebo vazbu modelu v testech jednotek – pouze otestujte chování metody akce, když je konfrontováno s neplatným ModelStatekódem:

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

Druhý test Create závisí na vrácení nullúložiště , takže je nakonfigurované úložiště na vrácení null. Není potřeba vytvořit testovací databázi (v paměti nebo jinak) a vytvořit dotaz, který vrátí tento výsledek. Test lze provést v jednom příkazu, jak ukazuje ukázkový kód:

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

Třetí Create test , ověřuje, Create_ReturnsNewlyCreatedIdeaForSessionže je volána metoda úložiště UpdateAsync . Napodobení se volá pomocí Verifiablea metoda napodobeného úložiště Verify je volána k potvrzení, že je ověřená metoda provedena. Není zodpovědností testu jednotek zajistit, aby UpdateAsync metoda uložila data – která se dá provést s integračním testem.

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

V ASP.NET Core 2.1 nebo novějším umožňuje ActionResult<T> (ActionResult<TValue>) vrátit typ odvozený nebo ActionResult vrátit určitý typ.

Ukázková aplikace obsahuje metodu, která vrací pro danou List<IdeaDTO> relaci id. Pokud relace id neexistuje, vrátí kontroler 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;
}

Dva testy ForSessionActionResult kontroleru jsou zahrnuty v sadě ApiIdeasControllerTests.

První test potvrdí, že kontroler vrátí ActionResult neexistující seznam nápadů pro neexistující relaci 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);
}

Pro platnou relaci iddruhý test potvrdí, že metoda vrátí:

  • List<IdeaDTO> TypActionResult.
  • ActionResult <T>. Hodnota je List<IdeaDTO> typ.
  • První položkou v seznamu je platný nápad odpovídající myšlence uložené v relaci napodobení (získané voláním 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);
}

Ukázková aplikace obsahuje také metodu vytvoření nové Idea pro danou relaci. Kontroler vrátí:

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

Do souboru ApiIdeasControllerTestsjsou zahrnuty tři testy CreateActionResult .

První text potvrzuje, že BadRequest je vrácen pro neplatný 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);
}

Druhý test zkontroluje, jestli NotFound se vrátí, pokud relace neexistuje.

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

Pro platnou relaci idkonečný test potvrzuje, že:

  • Metoda vrátí typ ActionResultBrainstormSession .
  • ActionResult <T>. Výsledek je .CreatedAtActionResult CreatedAtActionResult je podobná odpovědi vytvořené v roce 201 s hlavičkou Location .
  • ActionResult <T>. Hodnota je BrainstormSession typ.
  • Volání napodobení aktualizace relace , UpdateAsync(testSession)bylo vyvoláno. Volání Verifiable metody je kontrolováno spuštěním mockRepo.Verify() v kontrolních výrazech.
  • Pro relaci se vrátí dva Idea objekty.
  • Poslední položka ( Idea přidaná voláním UpdateAsyncnapodobení) odpovídá newIdea přidané relaci v testu.
[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);
}

Další prostředky