Menu / szukaj

Mockowanie typów DbContext oraz DbSet z wykorzystaniem Moq

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:

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);
}

Komentarze

avatar
Ram Jane
Odpowiedz

Is there any example to test Update DbSet like Add?

avatar
Michał Jankowski
Odpowiedz

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.

Dodaj komentarz

imię*

e-mail* (nie publikowany)

strona www