Jak napisać testy klasy która dodaje użytkownika?

0

Cześć,
mam problem z ogarnięciem testów jednostkowych. Być może moje podejście w ogóle jest błędne, więc prosiłbym o nakierowanie na właściwą drogę :)

Mam klasę, która w założeniu ma służyć m.in. do zakładania konta dla nowego użytkownika:

public class AccountService : IAccountService
{
	private readonly DbContext _dbContext;
	private readonly IMapper _mapper;
	private readonly IPasswordHasher<User> _passwordHasher;
	private readonly JwtConfig _jwt;

	public AccountService(DbContext dbContext, IMapper mapper, IPasswordHasher<User> passwordHasher, IOptionsMonitor<Jwt> optionsMonitor)
	{
		_dbContext = dbContext;
		_mapper = mapper;
		_passwordHasher = passwordHasher;
		_jwt = optionsMonitor.CurrentValue;
	}

	public async Task CreateAccount(CreateAccountDto dto)
	{   
		[cała logika, np. mapowanie z dto czy hashowanie hasła]
		
		_dbContext.Users.Add(user);
		await _dbContext.SaveChangesAsync();			
	}
}

I teraz chciałem wykonać test jednostkowy. Na ten moment mam to:

[Fact]
public async Task CreateAccount_ForValidData_InvokeSaveChangesAsync()
{
	//arrange
	var systemDbContextMock = new Mock<SystemDbContext>();
	systemDbContextMock.Setup(x => x.Users.Add(new User() { Name = "", Email = "", PasswordHash = "" })).Returns((EntityEntry<User> u) => u);

	var mapperMock = new Mock<IMapper>();
	var passwordHasherMock = new Mock<IPasswordHasher<User>>();
	var jwtMock = new Mock<IOptionsMonitor<JwtConfig>>();

	var accountService = new AccountService(systemDbContextMock.Object, mapperMock.Object, passwordHasherMock.Object, jwtMock.Object);

	//act
	await accountService.CreateAccount(new CreateAccountDto() { Name = "", Email = "", Password = "" });
	
	//assert
	systemDbContextMock.Verify(m => m.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once());
}

Czyli w założeniu test powinien zakończyć się powodzeniem jeżeli zostanie wywołana metoda SaveChangesAsync.
Przy uruchomieniu testu mam jednak komunikat:

Message:
System.NotSupportedException : Specified method is not supported.

Stack Trace:
IQueryable.get_Provider()
Queryable.Any[TSource](IQueryable`1 source, Expression`1 predicate)
AccountService.CreateAccount(CreateAccountDto dto)
AccountServiceUnitTest.CreateAccount_ForValidData_InvokeSaveChangesAsync()

Będę wdzięczny za wskazówki jak prawidło napisać taki test.

0

Tak na szybko patrząc to brakuje ci w mocku dbkontekstu implementacji tej metody savechangesasync.

2

@No.Way:

Myślałeś o użyciu bazy (dbcontext) in-memory zamiast mocka?

0
kzkzg napisał(a):

Tak na szybko patrząc to brakuje ci w mocku dbkontekstu implementacji tej metody savechangesasync.

Hmm, czyli coś takiego...?

systemDbContextMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>())).Verifiable();

Niestety, bez zmian.

markone_dev napisał(a):

@No.Way:

Myślałeś o użyciu bazy (dbcontext) in-memory zamiast mocka?

Wtedy to już będą testy integracyjne, o ile dobrze rozumiem? Generalnie następnym krokiem będą właśnie takie testy, ale najpierw muszę jedno ogarnąć :)

3
No.Way napisał(a):
markone_dev napisał(a):

@No.Way:

Myślałeś o użyciu bazy (dbcontext) in-memory zamiast mocka?

Wtedy to już będą testy integracyjne, o ile dobrze rozumiem? :)

Gadasz jakieś głupoty.

Nie przejmuj się takimi nazwami jak "testy jednostkowe" albo "testy integracyjne". Użyj do swoich testów bazy in-memory tak jak radzi Ci @markone_dev

No.Way napisał(a):

Generalnie następnym krokiem będą właśnie takie testy, ale najpierw muszę jedno ogarnąć :)

No to moja rada jest taka żebyś od razu zrobił testy z bazą in-memory; a te testy z mockowaną bazą wyrzuć do kosza i nie rób ich w ogóle.

Aha, no i ten kod:

var mapperMock = new Mock<IMapper>();
var passwordHasherMock = new Mock<IPasswordHasher<User>>();
var jwtMock = new Mock<IOptionsMonitor<JwtConfig>>();

też wyrzuć, i zamiast mocków wstaw prawdziwe instancje tych klas.

No.Way napisał(a):
	//assert
	systemDbContextMock.Verify(m => m.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once());
}

