Freigeben über


Testen mit einem Pseudoframework

Hinweis

Nur EF6 und höher: Die Features, APIs usw., die auf dieser Seite erläutert werden, wurden in Entity Framework 6 eingeführt. Wenn Sie eine frühere Version verwenden, gelten manche Informationen nicht.

Beim Schreiben von Tests für Ihre Anwendung ist es häufig wünschenswert, Zugriffe auf die Datenbank zu vermeiden. Entity Framework ermöglicht Ihnen dies, indem Sie einen Kontext erstellen, dessen Verhalten von Ihren Tests definiert wird und der Daten im Arbeitsspeicher nutzt.

Optionen zum Erstellen von Testdoubles

Es gibt zwei unterschiedliche Ansätze, mit denen Sie eine Version Ihres Kontexts im Arbeitsspeicher erstellen können.

  • Erstellen eigener Testdoubles: Dieser Ansatz umfasst das Schreiben Ihrer eigenen Implementierung Ihres Kontexts und DbSets im Arbeitsspeicher. Damit haben Sie sehr viel Kontrolle darüber, wie sich die Klassen verhalten, müssen aber relativ viel Code schreiben und verwalten.
  • Verwenden eines Pseudoframeworks zum Erstellen von Testdoubles: Mit einem Pseudoframework (z. B. Moq) können Sie die Implementierungen Ihres Kontexts und Ihrer Datensätze im Arbeitsspeicher dynamisch zur Laufzeit erstellen lassen.

In diesem Artikel wird die Verwendung eines Pseudoframeworks behandelt. Informationen zum Erstellen eigener Testdoubles finden Sie unter Testen mit eigenen Testdoubles.

Um die Verwendung von EF mit einem Pseudoframework zu demonstrieren, verwenden Sie Moq. Die einfachste Möglichkeit zum Einrichten von Moq besteht darin, das Moq-Paket von NuGet zu installieren.

Testen mit Versionen vor EF6

Das in diesem Artikel gezeigte Szenario hängt von einigen Änderungen ab, die in EF6 an DbSet vorgenommen wurden. Informationen zum Testen mit EF5 und früheren Versionen finden Sie unter Testen mit einem Fakekontext.

Einschränkungen bei EF-Testdoubles im Arbeitsspeicher

Testdoubles im Arbeitsspeicher stellen eine gute Möglichkeit dar, für Teile Ihrer Anwendung, die EF nutzen, Komponententests durchzuführen. Dabei verwenden Sie jedoch LINQ to Objects, um Abfragen für Daten im Arbeitsspeicher auszuführen. Dies kann zu einem anderen Verhalten führen als beim LINQ-Anbieter von EF (LINQ to Entities), wenn die Abfragen in SQL übersetzt werden, um sie für Ihre Datenbank auszuführen.

Ein Beispiel für einen solchen Unterschied ist das Laden verwandter Daten. Wenn Sie mehrere Blogs erstellen, die jeweils über verwandte Beiträge verfügen, werden bei Verwendung von Daten im Arbeitsspeicher immer die zugehörigen Beiträge für jeden Blog geladen. Bei der Ausführung mit einer Datenbank werden die Daten jedoch nur geladen, wenn Sie die Include-Methode verwenden.

Aus diesem Grund wird empfohlen, immer eine End-to-End-Überprüfung (zusätzlich zu Ihren Komponententests) einzubeziehen, um sicherzustellen, dass Ihre Anwendung ordnungsgemäß mit einer Datenbank funktioniert.

Verwenden dieses Artikels

Dieser Artikel enthält vollständige Codelisten, die Sie in Visual Studio kopieren können. Am einfachsten erstellen Sie ein Komponententestprojekt. Sie müssen .NET Framework 4.5 als Ziel angeben, um die Abschnitte mit asynchronem Code abzuschließen.

Das EF-Modell

Der zu testende Dienst verwendet ein EF-Modell mit den Klassen „BloggingContext“, „Blog“ und „Post“. Dieser Code kann mit EF Designer oder als Code First-Modell generiert werden.

using System.Collections.Generic;
using System.Data.Entity;

namespace TestingDemo
{
    public class BloggingContext : DbContext
    {
        public virtual DbSet<Blog> Blogs { get; set; }
        public virtual DbSet<Post> Posts { get; set; }
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }

        public virtual List<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public virtual Blog Blog { get; set; }
    }
}

Virtuelle DbSet-Eigenschaften mit EF Designer

