Bagikan melalui


Pengujian dengan ganda pengujian Anda sendiri

Catatan

Hanya EF6 dan seterusnya - Fitur, API, dll. yang dibahas di halaman ini dimasukkan dalam Entity Framework 6. Jika Anda menggunakan versi yang lebih lama, beberapa atau semua informasi tidak berlaku.

Saat menulis pengujian untuk aplikasi Anda, sering kali diinginkan untuk menghindari kena database. Entity Framework memungkinkan Anda untuk mencapai ini dengan membuat konteks – dengan perilaku yang ditentukan oleh pengujian Anda - yang menggunakan data dalam memori.

Opsi untuk membuat pengujian ganda

Ada dua pendekatan berbeda yang dapat digunakan untuk membuat versi dalam memori konteks Anda.

  • Buat ganda pengujian Anda sendiri - Pendekatan ini melibatkan penulisan implementasi dalam memori Anda sendiri dari konteks dan DbSets Anda. Ini memberi Anda banyak kontrol atas perilaku kelas tetapi dapat melibatkan penulisan dan memiliki jumlah kode yang wajar.
  • Gunakan kerangka kerja tiruan untuk membuat pengujian ganda - Menggunakan kerangka kerja tiruan (seperti Moq) Anda dapat memiliki implementasi dalam memori konteks dan set yang dibuat secara dinamis pada runtime untuk Anda.

Artikel ini akan menangani pembuatan double pengujian Anda sendiri. Untuk informasi tentang menggunakan kerangka kerja tiruan, lihat Pengujian dengan Kerangka Kerja Tiruan.

Pengujian dengan versi pra-EF6

Kode yang ditampilkan dalam artikel ini kompatibel dengan EF6. Untuk pengujian dengan EF5 dan versi yang lebih lama, lihat Pengujian dengan Konteks Palsu.

Batasan ganda pengujian dalam memori EF

Ganda pengujian dalam memori dapat menjadi cara yang baik untuk memberikan cakupan tingkat pengujian unit bit aplikasi Anda yang menggunakan EF. Namun, saat melakukan ini, Anda menggunakan LINQ ke Objek untuk menjalankan kueri terhadap data dalam memori. Ini dapat mengakibatkan perilaku yang berbeda dari menggunakan penyedia LINQ EF (LINQ ke Entitas) untuk menerjemahkan kueri ke dalam SQL yang dijalankan terhadap database Anda.

Salah satu contoh perbedaan tersebut adalah memuat data terkait. Jika Anda membuat serangkaian Blog yang masing-masing memiliki Posting terkait, maka ketika menggunakan data dalam memori, Posting terkait akan selalu dimuat untuk setiap Blog. Namun, saat berjalan terhadap database, data hanya akan dimuat jika Anda menggunakan metode Sertakan.

Untuk alasan ini, disarankan untuk selalu menyertakan beberapa tingkat pengujian end-to-end (selain pengujian unit Anda) untuk memastikan aplikasi Anda bekerja dengan benar terhadap database.

Mengikuti artikel ini

Artikel ini memberikan daftar kode lengkap yang dapat Anda salin ke Visual Studio untuk diikuti jika Anda mau. Paling mudah untuk membuat Proyek Pengujian Unit dan Anda harus menargetkan .NET Framework 4.5 untuk menyelesaikan bagian yang menggunakan asinkron.

Membuat antarmuka konteks

Kita akan melihat pengujian layanan yang memanfaatkan model EF. Agar dapat mengganti konteks EF kami dengan versi dalam memori untuk pengujian, kami akan menentukan antarmuka yang akan diterapkan konteks EF kami (dan ganda dalam memori).

Layanan yang akan kami uji akan mengkueri dan memodifikasi data menggunakan properti DbSet dari konteks kami dan juga memanggil SaveChanges untuk mendorong perubahan ke database. Jadi kami menyertakan anggota ini pada antarmuka.

using System.Data.Entity;

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

Model EF

Layanan yang akan kami uji menggunakan model EF yang terdiri dari BloggingContext dan kelas Blog dan Post. Kode ini mungkin telah dihasilkan oleh EF Designer atau menjadi model Code First.

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

