Testen mit ihren eigenen Test DoublesTesting with your own test doubles

Hinweis

Nur EF6 und höher: Die Features, APIs usw., die auf dieser Seite erläutert werden, wurden in Entity Framework 6 eingeführt.EF6 Onwards Only - The features, APIs, etc. discussed in this page were introduced in Entity Framework 6. Wenn Sie eine frühere Version verwenden, gelten manche Informationen nicht.If you are using an earlier version, some or all of the information does not apply.

Beim Schreiben von Tests für Ihre Anwendung ist es häufig wünschenswert, das Erreichen der Datenbank zu vermeiden.When writing tests for your application it is often desirable to avoid hitting the database. Entity Framework ermöglicht es Ihnen, dies zu erreichen, indem Sie einen Kontext – mit von den Tests definiertem Verhalten – erstellen, der Daten im Arbeitsspeicher nutzt.Entity Framework allows you to achieve this by creating a context – with behavior defined by your tests – that makes use of in-memory data.

Optionen zum Erstellen von Test DoublesOptions for creating test doubles

Es gibt zwei verschiedene Ansätze, die verwendet werden können, um eine in-Memory-Version Ihres Kontexts zu erstellen.There are two different approaches that can be used to create an in-memory version of your context.

  • Erstellen Sie eigene Test Doubles – diese Vorgehensweise umfasst das Schreiben einer eigenen in-Memory-Implementierung Ihres Kontexts und von dbsets.Create your own test doubles – This approach involves writing your own in-memory implementation of your context and DbSets. Dadurch haben Sie viele Kontrolle darüber, wie sich die Klassen Verhalten, aber Sie können das Schreiben und das Besitz einer angemessenen Menge an Code einschließen.This gives you a lot of control over how the classes behave but can involve writing and owning a reasonable amount of code.
  • Verwenden eines kontextbasierten Frameworks zum Erstellen von Test Doubles – mithilfe eines kontextbasierten Frameworks (wie z. b. von muq) können Sie die in-Memory-Implementierungen Ihres Kontexts und Sätze dynamisch zur Laufzeit erstellen.Use a mocking framework to create test doubles – Using a mocking framework (such as Moq) you can have the in-memory implementations of you context and sets created dynamically at runtime for you.

In diesem Artikel wird das Erstellen eines eigenen Test Double behandelt.This article will deal with creating your own test double. Weitere Informationen zur Verwendung eines-Frameworks finden Sie unter Testen mit einem-Frameworks.For information on using a mocking framework see Testing with a Mocking Framework.

Testen mit Pre-EF6-VersionenTesting with pre-EF6 versions

Der in diesem Artikel gezeigte Code ist kompatibel mit EF6.The code shown in this article is compatible with EF6. Informationen zu Tests mit EF5 und einer früheren Version finden Sie untertests mit einem gefälschten Kontext.For testing with EF5 and earlier version see Testing with a Fake Context.

Einschränkungen von EF-in-Memory-Test DoublesLimitations of EF in-memory test doubles

In-Memory-Test Doubles können eine gute Möglichkeit zum Bereitstellen von Komponenten Testebene für Bits Ihrer Anwendung sein, die EF verwenden.In-memory test doubles can be a good way to provide unit test level coverage of bits of your application that use EF. Dabei verwenden Sie jedoch LINQ to Objects, um Abfragen für in-Memory-Daten auszuführen.However, when doing this you are using LINQ to Objects to execute queries against in-memory data. Dies kann zu einem anderen Verhalten führen als die Verwendung des LINQ-Anbieters (LINQ to Entities) von EF, um Abfragen in SQL zu übersetzen, die für die Datenbank ausgeführt werden.This can result in different behavior than using EF’s LINQ provider (LINQ to Entities) to translate queries into SQL that is run against your database.

Ein Beispiel für einen solchen Unterschied besteht darin, verknüpfte Daten zu laden.One example of such a difference is loading related data. Wenn Sie eine Reihe von Blogs erstellen, die jeweils über verwandte Beiträge verfügen, werden bei der Verwendung von in-Memory-Daten die zugehörigen Beiträge immer für jeden Blog geladen.If you create a series of Blogs that each have related Posts, then when using in-memory data the related Posts will always be loaded for each Blog. Wenn Sie jedoch für eine Datenbank ausführen, werden die Daten nur geladen, wenn Sie die Include-Methode verwenden.However, when running against a database the data will only be loaded if you use the Include method.

