Testen mit eigenen Testdoubles

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 das Erstellen eigener Testdoubles behandelt. Informationen zur Verwendung eines Pseudoframeworks finden Sie unter Testen mit einem Pseudoframework.

Testen mit Versionen vor EF6

Der in diesem Artikel gezeigte Code ist mit EF6 kompatibel. 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.

Erstellen einer Kontextschnittstelle

Sie werden sich mit dem Testen eines Diensts befassen, der ein EF-Modell verwendet. Um den EF-Kontext für die Tests durch eine Version im Arbeitsspeicher zu ersetzen, definieren Sie eine Schnittstelle, die Ihr EF-Kontext (und sein Double im Arbeitsspeicher) implementiert.

Der zu testende Dienst fragt Daten mithilfe der DbSet-Eigenschaften Ihres Kontexts ab und ändert sie. Außerdem wird SaveChanges aufgerufen, um Änderungen in die Datenbank zu pushen. Aus diesem Grund werden auch diese Member in die Schnittstelle eingeschlossen.

using System.Data.Entity;

namespace TestingDemo
{
    public interface IBloggingContext
    {
        DbSet<Blog> Blogs { get; }
        DbSet<Post> Posts { get; }
        int SaveChanges();
    }
}

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, IBloggingContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public 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; }
    }
}

Implementieren der Kontextschnittstelle mit dem EF Designer

Beachten Sie, dass der Kontext die IBloggingContext-Schnittstelle implementiert.

Wenn Sie Code First anwenden, können Sie Ihren Kontext direkt bearbeiten, um die Schnittstelle zu implementieren. 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 die Schnittstelle wie dargestellt hinzu.

<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext, IBloggingContext

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 IBloggingContext _context;

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

        public Blog AddBlog(string name, string url)
        {
            var blog = new Blog { Name = name, Url = url };
            _context.Blogs.Add(blog);
            _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();
        }
    }
}

Erstellen der Testdoubles im Arbeitsspeicher

Da Sie nun über das eigentliche EF-Modell und den Dienst verfügen, der es auch verwenden kann, können Sie nun ein Testdouble im Arbeitsspeicher erstellen, das Sie für Tests verwenden können. Für diesen Kontext wurde ein TestContext-Testdouble erstellt. Bei Testdoubles können Sie das gewünschte Verhalten auswählen, um es an die Tests anzupassen, die Sie ausführen werden. In diesem Beispiel erfassen Sie nur die Anzahl der Aufrufe von SaveChanges. Sie können aber beliebige Logik einbeziehen, um die Tests an Ihr Szenario anzupassen.

Außerdem wurde ein TestDbSet erstellt, das eine -Implementierung von DbSet im Arbeitsspeicher bereitstellt. Es wurde eine vollständige Implementierung für alle Methoden in DbSet (mit Ausnahme von Find) bereitgestellt, Sie müssen aber nur die Member implementieren, die in Ihrem Testszenario verwendet werden.

TestDbSet verwendet einige andere Infrastrukturklassen, die eingeschlossen wurden, um sicherzustellen, dass asynchrone Abfragen verarbeitet werden können.

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

namespace TestingDemo
{
    public class TestContext : IBloggingContext
    {
        public TestContext()
        {
            this.Blogs = new TestDbSet<Blog>();
            this.Posts = new TestDbSet<Post>();
        }

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
        public int SaveChangesCount { get; private set; }
        public int SaveChanges()
        {
            this.SaveChangesCount++;
            return 1;
        }
    }

    public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
        where TEntity : class
    {
        ObservableCollection<TEntity> _data;
        IQueryable _query;

        public TestDbSet()
        {
            _data = new ObservableCollection<TEntity>();
            _query = _data.AsQueryable();
        }

        public override TEntity Add(TEntity item)
        {
            _data.Add(item);
            return item;
        }

        public override TEntity Remove(TEntity item)
        {
            _data.Remove(item);
            return item;
        }

        public override TEntity Attach(TEntity item)
        {
            _data.Add(item);
            return item;
        }

        public override TEntity Create()
        {
            return Activator.CreateInstance<TEntity>();
        }

        public override TDerivedEntity Create<TDerivedEntity>()
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }

