Logika kontroleru testu jednotek v ASP.NET Core

Od Steve Smith

Testy jednotek zahrnují testování části aplikace izolovaně od její infrastruktury a závislostí. Při testování částí logiky kontroleru se testuje pouze obsah jedné akce, nikoli chování jeho závislostí nebo samotné architektury.

Kontrolery testování částí

Nastavte testy jednotek akcí kontroleru, abyste se zaměřili na chování kontroleru. Test jednotek kontroleru se vyhne 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, jsou zpracovávány integračními testy. Další informace o integračních testech najdete v tématu Integrační testy v ASP.NET Core .

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

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

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

Kontroler zobrazí seznam brainstormingových relací a umožňuje vytvářet nové Home brainstormingové relace pomocí požadavku 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:

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

  • Napodoní IBrainstormSessionRepository službu pomocí metody GetTestSessions . GetTestSessions vytvoří dvě napodobité brainstormingové relace s názvy kalendářních dat a relací.
  • Spustí Index metodu .
  • Provede kontrolní výrazy pro výsledek vrácený metodou :
[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 testuje, že:

Neplatný stav modelu se testuje přidáním chyb pomocí příkazu 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 model ModelState není platný, vrátí se totéž ViewResult jako pro požadavek GET. Test se nepokusí 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 testuje. Tyto testy jednotek testují pouze kód v metodě akce.

Druhý test ověří, zda je ModelState platná hodnota :

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

Napodobovaná volání, která nejsou volána, se obvykle ignorují, ale volání na konci volání instalace umožňuje napodobování Verifiable ověření v testu. To se provádí pomocí volání , které test selže, pokud nebyla volána mockRepo.Verify očekávaná metoda.

Poznámka

Knihovna Moq použitá v této ukázce umožňuje kombinovat ověřitelné nebo "striktní" napodobilí s ne ověřitelnými napodobami (také nazývanými "volné" napodočení nebo zástupné procedury). Přečtěte si další informace o přizpůsobení napodobování chování pomocí Moq.

SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní relací brainstormingu. Kontroler obsahuje logiku pro zpracování neplatných hodnot (v následujícím příkladu existují dva scénáře, které tyto id return scénáře pokrývají). Poslední příkaz return vrátí novou hodnotu zobrazení ( StormSessionViewModel Controllers/SessionController.cs):

public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

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

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

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

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

        return View(viewModel);
    }
}

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ři přechodu na kontroler Ideas zpřístupní aplikace funkce jako webové rozhraní API na api/ideas trase:

  • Metoda vrátí seznam nápadů ( ) spojených s IdeaDTO brainstormingovou ForSession relací.
  • 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 zahrnují více dat, než klient vyžaduje.
  • Zbytečně směšte interní doménový model aplikace s veřejně zveřejněným rozhraním API.

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

Dále ukázková aplikace předvede testy jednotek pro metody rozhraní API kontroleru Create ForSession Ideas a .

Ukázková aplikace obsahuje dva ForSession testy. První test určí, jestli se pro neplatnou relaci vrátí ForSession NotFoundObjectResult hodnota (HTTP nena nalezena):

[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 vrátí seznam nápadů na relace ( ) pro ForSession <List<IdeaDTO>> platnou relaci. Kontroly také prověří první myšlenku, aby se potvrdilo, že Name je její vlastnost správná:

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

K otestování chování metody, když je neplatný, přidá ukázková aplikace do kontroleru v rámci Create ModelState testu chybu modelu. V testech jednotek se nepokoušejte testovat ověření modelu ani vazbu modelu, pouze otestujte chování metody akce, když je zachované — neplatným znakem 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);
}

Druhý test metody závisí na tom, které úložiště vrací , takže Create null napodobné úložiště je nakonfigurované tak, aby vrátilo null . Není nutné vytvářet testovací databázi (v paměti ani jinak) a vytvářet dotazy, které vrátí tento výsledek. Test lze provést jedním příkazem, 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í test ověří, že je volána Create Create_ReturnsNewlyCreatedIdeaForSession metoda UpdateAsync úložiště. Napodobil se s a volá se metoda napodobilí úložiště, aby se potvrdilo, že se spustí Verifiable Verify ověřitelná metoda. Není zodpovědností testu jednotek zajistit, aby metoda uložila data, která lze provést pomocí UpdateAsync — integračního testu.

