Aracılığıyla paylaş


Sahte bir çerçeveyle test etme

Dekont

Yalnızca EF6'ya Doğru - Bu sayfada ele alınan özellikler, API'ler vb. Entity Framework 6'da sunulmuştur. Önceki bir sürümü kullanıyorsanız, bilgilerin bir kısmı veya tümü geçerli değildir.

Uygulamanız için testler yazarken veritabanına çarpmamak genellikle tercih edilir. Entity Framework, bellek içi verileri kullanan bir bağlam (testleriniz tarafından tanımlanan davranışla) oluşturarak bunu başarmanızı sağlar.

Test çiftleri oluşturma seçenekleri

Bağlamınızın bellek içi sürümünü oluşturmak için kullanılabilecek iki farklı yaklaşım vardır.

  • Kendi test çiftlerinizi oluşturma – Bu yaklaşım, bağlamınız ve DbSet'leriniz için kendi bellek içi uygulamanızı yazmayı içerir. Bu, sınıfların davranışı üzerinde çok fazla denetim sağlar, ancak makul miktarda kod yazmayı ve bu koda sahip olmayı içerebilir.
  • Test çiftleri oluşturmak için sahte çerçeve kullanın: Sahte çerçeve (Moq gibi) kullanarak bağlamınızın ve kümelerinizin bellek içi uygulamalarını sizin için çalışma zamanında dinamik olarak oluşturabilirsiniz.

Bu makalede sahte çerçeve kullanımı ele alacaktır. Kendi test çiftlerinizi oluşturmak için bkz . Kendi Test Çiftlerinizle Test Etme.

EF'yi sahte bir çerçeveyle kullanmayı göstermek için Moq kullanacağız. Moq almanın en kolay yolu, NuGet'ten Moq paketini yüklemektir.

EF6 öncesi sürümlerle test etme

Bu makalede gösterilen senaryo, EF6'da DbSet'te yaptığımız bazı değişikliklere bağlıdır. EF5 ve önceki sürümle test etmek için bkz . Fake Context ile test etme.

EF bellek içi test çiftlerinin sınırlamaları

Bellek içi test çiftleri, uygulamanızın EF kullanan bitlerinin birim test düzeyi kapsamını sağlamak için iyi bir yol olabilir. Ancak, bunu yaparken bellek içi verilere karşı sorgu yürütmek için LINQ to Objects kullanıyorsunuz. Bu, sorguları veritabanınızda çalıştırılacak SQL'e çevirmek için EF'in LINQ sağlayıcısını (LINQ to Entities) kullanmaktan farklı davranışlara neden olabilir.

Bu tür bir farkın bir örneği, ilgili verileri yüklemektir. Her birinin ilgili Gönderileri olan bir dizi Blog oluşturursanız, bellek içi verileri kullanırken ilgili Gönderiler her blog için her zaman yüklenir. Ancak, bir veritabanında çalışırken veriler yalnızca Include yöntemini kullanırsanız yüklenir.

Bu nedenle, uygulamanızın bir veritabanında düzgün çalıştığından emin olmak için her zaman bir düzey uçtan uca test (birim testlerinize ek olarak) eklemeniz önerilir.

Bu makaleyle birlikte takip

Bu makalede, dilerseniz takip etmek üzere Visual Studio'ya kopyalayabileceğiniz eksiksiz kod listeleri sağlanır. Birim Testi Projesi oluşturmak en kolayıdır ve zaman uyumsuz kullanan bölümleri tamamlamak için .NET Framework 4.5'i hedeflemeniz gerekir.

EF modeli

Test edeceğimiz hizmet, BloggingContext ve Blog ve Post sınıflarından oluşan bir EF modelini kullanır. Bu kod EF Tasarım Aracı tarafından oluşturulmuş olabilir veya bir Code First modeli olabilir.

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

EF Tasarım Aracı ile Sanal DbSet özellikleri

Bağlamdaki DbSet özelliklerinin sanal olarak işaretlendiğini unutmayın. Bu, sahte çerçevenin bağlamımızdan türetilmesine ve bu özellikleri sahte bir uygulamayla geçersiz kılmasına olanak sağlar.

