Pengujian dengan kerangka kerja tiruan
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 penggunaan kerangka kerja tiruan. Untuk membuat ganda pengujian Anda sendiri, lihat Pengujian dengan Double Pengujian Anda Sendiri.
Untuk menunjukkan penggunaan EF dengan kerangka kerja tiruan, kita akan menggunakan Moq. Cara term mudah untuk mendapatkan Moq adalah dengan menginstal paket Moq dari NuGet.
Pengujian dengan versi pra-EF6
Skenario yang ditunjukkan dalam artikel ini tergantung pada beberapa perubahan yang kami buat pada DbSet di 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.
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
{
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; }
}
}
Properti Virtual DbSet dengan EF Designer
Perhatikan bahwa properti DbSet pada konteks ditandai sebagai virtual. Ini akan memungkinkan kerangka kerja tiruan untuk berasal dari konteks kami dan mengambil alih properti ini dengan implementasi yang ditipu.
Jika Anda menggunakan Code First, Anda dapat mengedit kelas anda secara langsung. 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 dalam kata kunci virtual seperti yang ditunjukkan.
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));
}
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 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();
}
}
}
Menguji skenario non-kueri
Itu saja yang perlu kita lakukan untuk mulai menguji metode non-kueri. Pengujian berikut menggunakan Moq untuk membuat konteks. Kemudian membuat Blog> DbSet<dan menghubungkannya untuk dikembalikan dari properti Blog konteks. Selanjutnya, konteks digunakan untuk membuat BlogService baru yang kemudian digunakan untuk membuat blog baru - menggunakan metode AddBlog. Terakhir, pengujian memverifikasi bahwa layanan menambahkan Blog baru dan disebut SaveChanges pada konteks.
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());
}
}
}
Menguji skenario kueri
Agar dapat menjalankan kueri terhadap pengujian DbSet dua kali lipat, kita perlu menyiapkan implementasi IQueryable. Langkah pertama adalah membuat beberapa data dalam memori - kami menggunakan Blog> Daftar<. Selanjutnya, kami membuat konteks dan Blog DBSet<> kemudian menyambungkan implementasi IQueryable untuk DbSet - mereka hanya mendelegasikan ke penyedia LINQ ke Objects yang berfungsi dengan List<T>.
Kita kemudian dapat membuat BlogService berdasarkan tes ganda kita dan memastikan bahwa data yang kita dapatkan kembali dari GetAllBlogs diurutkan berdasarkan nama.
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);
}
}
}
Pengujian dengan kueri asinkron
Entity Framework 6 memperkenalkan serangkaian metode ekstensi yang dapat digunakan untuk menjalankan kueri secara asinkron. Contoh metode ini termasuk ToListAsync, FirstAsync, ForEachAsync, dll.
Karena kueri Entity Framework menggunakan LINQ, metode ekstensi ditentukan pada IQueryable dan IEnumerable. Namun, karena hanya dirancang untuk digunakan dengan Kerangka Kerja Entitas, Anda mungkin menerima kesalahan berikut jika Anda mencoba menggunakannya pada kueri LINQ yang bukan kueri Kerangka Kerja Entitas:
Sumber IQueryable tidak mengimplementasikan IDbAsyncEnumerable{0}. Hanya sumber yang mengimplementasikan IDbAsyncEnumerable yang dapat digunakan untuk operasi asinkron Kerangka Kerja Entitas. Untuk detail selengkapnya, lihat http://go.microsoft.com/fwlink/?LinkId=287068.
Sementara metode asinkron hanya didukung saat berjalan terhadap kueri EF, Anda mungkin ingin menggunakannya dalam pengujian unit Saat menjalankan pengujian dalam memori ganda dari DbSet.
Untuk menggunakan metode asinkron, kita perlu membuat DbAsyncQueryProvider dalam memori untuk memproses kueri asinkron. Sementara itu akan mungkin untuk menyiapkan penyedia kueri menggunakan Moq, jauh lebih mudah untuk membuat pengujian implementasi ganda dalam kode. Kode untuk implementasi ini adalah sebagai berikut:
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; }
}
}
}
Sekarang setelah kita memiliki penyedia kueri asinkron, kita dapat menulis pengujian unit untuk metode GetAllBlogsAsync baru kita.
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);
}
}
}