FluentAssertions.Mvc – asserty dla ASP.NET MVC

Wprowadzenie

Dwa tygodnie temu opublikowałem wpis o bibliotece Fluent Assertions, która bardzo ułatwia pisanie assertów w testach (zachęcam do przeczytania w pierwszej kolejności owego wpisu). Pokazałem, jak korzystać z biblioteki oraz jak łatwo można rozszerzać możliwości biblioteki o własne metody. Dzisiaj natomiast pokażę Ci, jak testować kontrolery w ASP.NET MVC za pomocą rozszerzenia o nazwie FluentAssertions.MVC (https://github.com/fluentassertions/fluentassertions.mvc). Poćwiczymy również dodawanie kolejnych metod rozszerzających możliwości Fluent Assertions.

Przykład do testów

Zacznę od przedstawienia kodu, który będę chciał przetestować. Ma on trzy proste akcje. Details służy do wyświetlenia informacji o produkcie, natomiast dwie akcje Create służą do dodania nowego produktu. Kontroler wygląda tak:

public class ProductsController : Controller
{
private Lazy<IProductLogic> _logic;
protected IProductLogic Logic
{
get { return _logic.Value; }
}
private Lazy<IMapper> _mapper;
protected IMapper Mapper
{
get { return _mapper.Value; }
}
public ProductsController(Lazy<IProductLogic> logic,
Lazy<IMapper> mapper)
{
_logic = logic;
_mapper = mapper;
}
public ActionResult Details(int id)
{
var result = Logic.GetById(id);
if (result.Success == false)
{
return RedirectToAction("Index");
}
var viewModel = Mapper.Map<ProductViewModel>(result.Value);
return View(viewModel);
}
public ActionResult Create()
{
return View(new ProductViewModel());
}
[HttpPost]
public ActionResult Create(ProductViewModel viewModel)
{
var product = Mapper.Map<Product>(viewModel);
var result = Logic.Create(product);
if (result.Success == false)
{
result.AddErrorToModelState(ModelState);
return View(viewModel);
}
return RedirectToAction("Index");
}
}

Kontroler posiada dwie zależności. Pierwsza to zależność związana z logiką biznesową produktów, natomiast druga to zależność dotycząca automappera. Logika jest bardzo prosta i zawiera dwie metody, których, myślę, nie trzeba tłumaczyć:

public interface IProductLogic
{
Result<Product> GetById(int id);
Result<Product> Create(Product product);
}

Testy akcji Details

Do testów, poza Fluent Assertions, wykorzystuję również xUnit, Moq oraz nBuilder.

Pierwsze testy napiszemy do akcji Details. W tym przypadku (tak jak zrobiłem to również w innych wpisach o testach) utworzyłem klasę bazową dla wszystkich testów akcji w kontrolerze. W klasie tej znajduje się metoda Create tworząca kontroler do testów i jego wszystkie zależności:

public class BaseTest
{
protected Mock<IProductLogic> Logic { get; set; }
protected Mock<IMapper> Mapper { get; set; }
protected virtual ProductsController Create()
{
Logic = new Mock<IProductLogic>();
Mapper = new Mock<IMapper>();
return new ProductsController(new Lazy<IProductLogic>(() => Logic.Object),
new Lazy<IMapper>(() => Mapper.Object));
}
}
view raw BaseTest.cs hosted with ❤ by GitHub

Rozszerzenie Fluent Assertions dla projektów ASP.NET MVC ma kilka wersji. W zależności od użytego ASP.NET MVC używamy odpowiedniej wersji pakietu. W przykładzie używam wersji 5, dlatego zainstalowałem pakiet FluentAssertions.Mvc5 (https://www.nuget.org/packages/FluentAssertions.Mvc5/). Jeśli używasz wcześniejszej wersji ASP.NET MVC, to wtedy odpowiednio zmieniasz numer na końcu nazwy pakietu.

Testy akcji Details są w dedykowanej klasie, która wygląda tak:

public class DetailsTests : BaseTest
{
protected ProductViewModel ViewModel { get; set; }
protected Product Product { get; set; }
protected Result<Product> ProductResult { get; set; }
protected override ProductsController Create()
{
var controller = base.Create();
Product = Builder<Product>.CreateNew().Build();
ProductResult = Result.Ok(Product);
Logic.Setup(l => l.GetById(It.IsAny<int>()))
.Returns(() => ProductResult);
ViewModel = Builder<ProductViewModel>.CreateNew().Build();
Mapper.Setup(m => m.Map<ProductViewModel>(It.IsAny<Product>()))
.Returns(ViewModel);
return controller;
}
[Fact]
public void Redirect_To_Index_When_Result_Is_Failure()
{
var controller = Create();
ProductResult = Result.Failure<Product>("Property", "Error");
var result = controller.Details(10);
result.Should()
.BeRedirectToRouteResult()
.WithAction("Index");
}
[Fact]
public void Return_View_With_Data_When_Result_Is_Failure()
{
var controller = Create();
var result = controller.Details(10);
result.Should()
.BeViewResult()
.WithDefaultViewName()
.Model
.Should()
.BeEquivalentTo(ViewModel);
}
}

W klasie nadpisuję metodę Create z klasy bazowej, w której dodatkowo konfiguruję podstawowe zachowanie mocków. Dzięki temu same testy będą już prostsze i zawierać będą tylko drobne zmiany w domyślnej konfiguracji.

Pierwszy test sprawdza, jak zachowa się akcja, gdy logika biznesowa zwróci błędny wynik (na ogół oznacza to, że produktu o danym ID nie ma w bazie). W przykładzie założyłem, że w takiej sytuacji użytkownik ma zostać przekierowany do akcji Index z listą wszystkich produktów.

W teście użyłem dodatkowych metod z FluentAssertions.MVC. Metoda BeRedirectToRouteResult sprawdza, czy wynikiem akcji jest przekierowanie. WithAction sprawdza natomiast nazwę akcji, do której użytkownik zostanie przekierowany.

Drugi test sprawdza sytuację, w której logika zwróci dane produktu, oraz czy akcja wyświetli odpowiedni widok i przekaże do niego odpowiednie dane (viewmodel). Metoda BeViewResult sprawdza, czy wynikiem jest widok, natomiast WithDefaultViewName sprawdza, czy nazwa widoku jest domyślna (taka sama, jak nazwa akcji). Na końcu sprawdzam, czy obiekt przekazany do widoku jest taki sam, jak viewmodel zwrócony przez automapper.

Jak widać, dodatkowe metody bardzo ułatwiają pisanie testów kontrolerów.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Brakujące metody

FluentAssertions.MVC zawiera sporo metod, które przydają się w testowaniu kontrolerów. Mnie jednak brakowało jednej metody, którą dodaję do swoich projektów.

Do wyświetlenia błędów walidacji wykorzystuję domyślny mechanizm z ASP.NET MVC. Kontroler przekopiowuje błędy z klasy Result do ModelState, dlatego potrzebuje prostej metody, która sprawdzałaby, czy w ModelState znajduje błąd zwrócony przez logikę. Metoda wygląda tak:

public static class ControllerExtensions
{
public static ControllerAssertions Should(this Controller instance)
{
return new ControllerAssertions(instance);
}
}
public class ControllerAssertions : ReferenceTypeAssertions<Controller, ControllerAssertions>
{
public ControllerAssertions(Controller controller)
{
Subject = controller;
}
protected override string Identifier => "controller";
public ControllerAssertions HasError(string property, string message, string because = "",
params object[] becauseArgs)
{
var state = Subject.ModelState.FirstOrDefault(m => m.Key == property);
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(state.Key == property)
.FailWith($"The controller should have error for property: {property}.");
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(state.Value.Errors.Any(e => e.ErrorMessage == message))
.FailWith($"The controller should have error with message: {message}.");
return this;
}
}

Podobnie, jak to było wcześniej, aby rozszerzyć Fluent Assertions, musimy dodać dwie klasy. Pierwsza dodaje metodę Should do typu, który chcemy testować (tutaj klasę Controller). Druga klasa dziedziczy natomiast po ReferenceTypeAssertions i zawiera metodę HasError, która sprawdza, czy w ModelState jest błąd o określonym komunikacie dla odpowiedniej właściwości.

Metody tej użyjemy w teście akcji Create naszego testowego kontrolera.

Testy akcji Create

W przykładzie przygotowałem jeden test akcji Create (powiązanej z żądaniem post), aby sprawdzić użycie metody HasError.

Podobnie jak wyżej, test znajduje się w dedykowanej klasie dla akcji Create. W klasie tej nadpisujemy metodę Create z domyślną konfiguracją mocków. Sama klasa z testem wygląda tak:

public class CreateTests : BaseTest
{
protected ProductViewModel ViewModel { get; set; }
protected Product Product { get; set; }
protected Result<Product> ProductResult { get; set; }
protected override ProductsController Create()
{
var controller = base.Create();
Product = Builder<Product>.CreateNew().Build();
ProductResult = Result.Ok(Product);
Logic.Setup(l => l.Create(It.IsAny<Product>()))
.Returns(() => ProductResult);
ViewModel = Builder<ProductViewModel>.CreateNew().Build();
Mapper.Setup(m => m.Map<Product>(It.IsAny<ProductViewModel>()))
.Returns(Product);
return controller;
}
[Fact]
public void Return_View_With_Errors_When_Result_Is_Failure()
{
var controller = Create();
ProductResult = Result.Failure<Product>("Property", "Error");
var result = controller.Create(ViewModel);
result.Should()
.BeViewResult()
.WithDefaultViewName()
.Model
.Should()
.BeEquivalentTo(ViewModel);
controller.Should()
.HasError("Property", "Error");
}
}
view raw CreateTests.cs hosted with ❤ by GitHub

Test w pierwszej kolejności sprawdza, czy akcja zwróciła domyślny widok z przekazanym viewmodelem (wykorzystałem te same metody, co w teście akcji Details). Na końcu za pomocą dodanej metody HasError sprawdzam, czy w ModelState znajduje się taki sam błąd, jaki zwróciła logika biznesowa (komunikat „Error” dla właściwości „Property”).

Przykład

Pracując nad tym wpisem, rozszerzyłem przykład z poprzedniego wpisu o Fluent Assertions. Dla przypomnienia – wpis znajduje się w repozytorium https://github.com/danielplawgo/FluentAssertionTests. Po pobraniu nie wymaga jakiejś dodatkowej konfiguracji.

Podsumowanie

Mam nadzieję, że po raz kolejny udało mi się pokazać Ci, jak fajną biblioteką jest Fluent Assertions. Tym razem zobaczyłeś, jak łatwo pisać testy kontrolerów w ASP.NET MVC. Zobaczyłeś również, jak rozszerzyć dostępne metody w sytuacji, gdy czegoś brakuje – czy to w samym Fluent Assertions, czy w jakimś jej rozszerzeniu.

Gorąco zachęcam do używania Fluent Assertions!

Szkolenie Automatyczne testy w .NET 5

SzkolenieAutomatyczne testy w .NET 5

Zainteresował Ciebie ten temat? A może chcesz więcej? Jak tak to zapraszam na moje autorskie szkolenie o automatycznych testach w .NET..

2 thoughts on “FluentAssertions.Mvc – asserty dla ASP.NET MVC

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.