[testy] Jak zmockować RoleManager, żeby można było ustawić Setup dla metod statycznych?

0

Witam.
Robię aplikację w ASP.NET MVC 5 (.NET Framework) z Entity Framework 6.2.
Do testów używam NUnit i Moq.
Potrzebuję zrobić test, żeby sprawdzić, czy roleResult.Succeeded daje true, gdy metoda statyczna RoleExists() zwraca fałsz dla roli "User".

HomeController.cs :

private ApplicationDbContext _db;
private RoleManager<IdentityRole> _roleManager;
private ApplicationUserManager _userManager;

public HomeController()
{
	_db = new ApplicationDbContext();
	_roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(_db));
	_userManager = new ApplicationUserManager(new UserStore<ApplicationUser>(_db));
	if (!_roleManager.RoleExists("User"))
	{
		IdentityResult roleResult = _roleManager.Create(new IdentityRole("User"));
	}
}

public HomeController(ApplicationDbContext context, ApplicationUserManager userManager, RoleManager<IdentityRole> roleManager, string role) // DI for testing
{
	_db = context;
	_roleManager = roleManager;
	_userManager = userManager;
	if (!_roleManager.RoleExists(role))
	{
		IdentityResult roleResult = _roleManager.Create(new IdentityRole(role));
	}
}

HomeTest.cs :

[Test]
public void HomeController_RoleDoesNotExist_AddRole()
{
	var mockContext = new Mock<ApplicationDbContext>();
	var mockUserManager = new Mock<ApplicationUserManager>();
	var mockRoleManager  = new Mock<RoleManager<IdentityRole>>();

	mockRoleManager.Setup(x => x.RoleExists("User")).Returns(false); // Moq wyrzuca tutaj błąd
	
	var homeController = new HomeController(mockContext.Object, mockUserManager.Object, mockRoleManager.Object, "User");

	// Assert, który muszę zrobić, ale nie wiem jak, bo muszę najpierw wziąć skądś wartość roleResult.Succeeded
}

Po pierwsze: Nie wiem jak zaimplementować kod do mojego testu, który ma być odpowiedzialny za zwrócenie wartości roleResult.Succeeded, która posłuży mi w Assert.

Po drugie: Moq nie może wykonać Setup dla metod statycznych i nie wiem jak sobie z tym poradzić w mojej sytuacji. Prawdopodobnie potrzebuje zamienić prywatne właściwości w klasie HomeController z nowymi interfejsami, ale wtedy kod, który jest w konstruktorze HomeController() będzie miał niezgodność typowania, na przykład: _db = new ApplicationDbContext() nie przekonwertuje na INewDbContext. Tutaj chodzi o to, że te interfejsy mają implementować to co te obiekty (np: new ApplicationDbContext()) i mają być użyte do mockowania.

Z góry dzięki za pomoc!

0

Tak po kolei, to:

  1. Kontroler nie powinien operować na DbContext.
  2. Sprawdzanie ról też nie powinno być w kontrolerze tylko w jakimś filtrze. I zdaje się, że jest do tego wbudowany filtr (Authorize).
  3. Testowanie kontrolerów to bezsensowny wysiłek. W kontrolerach nie powinno być niczego, co byłoby sens testować.
  4. Oddzielny konstruktor na potrzeby testów... zgroza.
0
  1. W tym kontrolerze są metody, które potrzebują pobrać coś z bzy danych i wrzucić to na widok, więc nie wiem jak bez prywatnego ApplicationDbContext miałoby to wyglądać.
  2. To jest tylko sprawdzenie czy czy istnieje rola, żeby _roleManager.Create nie wykonywać za każdym razem. Musiałbym chyba jakąś migracje zrobić, żeby te role dodawało przy tworzeniu bazy danych, ale załóżmy, że tak ma być jak jest.
  3. W znaczniej mierze moim zadaniem jest właśnie testowanie metod tego kontrolera, zwłaszcza, że moja aplikacja jest prosta.
  4. Na prawdę nie mam pojęcia jak to inaczej zrobić, żeby przetestować kontroler. Dependency injection przez konstruktor w kontrolerze to najlepsze co mi do głowy przyszło.

