Testen mit einem FrameworksTesting with a mocking framework

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 die festgelegt werden, die für Sie dynamisch zur Laufzeit erstellt werden.Use a mocking framework to create test doubles – Using a mocking framework (such as Moq) you can have the in-memory implementations of your context and sets created dynamically at runtime for you.

In diesem Artikel wird die Verwendung eines-Frameworks behandelt.This article will deal with using a mocking framework. Informationen zum Erstellen eigener Test Doubles finden Sie unter Testen mit ihren eigenen Test Doubles.For creating your own test doubles see Testing with Your Own Test Doubles.

Um die Verwendung von EF mit einem-Frameworks zu veranschaulichen, verwenden wir "muq".To demonstrate using EF with a mocking framework we are going to use Moq. Die einfachste Möglichkeit zum Aufrufen von "muq" ist die Installation des " muq"-Pakets von nuget.The easiest way to get Moq is to install the Moq package from NuGet.

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

Das in diesem Artikel gezeigte Szenario ist abhängig von einigen Änderungen, die wir an dbset in EF6 vorgenommen haben.The scenario shown in this article is dependent on some changes we made to DbSet in 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.

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

Eigenschaften von virtuellen dbsets mit dem EF-DesignerVirtual DbSet properties with EF Designer

Beachten Sie, dass die dbset-Eigenschaften für den Kontext als virtuell markiert sind.Note that the DbSet properties on the context are marked as virtual. Dadurch kann das Simulation Framework aus unserem Kontext abgeleitet werden und diese Eigenschaften mit einer simulierte Implementierung überschreiben.This will allow the mocking framework to derive from our context and overriding these properties with a mocked implementation.

Wenn Sie Code First verwenden, können Sie die Klassen direkt bearbeiten.If you are using Code First then you can edit your classes directly. 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 in der EDMX-Datei gespeichert ist, suchen Sie das folgende Code Fragment, und fügen Sie das Virtual-Schlüsselwort wie gezeigt hinzu.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 virtual keyword as shown.

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

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 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 nicht-Abfrage SzenariosTesting non-query scenarios

Das ist alles, was wir tun müssen, um nicht-Abfrage Methoden zu testen.That’s all we need to do to start testing non-query methods. Im folgenden Test wird "muq" verwendet, um einen Kontext zu erstellen.The following test uses Moq to create a context. Anschließend wird ein dbset erstellt <Blog> , das aus der Blogs-Eigenschaft des Kontexts zurückgegeben wird.It then creates a DbSet<Blog> and wires it up to be returned from the context’s Blogs property. Im nächsten Schritt wird der Kontext verwendet, um einen neuen Blog Dienst zu erstellen, der dann verwendet wird, um mithilfe der addblog-Methode einen neuen Blog zu erstellen –.Next, the context is used to create a new BlogService which is then used to create a new blog – using the AddBlog method. Schließlich wird mit dem Test überprüft, ob der Dienst einen neuen Blog hinzugefügt und "SaveChanges" für den Kontext aufgerufen hat.Finally, the test verifies that the service added a new Blog and called SaveChanges on the context.

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 Abfrage SzenariosTesting query scenarios

Um Abfragen für unseren dbset-Test Double ausführen zu können, müssen wir eine Implementierung von iquervable einrichten.In order to be able to execute queries against our DbSet test double we need to setup an implementation of IQueryable. Der erste Schritt besteht darin, einige Daten im Arbeitsspeicher zu erstellen – wir verwenden eine Liste <Blog> .The first step is to create some in-memory data – we’re using a List<Blog>. Als Nächstes erstellen wir einen Kontext und dbset und <Blog> Verknüpfen dann die iquerable-Implementierung für das dbset – Sie delegieren lediglich an den LINQ to Objects-Anbieter, der mit List arbeitet <T> .Next, we create a context and DBSet<Blog> then wire up the IQueryable implementation for the DbSet – they’re just delegating to the LINQ to Objects provider that works with List<T>.

Wir können dann basierend auf den Test Doubles 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 doubles and ensure that the data we get back from GetAllBlogs is ordered by name.

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 AbfragenTesting with async queries

In Entity Framework 6 wurde eine Reihe von Erweiterungs Methoden eingeführt, die verwendet werden können, um eine Abfrage asynchron auszuführen.Entity Framework 6 introduced a set of extension methods that can be used to asynchronously execute a query. Beispiele für diese Methoden sind z. b. "delistasync", "firstasync", "foreachasync" undExamples of these methods include ToListAsync, FirstAsync, ForEachAsync, etc.

Da Entity Framework Abfragen LINQ verwenden, werden die Erweiterungs Methoden für iquervable und IEnumerable definiert.Because Entity Framework queries make use of LINQ, the extension methods are defined on IQueryable and IEnumerable. Da Sie jedoch nur für die Verwendung mit Entity Framework entworfen wurden, erhalten Sie möglicherweise die folgende Fehlermeldung, wenn Sie versuchen, Sie in einer LINQ-Abfrage zu verwenden, die keine Entity Framework Abfrage ist:However, because they are only designed to be used with Entity Framework you may receive the following error if you try to use them on a LINQ query that isn’t an Entity Framework query:

Die iquervable-Quelle implementiert idbasyncenumerable nicht {0} .The source IQueryable doesn't implement IDbAsyncEnumerable{0}. Nur Quellen, die idbasyncenumerable implementieren, können für Entity Framework asynchronen Vorgängen verwendet werden.Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. Weitere Informationen finden Sie unter http://go.microsoft.com/fwlink/?LinkId=287068 .For more details see http://go.microsoft.com/fwlink/?LinkId=287068.

Obwohl die asynchronen Methoden nur bei der Ausführung für eine EF-Abfrage unterstützt werden, können Sie Sie in Ihrem Komponenten Test verwenden, wenn Sie für einen Test Double-Wert im Arbeitsspeicher eines dbsets ausgeführt werden.Whilst the async methods are only supported when running against an EF query, you may want to use them in your unit test when running against an in-memory test double of a DbSet.

Um die Async-Methoden zu verwenden, müssen wir einen in-Memory-dbasyncqueryprovider erstellen, um die asynchrone Abfrage zu verarbeiten.In order to use the async methods we need to create an in-memory DbAsyncQueryProvider to process the async query. Obwohl es möglich wäre, mithilfe von "muq" einen Abfrage Anbieter einzurichten, ist es viel einfacher, eine Test-Double-Implementierung im Code zu erstellen.Whilst it would be possible to setup a query provider using Moq, it is much easier to create a test double implementation in code. Der Code für diese Implementierung lautet wie folgt:The code for this implementation is as follows:

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

Nun, da wir einen Async-Abfrage Anbieter haben, können wir einen Komponenten Test für unsere neue getallblogsasync-Methode schreiben.Now that we have an async query provider we can write a unit test for our new GetAllBlogsAsync method.

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