        public override ObservableCollection<TEntity> Local
        {
            get { return _data; }
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
        }
    }

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

Implementieren von Find

Eine generische Implementierung der Find-Methode ist schwierig. Wenn Sie Code testen müssen, der die Find-Methode verwendet, ist es am einfachsten, einen DbSet-Test für jeden Entitätstyp zu erstellen, der die Suche unterstützen soll. Anschließend können Sie Logik schreiben, um diesen spezifischen Entitätstyp zu finden, wie unten dargestellt.

using System.Linq;

namespace TestingDemo
{
    class TestBlogDbSet : TestDbSet<Blog>
    {
        public override Blog Find(params object[] keyValues)
        {
            var id = (int)keyValues.Single();
            return this.SingleOrDefault(b => b.BlogId == id);
        }
    }
}

Schreiben einiger Tests

Das ist alles, was Sie erledigen müssen, um mit dem Testen zu beginnen. Der folgende Test erstellt einen TestContext und dann basierend auf diesem Kontext einen Dienst. Der Dienst wird dann zum Erstellen eines neuen Blogs mithilfe der AddBlog-Methode verwendet. Abschließend überprüft der Test, ob der Dienst der Blogs-Eigenschaft des Kontexts einen neuen Blog hinzugefügt und SaveChanges im Kontext aufgerufen hat.

Dies ist nur ein Beispiel für die vielen Möglichkeiten, die Testdoubles im Arbeitsspeicher für Tests bieten. Sie können die Logik der Testdoubles und die Überprüfung beliebig an Ihre Anforderungen anpassen.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace TestingDemo
{
    [TestClass]
    public class NonQueryTests
    {
        [TestMethod]
        public void CreateBlog_saves_a_blog_via_context()
        {
            var context = new TestContext();

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

            Assert.AreEqual(1, context.Blogs.Count());
            Assert.AreEqual("ADO.NET Blog", context.Blogs.Single().Name);
            Assert.AreEqual("http://blogs.msdn.com/adonet", context.Blogs.Single().Url);
            Assert.AreEqual(1, context.SaveChangesCount);
        }
    }
}

Hier sehen Sie ein weiteres Beispiel für einen Test, der dieses Mal eine Abfrage ausführt. Der Test beginnt mit dem Erstellen eines Testkontexts mit einigen Daten in seiner Blog-Eigenschaft. Beachten Sie, dass die Daten nicht alphabetisch sortiert sind. Anschließend können Sie einen BlogService basierend auf dem Testkontext erstellen und sicherstellen, dass die von GetAllBlogs zurückgegebenen Daten nach Namen sortiert werden.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestingDemo
{
    [TestClass]
    public class QueryTests
    {
        [TestMethod]
        public void GetAllBlogs_orders_by_name()
        {
            var context = new TestContext();
            context.Blogs.Add(new Blog { Name = "BBB" });
            context.Blogs.Add(new Blog { Name = "ZZZ" });
            context.Blogs.Add(new Blog { Name = "AAA" });

            var service = new BlogService(context);
            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);
        }
    }
}

Abschließend schreiben Sie einen weiteren Test, der die asynchrone Methode verwendet, um sicherzustellen, dass die asynchrone Infrastruktur in TestDbSet funktioniert.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TestingDemo
{
    [TestClass]
    public class AsyncQueryTests
    {
        [TestMethod]
        public async Task GetAllBlogsAsync_orders_by_name()
        {
            var context = new TestContext();
            context.Blogs.Add(new Blog { Name = "BBB" });
            context.Blogs.Add(new Blog { Name = "ZZZ" });
            context.Blogs.Add(new Blog { Name = "AAA" });

            var service = new BlogService(context);
            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);
        }
    }
}