[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 verze 2.1 nebo novější umožňuje ActionResult <T> ( ) vrátit typ odvozený z nebo ActionResult<TValue> vrátit konkrétní ActionResult typ.

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

V rozhraní jsou ForSessionActionResult zahrnuty dva testy kontroleru. ApiIdeasControllerTests

První test potvrdí, že kontroler vrátí seznam nápadů pro neexistující relaci, ale ne ActionResult seznam neexistujících id nápadů:

[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 id druhý test potvrdí, že metoda vrátí:

  • S ActionResult List<IdeaDTO> typem.
  • Hodnota ActionResult <T> Hodnota je List<IdeaDTO> typ.
  • První položkou v seznamu je platná myšlenka, která odpovídá myšlence uložené v napodobné relaci (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 pro 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);
}

Součástí jsou CreateActionResult tři testy ApiIdeasControllerTests .

První text potvrdí, že se BadRequest vrátí 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 se NotFound 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);
}

U platné relace id finální test potvrdí, že:

  • Metoda vrátí s ActionResult BrainstormSession typem .
  • Hodnota ActionResult <T> Výsledkem je CreatedAtActionResult . CreatedAtActionResult je obdobou odpovědi 201 Created (Vytvořeno) s Location hlavičkou.
  • Hodnota ActionResult <T> Hodnota je BrainstormSession typ.
  • Bylo vyvoláno napodobné volání pro aktualizaci UpdateAsync(testSession) relace . Volání Verifiable metody se kontroluje spuštěním příkazu v mockRepo.Verify() kontrolních výrazech.
  • Pro Idea relaci se vrátí dva objekty.
  • Poslední položka (přidaná pomocí napodobového volání ) odpovídá položce přidané Idea UpdateAsync do relace v newIdea 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í hlavní roli v jakékoli ASP.NET Core MVC. Proto byste měli mít jistotu, že se kontrolery chovají tak, jak mají. Automatizované testy mohou detekovat chyby před nasazením aplikace do produkčního prostředí.

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

Testy jednotek logiky kontroleru

Testy jednotek zahrnují testování části aplikace izolovaně od její infrastruktury a závislostí. Při testování částí logiky kontroleru se testuje pouze obsah jedné akce, nikoli chování jeho závislostí nebo samotné architektury.

Nastavte testy jednotek akcí kontroleru, abyste se zaměřili na chování kontroleru. Test jednotek kontroleru se vyhne 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, jsou zpracovávány integračními testy. Další informace o integračních testech najdete v tématu Integrační testy v ASP.NET Core .

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

Pokud chcete předvést testování jednotek kontroleru, prohlédněte si následující kontroler v ukázkové aplikaci. Kontroler zobrazí seznam brainstormingových relací a umožňuje vytvářet nové Home brainstormingové relace pomocí požadavku 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:

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

  • Napodoní IBrainstormSessionRepository službu pomocí metody GetTestSessions . GetTestSessions vytvoří dvě napodobité brainstormingové relace s názvy kalendářních dat a relací.
  • Spustí Index metodu .
  • Provede kontrolní výrazy pro výsledek vrácený metodou :