Code First kullanıyorsanız sınıflarınızı doğrudan düzenleyebilirsiniz. EF Tasarım Aracı kullanıyorsanız bağlamınızı oluşturan T4 şablonunu düzenlemeniz gerekir. <edmx dosyanız altında iç içe yerleştirilmiş model_name.Context.tt> dosyasını açın, aşağıdaki kod parçasını bulun ve gösterildiği gibi sanal anahtar sözcüğü ekleyin.

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

Test edilecek hizmet

Bellek içi test çiftleri ile testi göstermek için BlogService için birkaç test yazacağız. Hizmet, yeni bloglar (AddBlog) oluşturup ada göre sıralanmış tüm Blogları döndürebilir (GetAllBlogs). GetAllBlogs'a ek olarak, tüm blogları ada göre zaman uyumsuz olarak sipariş edecek bir yöntem de sağladık (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();
        }
    }
}

Sorgu dışı senaryoları test etme

Sorgu dışı yöntemleri test etmeye başlamak için yapmamız gereken tek şey bu. Aşağıdaki test, bağlam oluşturmak için Moq kullanır. Ardından bir DbSet<Blogu> oluşturur ve bağlamın Bloglar özelliğinden döndürülmek üzere bağlama aktarır. Daha sonra bağlam, AddBlog yöntemini kullanarak yeni bir blog oluşturmak için kullanılan yeni bir BlogService oluşturmak için kullanılır. Son olarak test, hizmetin bağlam üzerinde SaveChanges adlı yeni bir Blog eklediğini doğrular.

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

Sorgu senaryolarını test etme

DbSet test çiftimizde sorgu yürütebilmek için IQueryable uygulamasını ayarlamamız gerekir. İlk adım bazı bellek içi veriler oluşturmaktır. Liste<Blogu> kullanıyoruz. Ardından bir bağlam oluşturacağız ve DBSet Blogu> oluşturup DbSet<için IQueryable uygulamasını bağlayacağız; bunlar yalnızca Liste<T> ile çalışan LINQ to Objects sağlayıcısına temsilci olarak atanır.

Ardından test çiftlerini temel alan bir BlogService oluşturabilir ve GetAllBlogs'tan aldığımız verilerin ada göre sıralandığından emin olabilirsiniz.

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

Zaman uyumsuz sorgularla test etme

Entity Framework 6, bir sorguyu zaman uyumsuz olarak yürütmek için kullanılabilecek bir uzantı yöntemleri kümesi kullanıma sunulmuştur. Bu yöntemlere örnek olarak ToListAsync, FirstAsync, ForEachAsync vb. verilebilir.

Entity Framework sorguları LINQ kullandığından, uzantı yöntemleri IQueryable ve IEnumerable üzerinde tanımlanır. Ancak, bunlar yalnızca Entity Framework ile kullanılacak şekilde tasarlandığından, bunları Entity Framework sorgusu olmayan bir LINQ sorgusunda kullanmayı denerseniz aşağıdaki hatayı alabilirsiniz:

Kaynak IQueryable, IDbAsyncEnumerable{0} uygulamaz. Entity Framework zaman uyumsuz işlemleri için yalnızca IDbAsyncEnumerable uygulayan kaynaklar kullanılabilir. Diğer ayrıntılar için bkz: http://go.microsoft.com/fwlink/?LinkId=287068.

Zaman uyumsuz yöntemler yalnızca EF sorgusunda çalıştırılırken destekleniyor olsa da, dbSet'in bellek içi test çiftinde çalışırken bunları birim testinizde kullanmak isteyebilirsiniz.

Zaman uyumsuz yöntemleri kullanmak için zaman uyumsuz sorguyu işlemek için bellek içi DbAsyncQueryProvider oluşturmamız gerekir. Moq kullanarak bir sorgu sağlayıcısı ayarlamak mümkün olsa da kodda test çift uygulaması oluşturmak çok daha kolaydır. Bu uygulamanın kodu aşağıdaki gibidir:

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

Artık zaman uyumsuz bir sorgu sağlayıcısına sahip olduğumuza göre yeni GetAllBlogsAsync yöntemimiz için birim testi yazabiliriz.

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