No i testy które robią .Verify() też są ogólnie bardzo słabe.

Swoją drogą w ogóle testowanie klasy to też słaby pomysł, powinieneś testować zachowanie.

Podsumowanie, jak zrobić dobre testy

  1. Nie używaj .Verify()
  2. Nie używaj mocków klas Mapper, PasswordHasher
  3. Użyj bazy in-memory
3

Wtedy to już będą testy integracyjne, o ile dobrze rozumiem? Generalnie następnym krokiem będą właśnie takie testy, ale najpierw muszę jedno ogarnąć :)

Nie chcę się kłócić o semantykę, ale ten twój test i tak niczego sensownego nie testuje. Zrozumiałbym jeszcze sens takiego testu jak twój gdybyś miał tam jakąś logikę biznesową którą chcesz przetestować w izolacji od infrastruktury, ale to co robisz to sprawdzasz czy metoda SaveChangesAsync została wywołana. Po co taki test jednostkowy? Co on testuje? Czy dodałeś obiekt do wirtualnej kolekcji? Taki test jednostkowy nie ma najmniejszego sensu moim zdaniem, zwiększa tylko złożoność całej aplikacji bo dodaje boiler code który trzeba poprawiać przy każdej najmniejszej zmianie.

Dużo lepszym rozwiązaniem byłoby zamienić ten test na integracyjny, gdzie masz pod spodem jakąś namiastkę prawdziwej bazy danych i możesz sprawdzić czy rekord się rzeczywiście zapisze w bazie czy nie. Wtedy masz pewność czy twoja konfiguracja mapowania obiektów na tabele w bazie danych rzeczywiście działa, czyli czy działają constrainty na bazie, transakcje, pola wymagane/nie wymagane i tak dalej.

0

Ok, dzięki za odpowiedzi. W takim razie idę w kierunku testów z bazą in-memory.

0
No.Way napisał(a):

Ok, dzięki za odpowiedzi. W takim razie idę w kierunku testów z bazą in-memory.

Wywal też niepotrzebne mocki i .Verify().

2

In memory lepsze niż mocki ale to wciąż nie to samo co prawdziwa baza. Lepiej gdzieś w testach trzymać connection string do bazy testowej i testować w ten sposób. Nie wszystko co działa in memory zadziała na prawdziwej bazie.

0
szydlak napisał(a):

In memory lepsze niż mocki ale to wciąż nie to samo co prawdziwa baza. Lepiej gdzieś w testach trzymać connection string do bazy testowej i testować w ten sposób. Nie wszystko co działa in memory zadziała na prawdziwej bazie.

Tak i nie.

Jeśli coś nie działa na bazie in-memory, to może to być albo bug, i powinno być naprawione.

A nawet zakładając że tak jest, to bazie in-memory i tak jest bliżej do prawdziwej bazy niż mockom, więc w pytanie "mocki vs in-memory", baza in-memory jest oczywistym kandydatem.

0
Riddle napisał(a):
szydlak napisał(a):

In memory lepsze niż mocki ale to wciąż nie to samo co prawdziwa baza. Lepiej gdzieś w testach trzymać connection string do bazy testowej i testować w ten sposób. Nie wszystko co działa in memory zadziała na prawdziwej bazie.

Tak i nie.

Jeśli coś nie działa na bazie in-memory, to może to być albo bug, i powinno być naprawione.

Raczej miałem na myśli odwrotną sytuacje. In memory działa a na prawdziwej nie działa.

3
szydlak napisał(a):

In memory lepsze niż mocki ale to wciąż nie to samo co prawdziwa baza. Lepiej gdzieś w testach trzymać connection string do bazy testowej i testować w ten sposób. Nie wszystko co działa in memory zadziała na prawdziwej bazie.

Zależy o jakim typie bazy in-memory w EF Core mówimy. Sa 2. Jest in-memory provider, który jest tak naprawdę takim mockiem z własnym setupem klik1. Oraz masz In-memory SQLite, gdzie za każdym razem gdy uruchamiasz test to jest tworzona fizyczna baza SQLite i test wykonuje się na tej bazie klik2.

A jak potrzebujesz dokładnie takiej bazy jak na produkcji to masz test containers klik3

0
szydlak napisał(a):

In memory lepsze niż mocki ale to wciąż nie to samo co prawdziwa baza. Lepiej gdzieś w testach trzymać connection string do bazy testowej i testować w ten sposób. Nie wszystko co działa in memory zadziała na prawdziwej bazie.

mozna tez zrobic transakcje, tj. przed kazdym testtem zaczac transakcje, po tescie rollback
mysle tu o interfejsie ITestAction

0

A może prawdziwa bazka, ale inna instancja?

I może zamiast testów "jednostkowych" to rób te POSTy i GETy jak prawdziwi "userzy" tej webappki

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