Pisząc testy jednostkowe w aplikacjach, które przechowują dane w bazie danych prędzej, czy później będziemy zmuszeni do odizolowania warstwy dostępu do bazy danych. W opisywanym przypadku jako ORM wykorzystywany jest Entity Framework.
Kod definiujący podstawowe elementy wygląda w następujący sposób:
public class User { public int Id { get; set; } public string Login { get; set; } public string Name { get; set; } public string Surname { get; set; } public bool AccountLocked { get; set; } public virtual List<Role> Roles { get; set; } } public class UsersContext : DbContext { public virtual DbSet<User> Users { get; set; } public virtual DbSet<Role> Roles { get; set; } } public class UsersService { private readonly UsersContext usersContext; public UsersService(UsersContext usersContext) { this.usersContext = usersContext; } public User AddUser(string login, string name, string surname) { var newUser = this.usersContext.Users.Add( new User { Login = login, Name = name, Surname = surname, AccountLocked = false }); this.usersContext.SaveChanges(); return newUser; } public IList<User> GetLockedUsers () { return this.usersContext.Users.Where(x => x.AccountLocked).ToList(); } }
Chcąc odizolować warstwę danych od aplikacji można wykorzystać jedną z dwóch opcji:
- napisać dodatkową implementację klasy UsersContext, która na potrzeby testów jednostkowych będzie symulowała działanie bazy danych. Obiekt nowej klasy zostanie wstrzyknięty do obiektów, które do działania potrzebują klasę UsersContext.
- wykorzystać jeden z dostępnych frameworków mockujących – w tym przypadku Moq.
Oba rozwiązania mają pewne wady i zalety – pierwsze z nich pozwala na stworzenie środowiska bardzo dopasowanego do naszych potrzeb. Z drugiej strony będziemy musieli napisać dodatkowy kod, który w przyszłości będziemy musieli utrzymywać. Wykorzystując framework mockujący otrzymamy rozwiązanie, które jest mniej dostosowane do naszych wymagań, ale z drugiej strony nie będziemy ponosili kosztów stworzenia dodatkowej implementacji klasy UsersContext oraz późniejszego jej utrzymania. Co więcej, wykorzystanie mocków pozwala na łatwą zmianę zachowania obiektów, które mockujemy.
W opisywanym przykładzie zostaną wykorzystane następujące technologie:
- xUnit,
- Moq,
- AutoFixture.
Oczywiście testować będziemy metody z klasy UsersService.
W przypadku tego typów testów należy rozważyć dwa przypadki, zależne od tego czy metoda, którą testujemy wykorzystuje LINQ, w celu pobrania danych – metoda GetLockedUsers, czy też nie – AddUser.
Brak wykorzystania LINQ w metodzie testowanej
Przypadek drugi – bez wykorzystania LINQ – jest prostszy, choć wywołuje u mnie więcej wątpliwości odnośnie sposobu pisania testów jednostkowych. Do testowania metody AddUser można zastosować dwa podejścia:
- Zaizolować wywołanie this.usersContext.Users.Add, w taki sposób, aby stworzyło poprawny obiekt klasy User, a następne sprawdzić czy wynikiem metody AddUser będzie użytkownik, którego pola mają odpowiednie wartości:
[Theory, AutoData] public void AddUser_Invoke_UserCreated_v1(string expectedLogin, string expectedName, string expectedSurname) { // Arrange var userContextMock = new Mock<UsersContext>(); userContextMock.Setup(x => x.Users.Add(It.IsAny<User>())).Returns((User u) => u); var usersService = new UsersService(userContextMock.Object); // Act var user = usersService.AddUser(expectedLogin, expectedName, expectedSurname); // Assert Assert.Equal(expectedLogin, user.Login); Assert.Equal(expectedName, user.Name); Assert.Equal(expectedSurname, user.Surname); Assert.False(user.AccountLocked); userContextMock.Verify(x => x.SaveChanges(), Times.Once); }
- Częściowo zignorować wynik metody AddUser (sprawdzić tylko czy otrzymaliśmy jakiś obiekt) i sprawdzić czy poprawnie wywołaliśmy metodę dodającą obiekt do kontekstu:
[Theory, AutoData] public void AddUser_Invoke_UserCreated_v2(string expectedLogin, string expectedName, string expectedSurname) { // Arrange var userContextMock = new Mock<UsersContext>(); var usersMock = new Mock<DbSet<User>>(); usersMock.Setup(x => x.Add(It.IsAny<User>())).Returns((User u) => u); userContextMock.Setup(x => x.Users).Returns(usersMock.Object); var usersService = new UsersService(userContextMock.Object); // Act var user = usersService.AddUser(expectedLogin, expectedName, expectedSurname); // Assert Assert.NotNull(user); usersMock.Verify( x => x.Add( It.Is<User>( u => u.Login == expectedLogin && u.Name == expectedName && u.Surname == expectedSurname && !u.AccountLocked)), Times.Once); userContextMock.Verify(x => x.SaveChanges(), Times.Once); }
W obu przypadkach weryfikujemy, czy nastąpiło wywołanie metody SaveChanges.
Wracając do moich wątpliwości – pojawią się one w każdym przypadku, gdy w testach jednostkowych następuje sprawdzenie czy została wykonana jakaś metoda (dodatkowo można również sprawdzić, czy odpowiednie parametry zostały przekazane do tej metody). Wątpliwości dotyczą tego, czy w takich przypadkach sprawdzamy, poprawność działania metody, czy też jej implementację.
Czasami w testach sprawdzamy tylko czy metoda, którą testujemy wywołała inne metody. Przy tego typu testach należy pamiętać, że nie wykryją one błędu wynikającego z faktu zmienienia kolejności wywołania sprawdzanych metod. Z tego powodu bliżej mi do pierwszego zaprezentowanego rozwiązania, chociaż także w nim sprawdzamy, czy metoda SaveChanges została wykonana dokładnie raz.
LINQ w metodzie testowanej
Takie testy to ja lubię – czyste testy jednostkowe. Aby można było wykonać zapytanie LINQ na obiekcie klasy DbSet należy stworzyć listę obiektów (w tym przypadku użytkowników), a następnie powiązać ją z implementacją interfejsu IQueryable<out T> w klasie DbSet. Kod za to odpowiedzialny wygląda w następujący sposób:
var users = new List<User> { lockedUser, fixture.Build<User>().With(u => u.AccountLocked, false).Create(), fixture.Build<User>().With(u => u.AccountLocked, false).Create() }.AsQueryable(); var usersMock = new Mock<DbSet<User>>(); usersMock.As<IQueryable<User>>().Setup(m => m.Provider).Returns(users.Provider); usersMock.As<IQueryable<User>>().Setup(m => m.Expression).Returns(users.Expression); usersMock.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(users.ElementType); usersMock.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(users.GetEnumerator());
W przykładzie można zauważyć w jaki sposób można wykorzystać AutoFixture do tworzenia obiektów.
Cały test wygląda następująco:
[Fact] public void GetLockedUsers_Invoke_LockedUsers_v1() { // Arrange var fixture = new Fixture(); var lockedUser = fixture.Build<User>().With(u => u.AccountLocked, true).Create(); var users = new List<User> { lockedUser, fixture.Build<User>().With(u => u.AccountLocked, false).Create(), fixture.Build<User>().With(u => u.AccountLocked, false).Create() }.AsQueryable(); var usersMock = new Mock<DbSet<User>>(); usersMock.As<IQueryable<User>>().Setup(m => m.Provider).Returns(users.Provider); usersMock.As<IQueryable<User>>().Setup(m => m.Expression).Returns(users.Expression); usersMock.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(users.ElementType); usersMock.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(users.GetEnumerator()); var userContextMock = new Mock<UsersContext>(); userContextMock.Setup(x => x.Users).Returns(usersMock.Object); var usersService = new UsersService(userContextMock.Object); // Act var lockedUsers = usersService.GetLockedUsers (); // Assert Assert.Equal(new List<User> {lockedUser}, lockedUsers); }
Ponieważ mockowanie obiektów DbSet w każdym przypadku będzie wyglądało analogicznie proponuję wydzielić kod za to odpowiedzialny do osobnej metody / klasy:
private static Mock<DbSet<T>> CreateDbSetMock<T>(IEnumerable<T> elements) where T : class { var elementsAsQueryable = elements.AsQueryable(); var dbSetMock = new Mock<DbSet<T>>(); dbSetMock.As<IQueryable<T>>().Setup(m => m.Provider).Returns(elementsAsQueryable.Provider); dbSetMock.As<IQueryable<T>>().Setup(m => m.Expression).Returns(elementsAsQueryable.Expression); dbSetMock.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(elementsAsQueryable.ElementType); dbSetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(elementsAsQueryable.GetEnumerator()); return dbSetMock; }
Po takim zabiegu nasz test jednostkowy trochę się uprości oraz zwiększy się jego czytelność:
[Fact] public void GetLockedUsers_Invoke_LockedUsers_v2() { // Arrange var fixture = new Fixture(); var lockedUser = fixture.Build<User>().With(u => u.AccountLocked, true).Create(); IList<User> users = new List<User> { lockedUser, fixture.Build<User>().With(u => u.AccountLocked, false).Create(), fixture.Build<User>().With(u => u.AccountLocked, false).Create() }; var usersMock = CreateDbSetMock(users); var userContextMock = new Mock<UsersContext>(); userContextMock.Setup(x => x.Users).Returns(usersMock.Object); var usersService = new UsersService(userContextMock.Object); // Act var lockedUsers = usersService.GetLockedUsers (); // Assert Assert.Equal(new List<User> {lockedUser}, lockedUsers); }
DbSet mock, no results while calling usersMock.Object.ToList() secondly
Problem:
dbSetMock.As<IQueryable>().Setup(m => m.GetEnumerator()).Returns(elementsAsQueryable.GetEnumerator());
Solution:
dbSetMock.As<IQueryable>().Setup(m => m.GetEnumerator()).Returns(() => elementsAsQueryable.GetEnumerator());
-- Return a function that returns the enumerator, that will return a new enumerator each time you ask for it.
need to write a xunit test for „dbContext.Users.Where”, I tried the above you mentioned but i getting the below exception for dbcontext.setup ()
System.NotSupportedException : Unsupported expression: x => x.Users
Non-overridable members (here: dbContext.get_Users) may not be used in setup / verification expressions.
Could you please help me this?
Probably Users are not marked as virtual.
var userContextMock = new Mock();
userContextMock.Setup(x => x.Users.Add(It.IsAny())).Returns((User u) => u);
throws:
Cannot implicitly convert type 'app.Models.User’ to 'Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry’
Any idea how to fix it?
It depends on what you want to do.
One of the solutions can be as follow:
();())).Callback(
// Arrange
var userContextMock = new Mock
User user = null;
userContextMock.Setup(x => x.Users.Add(It.IsAny
(User u) =>
{
user = u;
});
var usersService = new UsersService(userContextMock.Object);
// Act
usersService.AddUser(expectedLogin, expectedName, expectedSurname);
// Assert
Assert.Equal(expectedLogin, user.Login);
Assert.Equal(expectedName, user.Name);
Assert.Equal(expectedSurname, user.Surname);
Assert.False(user.AccountLocked);
userContextMock.Verify(x => x.SaveChanges(), Times.Once);
Hi Michal,
This blog was really useful for me. Thank you very much for writing it in a simple way. I was testing the following method
public List GetAllAlarmByCustomer(CurrentUser user)
{
var alarmMessageList = _dbContext.AlarmMessages.Where(m => m.FkCustomerId == user.CustomerId);
return alarmMessageList.ToList();
}
And I developed my unit test using „Method with LINQ”. I am getting the following error ,
System.ArgumentException : The expression’s Body is not a MemberExpression. Most likely this is because it does not represent access to a property or field. (Parameter 'propertyPicker’)
And the line of code is,
var local= fixture.Build().With(u => (u.FkCustomerId == user.CustomerId), true).Create();
Could you please help me to find the problem with my code.?
Thanks in advance,
Sajina
Hi Sajina,
Mentioned line of code looks like AutoFixture and you are adding there some kind of logic expression. And you should use With to assign value to property in the generated object.
Can you please share your sample project?
You can find a sample project on GitHub repository
Good writeup!!
Beware that using LINQ multiple times on your mocked DbSet may end up yielding no results. To fix this, pass the mocked call to GetEnumerator() as a lambda, as seen here: https://stackoverflow.com/a/33528122
Will be improved in next version.
Take your attention on such importan thing like GetEnumerator which should called as Func here:
mockSet.As().Setup(m => m.GetEnumerator()).Returns( ( )=> data.GetEnumerator());
instead of your wrong implemenration
mockSet.As().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
Because Returns(data.GetEnumerator()); will reach end of collection after first loop and each next request to collection\mocked object will return empty result!
Good suggestion!
I found this tutorial very useful. I modified it slightly for my purposes, and to also use an interface to decouple my dbcontext derived class, from my equivalent of the service class. I’m not using XUnit or AutoFixture, but I am using NUnit and Moq, so the code is fairly close. Thanks for posting this!
Hi Michal,
I am facing a issue in my Unit test project, I am getting following issue mention in below link, Please help to troubleshoot the issue if you can.
https://social.msdn.microsoft.com/Forums/en-US/391a31ae-6e4d-454c-8852-e7266e982370/unit-test-project-issue?forum=adodotnetentityframework
Could you please provide an example of your problem as VS project zipped to one file. And describe what you would like to achieve.
Super wpis!
Hi Michal,
I appreciate your post, was trying it out on my project because I have a similar setup and was looking into unit test some of the user services that use the DbContext. On the method without LINQ, at least on the latest libraries, what you suggest does not compile. Specifically this line that performs an invalid cast:
usersMock.Setup(x => x.Add(It.IsAny())).Returns((User u) => u);
The mock set up using DbSet will make users be instance of Microsoft.EntityFramework.ChangeTracking.EntryEntitty, and the User is that of your project’s class „User”.
Just wanted to point that out
Hi Mark,
Could you please provide more details. I updated every dll to the latest version:
-- EF v6.2
-- Moq v4.8.1
-- xUnit v2.3.1
-- AutoFixture v4.0.1
And everything is working. Maybe you thought about EF Core?
Is there any example to test Update DbSet like Add?
Please check section „Method without LINQ”. There is example of unit test for usersContext.Users.Add. I believe this is what you would like to see.
[…] wywołania synchroniczne – Mockowanie typów DbContext oraz DbSet z wykorzystaniem Moq […]
[…] czas temu opisałem w jaki sposób można zamockować typy DbContext przy pomocy Moq -- Mockowanie typów DbContext oraz DbSet z wykorzystaniem Moq. Temat ten nie został wtedy całkowicie wyczerpany. Pozostał jeden element do opisania – […]
Akurat dzisiaj miałem ten sam problem -- jak mockować DbContext?.
Ze wszystkich możliwych rozwiązań wybrałem Effort -- in-memory database.
https://effort.codeplex.com/