Menerapkan antarmuka konteks dengan EF Designer

Perhatikan bahwa konteks kami mengimplementasikan antarmuka IBloggingContext.

Jika Anda menggunakan Code First, Anda dapat mengedit konteks secara langsung untuk mengimplementasikan antarmuka. Jika Anda menggunakan Desainer EF, Anda harus mengedit templat T4 yang menghasilkan konteks Anda. <Buka file model_name.Context.tt> yang disarangkan di bawah file edmx Anda, temukan fragmen kode berikut dan tambahkan di antarmuka seperti yang ditunjukkan.

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

Layanan yang akan diuji

Untuk menunjukkan pengujian dengan pengujian dalam memori ganda, kita akan menulis beberapa tes untuk BlogService. Layanan ini mampu membuat blog baru (AddBlog) dan mengembalikan semua Blog yang dipesan berdasarkan nama (GetAllBlogs). Selain GetAllBlogs, kami juga telah menyediakan metode yang secara asinkron akan mendapatkan semua blog yang dipesan berdasarkan nama (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();
        }
    }
}

Membuat ganda pengujian dalam memori

Sekarang setelah kita memiliki model EF nyata dan layanan yang dapat menggunakannya, saatnya untuk membuat pengujian dalam memori ganda yang dapat kita gunakan untuk pengujian. Kami telah membuat pengujian TestContext dua kali lipat untuk konteks kami. Dalam pengujian ganda kita bisa memilih perilaku yang kita inginkan untuk mendukung tes yang akan kita jalankan. Dalam contoh ini kami hanya menangkap berapa kali SaveChanges dipanggil, tetapi Anda dapat menyertakan logika apa pun yang diperlukan untuk memverifikasi skenario yang Anda uji.

Kami juga telah membuat TestDbSet yang menyediakan implementasi DbSet dalam memori. Kami telah menyediakan implementasi lengkap untuk semua metode di DbSet (kecuali untuk Temukan), tetapi Anda hanya perlu menerapkan anggota yang akan digunakan skenario pengujian Anda.

TestDbSet menggunakan beberapa kelas infrastruktur lain yang telah kami sertakan untuk memastikan bahwa kueri asinkron dapat diproses.

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

Menerapkan Temukan

Metode Temukan sulit diimplementasikan dengan cara generik. Jika Anda perlu menguji kode yang menggunakan metode Temukan, paling mudah untuk membuat DbSet pengujian untuk setiap jenis entitas yang perlu mendukung temukan. Anda kemudian dapat menulis logika untuk menemukan jenis entitas tertentu, seperti yang ditunjukkan di bawah ini.

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

Menulis beberapa tes

Itu saja yang perlu kita lakukan untuk memulai pengujian. Pengujian berikut membuat TestContext lalu layanan berdasarkan konteks ini. Layanan ini kemudian digunakan untuk membuat blog baru - menggunakan metode AddBlog. Terakhir, pengujian memverifikasi bahwa layanan menambahkan Blog baru ke properti Blog konteks dan disebut SaveChanges pada konteks.

Ini hanyalah contoh dari jenis hal-hal yang dapat Anda uji dengan tes dalam memori ganda dan Anda dapat menyesuaikan logika pengujian ganda dan verifikasi untuk memenuhi kebutuhan Anda.

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

Berikut adalah contoh lain dari pengujian - kali ini yang melakukan kueri. Pengujian dimulai dengan membuat konteks pengujian dengan beberapa data di properti Blog-nya - perhatikan bahwa data tidak dalam urutan alfabet. Kita kemudian dapat membuat BlogService berdasarkan konteks pengujian kita dan memastikan bahwa data yang kita dapatkan kembali dari GetAllBlogs diurutkan berdasarkan nama.

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

Terakhir, kita akan menulis satu pengujian lagi yang menggunakan metode asinkron kita untuk memastikan bahwa infrastruktur asinkron yang kita sertakan dalam TestDbSet berfungsi.

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