A co z moim problemem? Czytałem, że można zrobić jakieś wrappery w związku z tym, że Moq nie lubi metod statycznych, ale nie podołałem w ogarnianiu tego...
Jedyne co mi do głowy przychodzi to ogarnąć jakiś inny framework do mockowania.

1
rafineria888 napisał(a):
  1. W tym kontrolerze są metody, które potrzebują pobrać coś z bzy danych i wrzucić to na widok

Ale to jest właśnie błąd. Kontroler nie powinien zajmować się pobieraniem danych z bazy. Kontroler nie służy do realizacji logiki biznesowej.
http://commitandrun.pl/2016/05/30/Brutalne_prawdy_o_MVC/

więc nie wiem jak bez prywatnego ApplicationDbContext miałoby to wyglądać.

To powinno znajdować się w klasie z odpowiedniej warstwy, do której kontroler przekazuje wykonanie żądanego przez użytkownika zadania.

  1. To jest tylko sprawdzenie czy czy istnieje rola, żeby _roleManager.Create nie wykonywać za każdym razem. Musiałbym chyba jakąś migracje zrobić, żeby te role dodawało przy tworzeniu bazy danych, ale załóżmy, że tak ma być jak jest.

Czyli napisałeś ifa, który ma się wykonać tylko raz, a wszystkie pozostałe razy nie. Wspaniały pomysł!

  1. W znaczniej mierze moim zadaniem jest właśnie testowanie metod tego kontrolera, zwłaszcza, że moja aplikacja jest prosta.

Nawet w prostych aplikacjach można nie wpychać logiki do kontrolerów tylko stosować dobre praktyki.

  1. Na prawdę nie mam pojęcia jak to inaczej zrobić, żeby przetestować kontroler. Dependency injection przez konstruktor w kontrolerze to najlepsze co mi do głowy przyszło.

Abstrahując od tego, że najlepszym rozwiązaniem jest po prostu nie pchać logiki do kontrolerów, więc nie będzie czego testować, to DI jest dobrym pomysłem. Pytanie tylko, czemu używasz go jedynie w testach, a nie w normalnej pracy aplikacji? Po co ten bezparametrowy konstruktor, który zabiera wszystkie zyski z DI?

A co z moim problemem? Czytałem, że można zrobić jakieś wrappery w związku z tym, że Moq nie lubi metod statycznych, ale nie podołałem w ogarnianiu tego...
Jedyne co mi do głowy przychodzi to ogarnąć jakiś inny framework do mockowania.

Powodzenia.
Mockowanie to nie są jakieś czary, wszystko działa dzięki cechom języka takim jak dziedziczenie i nadpisywanie metod wirtualnych oraz generowanie w locie obiektów, które np. implementują mockowany interfejs. Metody statyczne nie są wirtualne, nie podlegają nadpisywaniu przy dziedziczeniu (ani dziedziczeniu w ogóle), więc normalny framework mockujący tego nie zrobi. Są ponoć jakieś płatne rozwiązania do tego, lepsze edycje VS też ponoć potrafią. Ale te narzędzia powstały z myślą o dopisywaniu testów do istniejących projektów słabej jakości i nietestowalnych w normalny sposób.

Możesz sobie zdefiniować jakiś interfejs z metodami CreateRole i RoleExists, zaimplementować ten interfejs w jakiejś klasie, która będzie używała wewnętrznie RoleManager<IdentityRole>. Kontroler niech przyjmuje w zależnościach ten nowy interfejs, dzięki czemu napiszesz test, który chcesz napisać, a przy okazji stworzysz jeszcze więcej spaghetti.

0

Przeniosłem kilka rzeczy (namespace to pokaże) do nowych plików i rozszerzyłem trochę tą nieszczęśliwą logikę dodawania ról (załóżmy, że po prostu to sprawdzanie i dodawanie ról musi być zaprogramowane przeze mnie).

ContextProvider.cs:

namespace ASP_Projekt.Models
{
    public interface IContextProvider
    {
        ApplicationDbContext GetContext();
    }

    public class ContextProvider : IContextProvider
    {
        public ApplicationDbContext GetContext()
        {
            return new ApplicationDbContext();
        }
    }
}

AppRoleManager.cs:

namespace ASP_Projekt.Models
{
    public interface IAppRoleManager
    {
        bool RoleExists(string roleName);
        IdentityResult Create(IdentityRole role);
    }