Aus diesem Grund empfiehlt es sich, immer einen gewissen Grad an End-to-End-Tests (zusätzlich zu den Komponententests) einzubeziehen, um sicherzustellen, dass Ihre Anwendung für eine Datenbank ordnungsgemäß funktioniert.For this reason, it is recommended to always include some level of end-to-end testing (in addition to your unit tests) to ensure your application works correctly against a database.

Im Anschluss an diesen ArtikelFollowing along with this article

Dieser Artikel enthält umfassende Code Auflistungen, die Sie in Visual Studio kopieren können, wenn Sie möchten.This article gives complete code listings that you can copy into Visual Studio to follow along if you wish. Es ist am einfachsten, ein Komponenten Test Projekt zu erstellen, und Sie müssen .NET Framework 4,5 als Ziel verwenden, um die Abschnitte mit "Async" zu vervollständigen.It's easiest to create a Unit Test Project and you will need to target .NET Framework 4.5 to complete the sections that use async.

Erstellen einer Kontext SchnittstelleCreating a context interface

Wir sehen uns nun an, wie wir einen Dienst testen, der ein EF-Modell verwendet.We're going to look at testing a service that makes use of an EF model. Um unseren EF-Kontext durch eine in-Memory-Version für Tests ersetzen zu können, definieren wir eine Schnittstelle, die der EF-Kontext (und der in-Memory-Double) implementiert.In order to be able to replace our EF context with an in-memory version for testing, we'll define an interface that our EF context (and it's in-memory double) will implement.

Der Dienst, den wir testen werden, fragt und ändert Daten mithilfe der dbset-Eigenschaften unseres Kontexts und ruft auch SaveChanges auf, um Änderungen an die Datenbank zu übernehmen.The service we are going to test will query and modify data using the DbSet properties of our context and also call SaveChanges to push changes to the database. Daher fügen wir diese Member in die Schnittstelle ein.So we're including these members on the interface.

using System.Data.Entity;

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

Das EF-ModellThe EF model

Der zu testende Dienst nutzt ein EF-Modell, das aus dem bloggingcontext und den Blog-und Post-Klassen besteht.The service we're going to test makes use of an EF model made up of the BloggingContext and the Blog and Post classes. Dieser Code wurde möglicherweise vom EF-Designer generiert oder ist ein Code First Modell.This code may have been generated by the EF Designer or be a Code First model.

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 Kontext Schnittstelle mit dem EF-DesignerImplementing the context interface with the EF Designer

Beachten Sie, dass unser Kontext die ibloggingcontext-Schnittstelle implementiert.Note that our context implements the IBloggingContext interface.

Wenn Sie Code First verwenden, können Sie den Kontext direkt bearbeiten, um die-Schnittstelle zu implementieren.If you are using Code First then you can edit your context directly to implement the interface. Wenn Sie den EF-Designer verwenden, müssen Sie die T4-Vorlage bearbeiten, mit der ihr Kontext generiert wird.If you are using the EF Designer then you’ll need to edit the T4 template that generates your context. Öffnen Sie die <model_name> . Context.tt-Datei, die unter der EDMX-Datei gespeichert ist, das folgende Code Fragment suchen und wie gezeigt in der-Schnittstelle hinzufügen.Open up the <model_name>.Context.tt file that is nested under you edmx file, find the following fragment of code and add in the interface as shown.

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

Zu testender DienstService to be tested

Um Tests mit in-Memory-Test Doubles zu veranschaulichen, schreiben wir einige Tests für einen Blogservice.To demonstrate testing with in-memory test doubles we are going to be writing a couple of tests for a BlogService. Der Dienst ist in der Lage, neue Blogs (addblog) zu erstellen und alle Blogs nach Namen (getallblogs) nach Namen zurückzugeben.The service is capable of creating new blogs (AddBlog) and returning all Blogs ordered by name (GetAllBlogs). Zusätzlich zu getallblogs haben wir auch eine Methode bereitgestellt, die asynchron alle Blogs geordnet nach Namen (getallblogsasync) erhält.In addition to GetAllBlogs, we’ve also provided a method that will asynchronously get all blogs ordered by name (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 Test Double-in-Memory-TestsCreating the in-memory test doubles

Nachdem wir nun über das echte EF-Modell und den Dienst verfügen, die es verwenden können, ist es an der Zeit, den in-Memory-Test Double zu erstellen, den wir für Tests verwenden können.Now that we have the real EF model and the service that can use it, it's time to create the in-memory test double that we can use for testing. Wir haben einen TestContext-Test Double für den Kontext erstellt.We've created a TestContext test double for our context. In Test Doubles wählen wir das gewünschte Verhalten aus, um die Tests zu unterstützen, die wir ausführen möchten.In test doubles we get to choose the behavior we want in order to support the tests we are going to run. In diesem Beispiel erfassen wir einfach, wie oft SaveChanges aufgerufen wird, aber Sie können die erforderliche Logik zum Überprüfen des Szenarios einschließen, das Sie testen.In this example we're just capturing the number of times SaveChanges is called, but you can include whatever logic is needed to verify the scenario you are testing.

Wir haben auch ein testdbset erstellt, das eine in-Memory-Implementierung von dbset bereitstellt.We've also created a TestDbSet that provides an in-memory implementation of DbSet. Wir haben eine vollständige Implementierung für alle Methoden in dbset (mit Ausnahme von Find) bereitgestellt, Sie müssen jedoch nur die Member implementieren, die in Ihrem Testszenario verwendet werden.We've provided a complete implemention for all the methods on DbSet (except for Find), but you only need to implement the members that your test scenario will use.

Testdbset nutzt einige andere Infrastruktur Klassen, die wir enthalten haben, um sicherzustellen, dass asynchrone Abfragen verarbeitet werden können.TestDbSet makes use of some other infrastructure classes that we've included to ensure that async queries can be processed.

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 der SucheImplementing Find

Die Find-Methode ist in einer generischen Weise schwierig zu implementieren.The Find method is difficult to implement in a generic fashion. Wenn Sie Code testen müssen, der die Find-Methode verwendet, ist es am einfachsten, ein Test-dbset für jeden Entitätstyp zu erstellen, der die Suche unterstützen muss.If you need to test code that makes use of the Find method it is easiest to create a test DbSet for each of the entity types that need to support find. Anschließend können Sie die Logik zum Auffinden dieses bestimmten Entitäts Typs schreiben, wie unten gezeigt.You can then write logic to find that particular type of entity, as shown below.

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 von TestsWriting some tests

Das ist alles, was wir tun müssen, um den Test zu starten.That’s all we need to do to start testing. Der folgende Test erstellt ein TestContext und dann einen Dienst, der auf diesem Kontext basiert.The following test creates a TestContext and then a service based on this context. Der Dienst wird dann verwendet, um mithilfe der addblog-Methode einen neuen Blog – zu erstellen.The service is then used to create a new blog – using the AddBlog method. Schließlich überprüft der Test, ob der Dienst einen neuen Blog zur Blogs-Eigenschaft des Kontexts hinzugefügt hat und "SaveChanges" im Kontext heißt.Finally, the test verifies that the service added a new Blog to the context's Blogs property and called SaveChanges on the context.

Dies ist nur ein Beispiel für die Art der Dinge, die Sie mit einem in-Memory-Test Double testen können, und Sie können die Logik der Test Doubles und die Überprüfung an Ihre Anforderungen anpassen.This is just an example of the types of things you can test with an in-memory test double and you can adjust the logic of the test doubles and the verification to meet your requirements.

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

Im folgenden finden Sie ein weiteres Beispiel für einen Test. dieser Zeitpunkt führt eine Abfrage aus.Here is another example of a test - this time one that performs a query. Der Test beginnt mit dem Erstellen eines Test Kontexts mit einigen Daten in seiner Blog-Eigenschaft. Beachten Sie, dass die Daten nicht in alphabetischer Reihenfolge vorliegen.The test starts by creating a test context with some data in its Blog property - note that the data is not in alphabetical order. Wir können dann basierend auf dem Test Kontext einen BlogService erstellen und sicherstellen, dass die Daten, die wir von getallblogs erhalten, nach dem Namen geordnet sind.We can then create a BlogService based on our test context and ensure that the data we get back from GetAllBlogs is ordered by name.

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

Schließlich schreiben wir einen weiteren Test, der unsere Async-Methode verwendet, um sicherzustellen, dass die in testdbset enthaltene Async-Infrastruktur funktioniert.Finally, we'll write one more test that uses our async method to ensure that the async infrastructure we included in TestDbSet is working.

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