IActionResultCzęść NR.6 W tym wpisie pokaże Ci jak sprawdzać i testować zawartość IActionResult. Sprawdzimy widoki, do których nawigujemy i sprawdzimy też, czy zwracamy odpowiedni model w IActionResult.

W poprzednim wpisie zrobiliśmy dwa testy do kontrolera i próbowaliśmy postawić nasz projekt ASP.NET CORE (na razie bez sukcesu, bo nie mamy bazy SQL Server). 

Do skończenia tego przykładowego projektu TDD zostało nam jeszcze parę testów.

A dokładnie dwa. Oto wymagania, które nam zostały:

  • Chcemy zwrócić model błędu, gdy gra nie jest dostępna już do zakupu.
    • Użytkownik później ten model zobaczy na stronie HTML.
  • Chcemy sprawdzić, czy nasz kontroler zwraca poprawny IActionResult w metodzie BuyGame.
    • Jeżeli zakup wykonał się poprawnie, to chcemy odesłać użytkownika do odpowiedniej strony. Gdy zakup się, nie udał, to chcemy go przekierować do strony z błędem.

Chcielibyśmy, aby działanie naszego kontrolera wyglądał tak. Jak widzisz tłumaczymy model na request na samym początku. Potem wysyłamy zapytanie do procesora i robimy przekierowanie na odpowiednią stronę w zależności od tego, czy udało nam się kupić grę.

Co więcej, gdy nam się nie udało kupić gry, powinniśmy otrzymać model z błędem, który później zostanie obsłużony w widoku.

Diagram działania akcji BuyGame w Controller

Piszemy więc kolejny test.

Sprawdzenie obiektu zwracanego w IActionResult

Najpierw sprawdźmy, czy dodajemy model błędu, gdy gra nie jest dostępna.

[Fact]
public void ShouldAddModelErrorIfGameIsNotAvailabe()
{

}

W MOCK-u ustawiamy parametry zwrotne naszego fałszywego obiektu dzięki metodzie Setup.

W tym przypadku zwracamy status "GameIsNotAvailable".

[Fact]
public void ShouldAddModelErrorIfGameIsNotAvailabe()
{
    //Arange
    var processorMock = new Mock<IGameBuyingRequestProcessor>();
    var controller = new HomeController(processorMock.Object);
    var buyGame = new BuyGameModel() { Game = new GameModel() };
    
    processorMock.Setup(x => x.BuyGame(It.IsAny<GameBuyingRequest>()))
        .Returns(new GameBuyingResult()
        {
            PurchaseId = null,
            StatusCode = GameBuyingResultCode.GameIsNotAvailable
        });
    
    //Act
    var actionResult = controller.BuyGame(buyGame) as ViewResult;
    Assert.NotNull(actionResult);
    
    var errorModel = actionResult.Model;
    Assert.NotNull(errorModel);

}

Później chce sprawdzić w IActionResult, czy znajduje się mój model. Muszę rzutować IActionResult na ViewResult. We właściwości Model znajduje się mój model, który chce wysłać.

Analogicznie mógłbyś sprawdzić, czy dana akcja zwraca typ JSONResult lub RedirectResult.

Brakuje jeszcze sprawdzenia, czy typ klasy mojego modelu się zgadza. Tylko ja nie mam modelu "błędu"u w swojej aplikacji. Trzeba więc go utworzyć.

public class ErrorModel
{
    public string Message { get; set; }
}

Teraz mogę dodać ostatnie sprawdzenie typu modelu zwracanego.

var checktype = errorModel is ErrorModel;
Assert.True(checktype);

Alternatywnie można skorzystać z Assert-u, który sprawdza typ obiektu

Assert.IsType(typeof(ErrorModel), errorModel);

Skończyliśmy więc pisać nasz test. Teraz czas poprawić naszą akcję BuyGame w kontrolerze.

public IActionResult BuyGame(BuyGameModel buyGame)
{
    var request = new GameBuyingRequest()
    {
        Email = buyGame.Email,
        FirstName = buyGame.FirstName,
        LastName = buyGame.LastName,
    };
    request.Date = DateTime.Now;
    request.GameToBuy = new Game()
    {
        Id = buyGame.Game.Id,
        Name = buyGame.Game.Name
    };

    if (!ModelState.IsValid)
        return View(); //TODO

    var result = _processor.BuyGame(request);

    if (result.StatusCode == GameBuyingResultCode.GameIsNotAvailable)
        return View
        (
            new ErrorModel()
            { Message = "Nie ma już tej gry w magazynie" }
        );

    return View();
}

Test kończy się teraz na zielono. Możemy przejść do następnego wymagania.

Sprawdzenie, czy przekierowujemy się do odpowiednio widoku