Beachten Sie, dass die DbSet-Eigenschaften im Kontext als virtuell gekennzeichnet sind. Dadurch kann das Pseudoframework von Ihrem Kontext ableiten und diese Eigenschaften mit einer simulierten Implementierung überschreiben.

Wenn Sie Code First anwenden, können Sie Ihre Klassen direkt bearbeiten. Wenn Sie den EF Designer verwenden, müssen Sie die T4-Vorlage bearbeiten, die Ihren Kontext generiert. Öffnen Sie die Datei „<Modellname>.Context.tt“, die in der EDMX-Datei geschachtelt ist, suchen Sie das folgende Codefragment, und fügen Sie das Schlüsselwort „virtual“ wie dargestellt hinzu.

public string DbSet(EntitySet entitySet)
{
    return string.Format(
        CultureInfo.InvariantCulture,
        "{0} virtual DbSet\<{1}> {2} {{ get; set; }}",
        Accessibility.ForReadOnlyProperty(entitySet),
        _typeMapper.GetTypeName(entitySet.ElementType),
        _code.Escape(entitySet));
}

Der zu testende Dienst

Um Tests mit Testdoubles im Arbeitsspeicher zu demonstrieren, schreiben Sie einige Tests für einen Blogdienst (BlogService). Der Dienst kann neue Blogs erstellen (AddBlog) und alle Blogs nach Namen sortiert zurückgeben (GetAllBlogs). Neben GetAllBlogs ist eine weitere Methode verfügbar, mit der ebenfalls alle Blogs nach Namen sortiert abgerufen werden, aber asynchron (GetAllBlogsAsync).

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;

namespace TestingDemo
{
    public class BlogService
    {
        private BloggingContext _context;

        public BlogService(BloggingContext context)
        {
            _context = context;
        }

        public Blog AddBlog(string name, string url)
        {
            var blog = _context.Blogs.Add(new Blog { Name = name, Url = url });
            _context.SaveChanges();

            return blog;
        }

        public List<Blog> GetAllBlogs()
        {
            var query = from b in _context.Blogs
                        orderby b.Name
                        select b;

            return query.ToList();
        }

        public async Task<List<Blog>> GetAllBlogsAsync()
        {
            var query = from b in _context.Blogs
                        orderby b.Name
                        select b;

            return await query.ToListAsync();
        }
    }
}

Testen von Szenarien ohne Abfrage

Das ist alles, was Sie erledigen müssen, um mit dem Testen von Methoden ohne Abfragen zu beginnen. Der folgende Test verwendet Moq, um einen Kontext zu erstellen. Anschließend wird ein DbSet<Blog> erstellt und als Rückgabe von der Blogs-Eigenschaft des Kontexts festgelegt. Als Nächstes wird der Kontext verwendet, um eine neue BlogService-Instanz zu erstellen, die dann zum Erstellen eines neuen Blogs mithilfe der AddBlog-Methode verwendet wird. Schließlich überprüft der Test, ob der Dienst einen neuen Blog hinzugefügt und SaveChanges im Kontext aufgerufen hat.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Data.Entity;

namespace TestingDemo
{
    [TestClass]
    public class NonQueryTests
    {
        [TestMethod]
        public void CreateBlog_saves_a_blog_via_context()
        {
            var mockSet = new Mock<DbSet<Blog>>();

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

            var service = new BlogService(mockContext.Object);
            service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");

            mockSet.Verify(m => m.Add(It.IsAny<Blog>()), Times.Once());
            mockContext.Verify(m => m.SaveChanges(), Times.Once());
        }
    }
}

Testen von Abfrageszenarien

Um Abfragen für Ihr DbSet-Testdouble ausführen zu können, müssen Sie eine Implementierung von IQueryable einrichten. Der erste Schritt besteht darin, einige Daten im Arbeitsspeicher zu erstellen. Dazu verwenden Sie einen List<Blog>. Als Nächstes erstellen Sie einen Kontext und DBSet<Blog> und verbinden die IQueryable-Implementierung für das DbSet, indem Sie einfach an den LINQ to Objects-Anbieter delegieren, der List<T> verwendet.

Anschließend können Sie einen BlogService basierend auf den Testdoubles erstellen und sicherstellen, dass die von GetAllBlogs zurückgegebenen Daten nach Namen sortiert werden.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace TestingDemo
{
    [TestClass]
    public class QueryTests
    {
        [TestMethod]
        public void GetAllBlogs_orders_by_name()
        {
            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);

            var service = new BlogService(mockContext.Object);
            var blogs = service.GetAllBlogs();

            Assert.AreEqual(3, blogs.Count);
            Assert.AreEqual("AAA", blogs[0].Name);
            Assert.AreEqual("BBB", blogs[1].Name);
            Assert.AreEqual("ZZZ", blogs[2].Name);
        }
    }
}

