web-dev-qa-db-fra.com

NSubstitue DbSet/IQueryable <T>

EntityFramework 6 est donc beaucoup mieux testable que les versions précédentes. Et il y a quelques exemples intéressants sur Internet pour des frameworks comme Moq, mais le cas est, je préfère utiliser NSubstitute. J'ai traduit les exemples de "non requête" pour qu'ils utilisent l'utilisation de NSubstitute, mais je n'arrive pas à comprendre le "test de requête".

Comment la fonction items.As<IQueryable<T>>().Setup(m => m.Provider).Returns(data.Provider); de Moq se traduit-elle en NSubstitute? J'ai pensé quelque chose comme ((IQueryable<T>) items).Provider.Returns(data.Provider); mais cela n'a pas fonctionné. J'ai aussi essayé items.AsQueryable().Provider.Returns(data.Provider); mais cela n'a pas fonctionné non plus.

L'exception que je reçois est:

"System.NotImplementedException: le membre 'IQueryable.Provider' N'a pas été implémenté sur le type 'DbSet1Proxy' which inherits from 'DbSet1'. Les doubles tests pour 'DbSet`1' doivent fournir les implémentations des méthodes et propriétés Qui sont utilisées."

Alors laissez-moi citer l'exemple de code du lien ci-dessus. Cet exemple de code utilise Moq pour simuler DbContext et DbSet.

public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  }.AsQueryable();

  var mockSet = new Mock<DbSet<Blog>>();
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

  var mockContext = new Mock<BloggingContext>();
  mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);

  // ...
}

Et voici à quel point je viens avec NSubstitute