Chcemy teraz zrobić test, który sprawdzi, czy przekierowujemy się do odpowiedniej strony w zależności od tego, czy gra jest dostępna, czy nie

Do projektu ASP.NET CORE dodałem następujące widoki:

Widoki w ASP.NET CORE

Chcemy sprawdzić, czy przekierowujemy użytkownika prawidłowo w zależności od operacji. Mamy więc dwie metody testujące, które chcą potwierdzić ten wymóg.

[Fact]
public void ShouldRedirectToGameIsNotAvaliableView()
{

}

[Fact]
public void ShouldRedirectToSuccessView()
{

}

Na podstawie poprzedniego przykładu już wiesz jak zdobyć informację o modelu zwrotnym. Analogicznie możesz sprawdzić, do jakiego widoku jesteśmy kierowani.

[Fact]
public void ShouldRedirectToGameIsNotAvaliableView()
{
    var buyGame = new BuyGameModel() { Game = new GameModel() };
    var processorMock = new Mock<IGameBuyingRequestProcessor>();
    var controller = new HomeController(processorMock.Object);

    processorMock.Setup(x => x.BuyGame(It.IsAny<GameBuyingRequest>()))
        .Returns(new GameBuyingResult()
        {
            PurchaseId = null,
            StatusCode = GameBuyingResultCode.GameIsNotAvailable
        });

    IActionResult actionResult = controller.BuyGame(buyGame);

    Assert.IsType<ViewResult>(actionResult);
    var viewResult = actionResult as ViewResult;

    Assert.NotNull(viewResult.ViewName);
    Assert.Equal(viewResult.ViewName, "Views/Home/GameIsNotAvaliable.cshtml");
}

Podobnie wygląda test sprawdzający sukces strony

[Fact]
public void ShouldRedirectToSuccessView()
{
    var buyGame = new BuyGameModel() { Game = new GameModel() };
    var processorMock = new Mock<IGameBuyingRequestProcessor>();
    var controller = new HomeController(processorMock.Object);

    processorMock.Setup(x => x.BuyGame(It.IsAny<GameBuyingRequest>()))
        .Returns(new GameBuyingResult()
        {
            PurchaseId = 77,
            StatusCode = GameBuyingResultCode.Success
        });

    IActionResult actionResult = controller.BuyGame(buyGame);

    Assert.IsType<ViewResult>(actionResult);
    var viewResult = actionResult as ViewResult;

    Assert.NotNull(viewResult.ViewName);
    Assert.Equal(viewResult.ViewName, "Views/Home/Success.cshtml");
}

Pora na mało zmianę w kodzie kontrolera, aby te nowe testy zakończyły się na zielono.

public IActionResult BuyGame(BuyGameModel buyGame)
{
    var request = new GameBuyingRequest()
    {
        Email = buyGame.Email,
        FirstName = buyGame.FirstName,
        LastName = buyGame.LastName,
    };
    request.Date = DateTime.Now;
    request.GameToBuy = new Game()
    {
        Id = buyGame.Game.Id,
        Name = buyGame.Game.Name
    };

    if (!ModelState.IsValid)
        return View(); //TODO

    var result = _processor.BuyGame(request);

    if (result.StatusCode == GameBuyingResultCode.GameIsNotAvailable)
        return View
        (
            "Views/Home/GameIsNotAvaliable.cshtml",
            new ErrorModel()
            { Message = "Nie ma już tej gry w magazynie" }
        );

    return View("Views/Home/Success.cshtml");
}

Skończyliśmy ?

Test się popsuły

Nie zupełnie, ponieważ nasz pierwszy test z tego wpisu uszkodził wszystkie inne testy, które zrobiliśmy w poprzednim wpisie. Teraz wiesz, dlaczego po każdym teście warto sprawdzić, czy poprzednie testy nadal działają.

Poza tym, jak zapewne zauważyłeś dużo kodu w tych testach się powtarza. Ciągle tworzymy modele, requesty, responsy i modyfikujemy odpowiedź metody BuyGame na MOCKU.

Testy się popsuły

Wszystko związane z tworzeniem obiektów pomocniczych ląduje do konstruktora.