[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 testuje, že:

Neplatný stav modelu se testuje přidáním chyb pomocí příkazu 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 model ModelState není platný, vrátí se totéž ViewResult jako pro požadavek GET. Test se nepokusí 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 testuje. Tyto testy jednotek testují pouze kód v metodě akce.

Druhý test ověří, zda je ModelState platná hodnota :

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

Napodobovaná volání, která nejsou volána, se obvykle ignorují, ale volání na konci volání instalace umožňuje napodobování Verifiable ověření v testu. To se provádí pomocí volání , které test selže, pokud nebyla volána mockRepo.Verify očekávaná metoda.

Poznámka

Knihovna Moq použitá v této ukázce umožňuje kombinovat ověřitelné nebo "striktní" napodočení s ne ověřitelnými napodobami (také nazývanými "volné" napodočení nebo zástupné procedury). Přečtěte si další informace o přizpůsobení chování napodoba pomocí Moq.

SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní relací brainstormingu. Kontroler obsahuje logiku pro zpracování neplatných hodnot (v následujícím příkladu existují dva scénáře, které tyto id return scénáře pokrývají). Poslední příkaz return vrátí novou hodnotu zobrazení ( StormSessionViewModel Controllers/SessionController.cs):

public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

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

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

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

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

        return View(viewModel);
    }
}

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ři přechodu na kontroler Ideas zpřístupní aplikace funkce jako webové rozhraní API na api/ideas trase:

  • Metoda vrátí seznam nápadů ( ) spojených s IdeaDTO brainstormingovou ForSession relací.
  • 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 zahrnují více dat, než klient vyžaduje.
  • Zbytečně směšte interní doménový model aplikace s veřejně zveřejněným rozhraním API.

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

Dále ukázková aplikace předvede testy jednotek pro metody rozhraní API kontroleru Create ForSession Ideas a .

Ukázková aplikace obsahuje dva ForSession testy. První test určí, jestli se pro neplatnou relaci vrátí ForSession NotFoundObjectResult hodnota (HTTP nena nalezena):

[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 vrátí seznam nápadů na relace ( ) pro ForSession <List<IdeaDTO>> platnou relaci. Kontroly také prověří první myšlenku, aby se potvrdilo, že Name je její vlastnost správná:

[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í metody, když je neplatný, ukázková aplikace přidá do kontroleru v rámci Create ModelState testu chybu modelu. V testech jednotek se nepokoušejte testovat ověření modelu ani vazbu modelu, pouze otestujte chování metody akce, když je zachované — neplatným znakem 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);
}

Druhý test metody závisí na tom, které úložiště vrací , takže Create null napodobné úložiště je nakonfigurované tak, aby vrátilo null . Není nutné vytvářet testovací databázi (v paměti ani jinak) a vytvářet dotaz, který vrátí tento výsledek. Test lze provést jediným příkazem, 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í test ověří, že je volána Create Create_ReturnsNewlyCreatedIdeaForSession metoda UpdateAsync úložiště. Napodobil se s a volá se metoda napodobilí úložiště, aby se potvrdilo, že se spustí Verifiable Verify ověřitelná metoda. Není zodpovědností testu jednotek zajistit, aby metoda uložila data, která lze UpdateAsync — provést pomocí integračního testu.

[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 verze 2.1 nebo novější umožňuje ActionResult <T> ( ) vrátit typ odvozený z nebo ActionResult<TValue> vrátit konkrétní ActionResult typ.

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

V rozhraní jsou ForSessionActionResult zahrnuty dva testy kontroleru. ApiIdeasControllerTests

První test potvrdí, že kontroler vrátí seznam nápadů pro neexistující relaci, ale ne ActionResult seznam neexistujících id nápadů:

[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 id druhý test potvrdí, že metoda vrátí:

  • S ActionResult List<IdeaDTO> typem.
  • Hodnota ActionResult <T> Hodnota je List<IdeaDTO> typ.
  • První položkou v seznamu je platná myšlenka, která odpovídá myšlence uložené v napodobné relaci (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 pro 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);
}

Součástí jsou CreateActionResult tři testy ApiIdeasControllerTests .

První text potvrdí, že se BadRequest vrátí 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 se NotFound 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 id konečný test potvrzuje, že:

  • Metoda vrací ActionResult s BrainstormSession typem.
  • ActionResult <T> . Výsledkem je CreatedAtActionResult . CreatedAtActionResult je analogický k 201 vytvořené odpovědi s Location hlavičkou.
  • ActionResult <T> . Hodnota je BrainstormSession typ.
  • UpdateAsync(testSession)Bylo vyvoláno nepoužité volání aktualizace relace. VerifiableVolání metody je kontrolováno spuštěním mockRepo.Verify() ve kontrolním výrazu.
  • IdeaPro relaci jsou vraceny dva objekty.
  • Poslední položka ( Idea přidaná přívoláním metody) se UpdateAsync shoduje s newIdea přidáním do relace 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ší zdroje informací