public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  }.AsQueryable();

  var mockSet = Substitute.For<DbSet<Blog>>();
  // it's the next four lines I don't get to work
  ((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
  ((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
  ((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
  ((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());

  var mockContext = Substitute.For<BloggingContext>();
  mockContext.Blogs.Returns(mockSet);

  // ...
}

Donc la question est; Comment substituer une propriété de IQueryable (comme fournisseur)?

33
s.meijer

Cela est dû à la syntaxe spécifique de NSubstitute. Par exemple dans: 

((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);

NSubstitute appelle le getter du fournisseur, puis spécifie la valeur de retour. Cet appel getter n'est pas intercepté par le remplaçant et vous obtenez une exception. Cela est dû à une implémentation explicite de la propriété IQueryable.Provider dans la classe DbQuery. 

Vous pouvez explicitement créer des substituts pour plusieurs interfaces avec NSub et créer un proxy qui couvre toutes les interfaces spécifiées. Les appels aux interfaces seront alors interceptés par le substitut . Veuillez utiliser la syntaxe suivante:

// Create a substitute for DbSet and IQueryable types:
var mockSet = Substitute.For<DbSet<Blog>, IQueryable<Blog>>();

// And then as you do:
((IQueryable<Blog>)mockSet).Provider.Returns(data.Provider);
...
36
Alexandr Nikitin

Merci à Kevin, j'ai trouvé le problème dans la traduction de mon code.

Les exemples de code unittest se moquent de DbSet, mais NSubstitute nécessite la mise en oeuvre de l'interface. Donc, l'équivalent de Moqs new Mock<DbSet<Blog>>() pour NSubstitute est Substitute.For<IDbSet<Blog>>(). Vous n'êtes pas toujours obligé de fournir l'interface, c'est pourquoi j'ai été dérouté. Mais dans ce cas précis, cela s’est avéré crucial.

Il est également apparu que nous n’avions pas besoin de transtyper vers Queryable lors de l’utilisation de l’interface IDbSet.

Donc, le code de test de travail:

public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
    new Blog { Name = "BBB" },
    new Blog { Name = "ZZZ" },
    new Blog { Name = "AAA" },
  }.AsQueryable();

  var mockSet = Substitute.For<IDbSet<Blog>>();
  mockSet.Provider.Returns(data.Provider);
  mockSet.Expression.Returns(data.Expression);
  mockSet.ElementType.Returns(data.ElementType);
  mockSet.GetEnumerator().Returns(data.GetEnumerator());

  var mockContext = Substitute.For<BloggingContext>();
  mockContext.Blogs.Returns(mockSet);

  // Act and Assert ...
}

J'ai écrit une petite méthode d'extention pour nettoyer la section Arrangement des tests unitaires. 

public static class ExtentionMethods
{
    public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
    {
        dbSet.Provider.Returns(data.Provider);
        dbSet.Expression.Returns(data.Expression);
        dbSet.ElementType.Returns(data.ElementType);
        dbSet.GetEnumerator().Returns(data.GetEnumerator());
        return dbSet;
    }
}

// usage like:
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);

Pas la question, mais au cas où vous auriez également besoin de pouvoir prendre en charge les opérations asynchrones:

public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
  dbSet.Provider.Returns(data.Provider);
  dbSet.Expression.Returns(data.Expression);
  dbSet.ElementType.Returns(data.ElementType);
  dbSet.GetEnumerator().Returns(data.GetEnumerator());

  if (dbSet is IDbAsyncEnumerable)
  {
    ((IDbAsyncEnumerable<T>) dbSet).GetAsyncEnumerator()
      .Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));
    dbSet.Provider.Returns(new TestDbAsyncQueryProvider<T>(data.Provider));
  }

  return dbSet;
}

// create substitution with async
var mockSet = Substitute.For<IDbSet<Blog>, IDbAsyncEnumerable<Blog>>().Initialize(data);
// create substitution without async
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
17
s.meijer

Ceci est ma méthode statique générique statique pour générer de faux DbSet. Cela peut être utile.

 public static class CustomTestUtils
{
    public static DbSet<T> FakeDbSet<T>(List<T> data) where T : class
    {
        var _data = data.AsQueryable();
        var fakeDbSet = Substitute.For<DbSet<T>, IQueryable<T>>();
        ((IQueryable<T>)fakeDbSet).Provider.Returns(_data.Provider);
        ((IQueryable<T>)fakeDbSet).Expression.Returns(_data.Expression);
        ((IQueryable<T>)fakeDbSet).ElementType.Returns(_data.ElementType);
        ((IQueryable<T>)fakeDbSet).GetEnumerator().Returns(_data.GetEnumerator());

        fakeDbSet.AsNoTracking().Returns(fakeDbSet);

        return fakeDbSet;
    }

}
4
Krzysztof Bronowski

J'ai écrit une enveloppe il y a environ un an autour du même code que celui auquel vous faites référence depuis Test avec vos propres tests doubles (à partir de EF6) . Ce wrapper se trouve sur GitHub DbContextMockForUnitTests . Le but de cette enveloppe est de réduire la quantité de code répétitif/en double nécessaire pour configurer les tests unitaires qui utilisent EF afin de simuler DbContext et DbSets. La plupart du code fictif EF que vous avez dans l'OP peut être réduit à 2 lignes de code (et seulement 1 si vous utilisez DbContext.Set<T> au lieu de propriétés DbSet) et le code fictif est ensuite appelé dans le wrapper.

Pour l'utiliser, copiez et incluez les fichiers du dossier MockHelpers dans votre projet test.

Voici un exemple de test utilisant ce que vous aviez ci-dessus, notez qu'il ne reste plus que 2 lignes de code pour configurer le modèle DbSet<T> dans la variable DbContext simulée.

public void GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  };

  var mockContext = Substitute.For<BloggingContext>();

  // Create and assign the substituted DbSet
  var mockSet = data.GenerateMockDbSet();
  mockContext.Blogs.Returns(mockSet);

  // act
}

Il est tout aussi facile d’en faire un test qui appelle quelque chose qui utilise le modèle async/wait comme .ToListAsync() sur le DbSet<T>.

public async Task GetAllBlogs_orders_by_name()
{
  // Arrange
  var data = new List<Blog>
  {
     new Blog { Name = "BBB" },
     new Blog { Name = "ZZZ" },
     new Blog { Name = "AAA" },
  };

  var mockContext = Substitute.For<BloggingContext>();

  // Create and assign the substituted DbSet
  var mockSet = data.GenerateMockDbSetForAsync(); // only change is the ForAsync version of the method
  mockContext.Blogs.Returns(mockSet);

  // act
}
2
Igor

Vous ne devriez pas avoir besoin de vous moquer de toutes les pièces d'IQueryable. Lorsque j'utilise NSubstitute pour me moquer d'un fichier texte EF DbContext, je fais quelque chose comme ça:

interface IContext
{
  IDbSet<Foo> Foos { get; set; }
}

var context = Substitute.For<IContext>();

context.Foos.Returns(new MockDbSet<Foo>());

Avec une simple implémentation de IDbSet autour d'une liste ou quelque chose pour mon MockDbSet ().

En règle générale, vous devez mapper des interfaces et non des types, car NSubstitute ne fera que remplacer les méthodes virtuelles.

0
Kevin