public class HomeControllerTests
{
private Mock<IGameBuyingRequestProcessor> _processorMock;
private HomeController _homeController;
private BuyGameModel _buyGameModel;
private GameBuyingRequest _request;

public HomeControllerTests()
{
    _processorMock = new Mock<IGameBuyingRequestProcessor>();
    _homeController = new HomeController(_processorMock.Object);

    _buyGameModel = new BuyGameModel()
    {
        FirstName = "Cezary",
        Email = "Walenciuk@c.com",
        LastName = "Walenciuk",
        Game = new GameModel()
        {
            Id = 99,
            Name = "Mortal Kombat"
        }
    };

    _request = new GameBuyingRequest()
    {
        FirstName = "Cezary",
        LastName = "Walenciuk",
        Email = "Walenciuk@c.com",
        Date = DateTime.Now,
        GameToBuy = new Game() { Id = 99, Name = "Mortal Kombat" }
    };

    _processorMock.Setup(x => x.BuyGame(It.IsAny<GameBuyingRequest>()))
        .Returns(new GameBuyingResult()
        {
            PurchaseId = 11,
            StatusCode = GameBuyingResultCode.Success
        });
}

Teraz każdy z naszych testów jest bardziej przejrzysty. Pamiętaj jednak, że dla większości testów trzeba ponownie zrobić Setup dla fałszywego obiektu procesora.

[Fact]
public void ShouldCallGameBuyingRequestProcessor()
{
    //Act
    _homeController.BuyGame(_buyGameModel);

    //Assert
    Assert.Equal(_buyGameModel.FirstName, _request.FirstName);
    Assert.Equal(_buyGameModel.LastName, _request.LastName);
    Assert.Equal(_buyGameModel.Email, _request.Email);
    Assert.Equal(_buyGameModel.Game.Id, _request.GameToBuy.Id);
    Assert.Equal(_buyGameModel.Game.Name, _request.GameToBuy.Name);

    _processorMock.Verify(x => x.BuyGame
    (It.IsAny<GameBuyingRequest>()), Times.Once);
}

[Theory]
[InlineData(0, false)]
[InlineData(1, true)]
public void ShouldCallGameBuyingRequestProcessorIfModelIsValid
    (int expectedNumberOfCalls, bool isModelValid)
{
    if (!isModelValid)
    {
        _homeController.ModelState.AddModelError("JustTest", "AnErrorMessage");
    }

    //Act
    _homeController.BuyGame(_buyGameModel);

    //Assert
    _processorMock.Verify(x => x.BuyGame
    (It.IsAny<GameBuyingRequest>()), Times.Exactly(expectedNumberOfCalls));
    _homeController.ModelState.Clear();
}

[Fact]
public void ShouldAddModelErrorIfGameIsNotAvailabe()
{
    //Arangge
    _processorMock.Setup(x => x.BuyGame(It.IsAny<GameBuyingRequest>()))
        .Returns(new GameBuyingResult()
        {
            PurchaseId = null,
            StatusCode = GameBuyingResultCode.GameIsNotAvailable
        });

    //Act
    var actionResult = _homeController.BuyGame(_buyGameModel) as ViewResult;
    Assert.NotNull(actionResult);

    var errorModel = actionResult.Model;
    Assert.NotNull(errorModel);

    var checktype = errorModel is ErrorModel;
    Assert.True(checktype);
}

[Fact]
public void ShouldRedirectToGameIsNotAvaliableView()
{
    _processorMock.Setup(x => x.BuyGame(It.IsAny<GameBuyingRequest>()))
        .Returns(new GameBuyingResult()
        {
            PurchaseId = null,
            StatusCode = GameBuyingResultCode.GameIsNotAvailable
        });

    IActionResult actionResult = _homeController.BuyGame(_buyGameModel);

    Assert.IsType<ViewResult>(actionResult);
    var viewResult = actionResult as ViewResult;

    Assert.NotNull(viewResult.ViewName);
    Assert.Equal(viewResult.ViewName, "Views/Home/GameIsNotAvaliable.cshtml");
}

[Fact]
public void ShouldRedirectToSuccessView()
{
    IActionResult actionResult = _homeController.BuyGame(_buyGameModel);

    Assert.IsType<ViewResult>(actionResult);
    var viewResult = actionResult as ViewResult;

    Assert.NotNull(viewResult.ViewName);
    Assert.Equal(viewResult.ViewName, "Views/Home/Success.cshtml");
}

Teraz wszystkie testy są na zielono. Jak widzisz, refactoring testów jest potrzebny. W pewnym momencie ilość metod testowych może przytłoczyć i wszystko wgląda jak kod spaghetti. Dlatego o testy trzeba dbać.

Wszystkie testy na zielono

To było na tyle, jeśli chodzi o test i pisanie projektu w stylu TDD. Co prawda zostało nam jeszcze uruchomić aplikację ASP.NET CORE całościowo, ale do tego jest nam potrzebny SQL Server.

Musimy też uzupełnić HTML-em pliki .cshtml. Bez widoków i formularzy nie będziesz wiedział, jak akcje kontrolera naprawdę działają. Chociaż w testach wygląda na to, że jest wszystko okej.

To nie koniec jednak cyklu TDD zostało jeszcze dużo zagadnień.