Testen mit asynchronen Abfragen

In Entity Framework 6 wurden mehrere Erweiterungsmethoden eingeführt, die zum asynchronen Ausführen einer Abfrage verwendet werden können. Beispiele für diese Methoden sind ToListAsync, FirstAsync, ForEachAsync usw.

Da für Entity Framework-Abfragen LINQ verwendet wird, werden die Erweiterungsmethoden in IQueryable und IEnumerable definiert. Da sie jedoch nur für die Verwendung mit Entity Framework konzipiert sind, erhalten Sie möglicherweise den folgenden Fehler, wenn Sie versuchen, sie für eine LINQ-Abfrage zu verwenden, die keine Entity Framework-Abfrage ist:

Die IQueryable-Quelle implementiert IDbAsyncEnumerable{0} nicht. Nur Quellen, die IDbAsyncEnumerable implementieren, können für asynchrone Entity Framework-Vorgänge verwendet werden. Weitere Informationen finden Sie unter http://go.microsoft.com/fwlink/?LinkId=287068.

Auch wenn die asynchronen Methoden nur bei der Ausführung mit einer EF-Abfrage unterstützt werden, können Sie sie bei Ihrem Komponententest beim Ausführen mit einem DbSet-Testdouble im Arbeitsspeicher verwenden.

Um die asynchronen Methoden zu verwenden, müssen Sie einen DbAsyncQueryProvider im Arbeitsspeicher erstellen, um die asynchrone Abfrage zu verarbeiten. Es wäre zwar möglich, einen Abfrageanbieter mit Moq einzurichten, aber es ist viel einfacher, eine Testdoubleimplementierung im Code zu erstellen. Der Code für diese Implementierung lautet wie folgt:

using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace TestingDemo
{
    internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
    {
        private readonly IQueryProvider _inner;

        internal TestDbAsyncQueryProvider(IQueryProvider inner)
        {
            _inner = inner;
        }

        public IQueryable CreateQuery(Expression expression)
        {
            return new TestDbAsyncEnumerable<TEntity>(expression);
        }

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return new TestDbAsyncEnumerable<TElement>(expression);
        }

        public object Execute(Expression expression)
        {
            return _inner.Execute(expression);
        }

        public TResult Execute<TResult>(Expression expression)
        {
            return _inner.Execute<TResult>(expression);
        }

        public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
        {
            return Task.FromResult(Execute(expression));
        }

        public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
        {
            return Task.FromResult(Execute<TResult>(expression));
        }
    }

    internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
    {
        public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
            : base(enumerable)
        { }

        public TestDbAsyncEnumerable(Expression expression)
            : base(expression)
        { }

        public IDbAsyncEnumerator<T> GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
        }

        IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
        {
            return GetAsyncEnumerator();
        }

        IQueryProvider IQueryable.Provider
        {
            get { return new TestDbAsyncQueryProvider<T>(this); }
        }
    }

    internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
    {
        private readonly IEnumerator<T> _inner;

        public TestDbAsyncEnumerator(IEnumerator<T> inner)
        {
            _inner = inner;
        }

        public void Dispose()
        {
            _inner.Dispose();
        }

        public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(_inner.MoveNext());
        }

        public T Current
        {
            get { return _inner.Current; }
        }

        object IDbAsyncEnumerator.Current
        {
            get { return Current; }
        }
    }
}

Da Sie nun über einen asynchronen Abfrageanbieter verfügen, können Sie einen Komponententest für die neue GetAllBlogsAsync-Methode schreiben.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Threading.Tasks;

namespace TestingDemo
{
    [TestClass]
    public class AsyncQueryTests
    {
        [TestMethod]
        public async Task GetAllBlogsAsync_orders_by_name()
        {

            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<IDbAsyncEnumerable<Blog>>()
                .Setup(m => m.GetAsyncEnumerator())
                .Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator()));

            mockSet.As<IQueryable<Blog>>()
                .Setup(m => m.Provider)
                .Returns(new TestDbAsyncQueryProvider<Blog>(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);

            var service = new BlogService(mockContext.Object);
            var blogs = await service.GetAllBlogsAsync();

            Assert.AreEqual(3, blogs.Count);
            Assert.AreEqual("AAA", blogs[0].Name);
            Assert.AreEqual("BBB", blogs[1].Name);
            Assert.AreEqual("ZZZ", blogs[2].Name);
        }
    }
}