    public class AppRoleManager : IAppRoleManager
    {
        private ApplicationDbContext _db;
        private RoleManager<IdentityRole> _roleManager;

        public AppRoleManager()
        {
            _db = new ContextProvider().GetContext();
            _roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(_db));
        }

        public AppRoleManager(ApplicationDbContext context, RoleManager<IdentityRole> roleManager)
        {
            _db = context;
            _roleManager = roleManager;
        }

        public bool RoleExists(string roleName)
        {
            return _roleManager.RoleExists(roleName);
        }

        public IdentityResult Create(IdentityRole role)
        {
            return _roleManager.Create(role);
        }

        public void CloseDb()
        {
            _db.Dispose();
        }
    }
}

RoleService.cs:

namespace ASP_Projekt.Services
{
    public class RoleService
    {
        private IAppRoleManager _roleManager;
        public RoleService(IAppRoleManager roleManager)
        {
            _roleManager = roleManager;
        }

        public void RoleInit(List<string> roles)
        {
            foreach (string role in roles)
            {
                if (!_roleManager.RoleExists(role))
                {
                    IdentityResult roleResult = _roleManager.Create(new IdentityRole(role));
                    if (roleResult.Errors.Any())
                    {
                        foreach (string error in roleResult.Errors)
                        {
                            throw new Exception(error);
                        }
                    }
                }
            }
        }
    }
}

Teraz chyba mogę zacząć mockowanie:
HomeTest.cs:

var mockRoleManager  = new Mock<IAppRoleManager>();
mockRoleManager.Setup(x => x.RoleExists("User")).Returns(false);
mockRoleManager.Setup(x => x.Create(new IdentityRole() { Id = "id1", Name = "User"}));

var roleService = new RoleService(mockRoleManager.Object);
roleService.RoleInit(new List<string>()
{
	"User", "Admin"
});

Poproszę o komentarz do tego.
Dodatkowo nie wiem jak zrobić Assert dla tego testu (info w pierwszym poście).

0

Niedobrze.
Kontrolery powinny wywoływać odpowiednie metody z serwisu i zwracać wynik. Nic więcej. Wszystkim zajmuje się serwis. Np. kontroler WebApi (pomijam adnotacje):

public class OrderController: ControllerBase
{
  IOrderService service;

  public OrderController(IOrderService service)
  {
      this.service = service;
  }

  [HttpPost]
  public async Task<IActionResult> CreateOrder([FromBody]Order order)
  {
      var result = await service.CreateOrder(order);
      if(!result.IsSucceess)
          return BadRequest(result);
      else
          return Ok(order);
  }
}

To jest prosty przykład kontrolera. Serwis powinien zwracać jakiś rezultat. To może być cokolwiek, ale dobrze żeby to był jakiś IdentityResult albo coś w tym rodzaju (coś, co będzie miało kod błędu i jego opis).
Jak widzisz, nie ma tu ŻADNEJ logiki. Nie ma tu niczego do testowania. To wszystko dzieje się w serwisie i to serwis ma być testowany, np:

public class OrderService: IOrderService
{
    ApplicationDbContext db;
    public OrderService(ApplicationDbContext db)
    {
        this.db = db;
    }

    public async Task<IdentityResult> CreateOrder(Order order)
    {
        //tutaj jakaś logika to utworzenia order w bazie danych i na końcu:
        try
        {
           db.BeginTransaction();
           db.Save(order);
           db.CommitTransaction();
           return CreateOkResult();
        }
        catch(Exception ex)
        {
            db.RollbackTransaction();
            return CreateErrorResult(ex);
        }        
    }
}

To taki przykład z obsługą transakcji. Jak widzisz, jest tu TYLKO JEDEN konstruktor, który przyjmuje odpowiednie obiekty przez DI. U Ciebie dodatkowo jest konstruktor bezparametrowy, który odwołuje się do jakiegoś globalnego obiektu. To jest błąd, utrudniający poważnie testy jednostkowe.
W moim przypadku możesz sobie zamockować ApplicationDbContext i normalnie testować. Ponadto możesz sobie zamockować OrderService, jeśli potrzebujesz użyć tego w innym serwisie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1