Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga / Notatka
Tylko od EF6 wzwyż — funkcje, interfejsy API itp. omówione na tej stronie zostały wprowadzone w Entity Framework 6. Jeśli używasz starszej wersji, niektóre lub wszystkie informacje nie mają zastosowania.
Podczas pisania testów dla aplikacji często pożądane jest uniknięcie trafienia do bazy danych. Program Entity Framework umożliwia osiągnięcie tego celu, tworząc kontekst — z zachowaniem zdefiniowanym przez testy — który korzysta z danych w pamięci.
Opcje tworzenia dublerów testowych
Istnieją dwa różne podejścia, których można użyć do utworzenia wersji kontekstu w pamięci.
- Tworzenie własnych atrap testowych — takie podejście polega na pisaniu własnej implementacji kontekstu i DbSetów w pamięci. Zapewnia to dużą kontrolę nad zachowaniem klas, ale może obejmować pisanie i posiadanie rozsądnej ilości kodu.
- Użyj frameworku do pozorowania, aby utworzyć dublety testowe — używając frameworku do pozorowania (takiego jak Moq), możesz mieć implementacje kontekstu w pamięci i zestawy danych tworzone dynamicznie w czasie wykonywania.
Ten artykuł dotyczy użycia frameworku do mockowania. Aby tworzyć własne doblety testowe, zapoznaj się z Testowaniem za pomocą własnych dubletów testowych.
Aby zademonstrować użycie EF z frameworkiem do mockowania, użyjemy narzędzia Moq. Najprostszym sposobem pobrania moq jest zainstalowanie pakietu Moq z narzędzia NuGet.
Testowanie przy użyciu wersji pre-EF6
Scenariusz przedstawiony w tym artykule jest zależny od pewnych zmian wprowadzonych w DbSet w EF6. Aby uzyskać informacje na temat testowania przy użyciu programu EF5 i starszej wersji, zobacz Testowanie za pomocą fałszywego kontekstu.
Ograniczenia testu w pamięci EF podwaja się
Dublery testowe w pamięci mogą być dobrym sposobem zapewnienia pokrycia na poziomie testu jednostkowego części aplikacji korzystających z Entity Framework. Jednak w takim przypadku używasz linQ to Objects do wykonywania zapytań względem danych w pamięci. Może to spowodować inne zachowanie niż użycie dostawcy LINQ platformy EF (LINQ to Entities) do tłumaczenia zapytań na SQL, które są uruchamiane na Twojej bazie danych.
Przykładem takiej różnicy jest ładowanie powiązanych danych. Jeśli utworzysz serię blogów, które mają powiązane wpisy, podczas korzystania z danych w pamięci powiązane wpisy będą zawsze ładowane dla każdego bloga. Jednak gdy uruchamiasz zapytania do bazy danych, dane zostaną załadowane tylko, jeśli użyjesz metody Include.
Z tego powodu zaleca się, aby zawsze uwzględniać pewien poziom kompleksowego testowania (oprócz testów jednostkowych), aby upewnić się, że aplikacja działa prawidłowo w bazie danych.
Śledź ten artykuł
Ten artykuł zawiera pełne listy kodu, które można skopiować do programu Visual Studio, aby wykonać następujące czynności, jeśli chcesz. Najłatwiej jest utworzyć projekt testów jednostkowych, a do ukończenia sekcji, które używają async, należy użyć .NET Framework 4.5.
Model EF
Usługa, która zamierzamy przetestować, korzysta z modelu EF złożonego z klas BloggingContext i Blog i Post. Ten kod mógł zostać wygenerowany przez projektanta EF Lub być modelem 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; }
}
}
Właściwości wirtualnego DbSet w EF Designer
Należy pamiętać, że właściwości DbSet w kontekście są oznaczone jako wirtualne. Pozwoli to frameworkowi do pozorowania pochodzić z naszego kontekstu i zastąpić te właściwości pozorowaną implementacją.
Jeśli używasz funkcji Code First, możesz edytować klasy bezpośrednio. Jeśli używasz projektanta EF, musisz edytować szablon T4, który generuje kontekst. <Otwórz plik model_name.Context.tt>, który jest zagnieżdżony w pliku edmx, znajdź następujący fragment kodu i dodaj do niego słowo kluczowe "virtual", jak pokazano.
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));
}
Usługa do przetestowania
Aby zademonstrować testowanie za pomocą testów w pamięci, napiszemy kilka testów dla usługi blogowej. Usługa może tworzyć nowe blogi (AddBlog) i zwracać wszystkie blogi uporządkowane według nazwy (GetAllBlogs). Oprócz getAllBlogs udostępniliśmy również metodę, która asynchronicznie pobierze wszystkie blogi uporządkowane według nazwy (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();
}
}
}
Testowanie scenariuszy niezwiązanych z zapytaniami
To wszystko, co musimy zrobić, aby rozpocząć testowanie metod innych niż zapytania. Poniższy test używa narzędzia Moq do utworzenia kontekstu. Następnie tworzy `DbSet
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());
}
}
}
Testowanie scenariuszy zapytań
Aby móc wykonywać zapytania względem naszego testu DbSet, musimy skonfigurować implementację zapytania IQueryable. Pierwszym krokiem jest utworzenie danych w pamięci — używamy Listy<Blog>. Następnie utworzymy kontekst i DBSet<Blog>, a następnie skonfigurujemy implementację IQueryable dla DbSet — delegują one po prostu do dostawcy LINQ to Objects, który współpracuje z Listą<T>.
Następnie możemy utworzyć usługę BlogService na podstawie naszych podwojeń testowych i upewnić się, że dane, które otrzymujemy z usługi GetAllBlogs, są uporządkowane według nazwy.
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);
}
}
}
Testowanie za pomocą zapytań asynchronicznych
Program Entity Framework 6 wprowadził zestaw metod rozszerzeń, których można użyć do asynchronicznego wykonywania zapytania. Przykłady tych metod to ToListAsync, FirstAsync, ForEachAsync itp.
Ponieważ zapytania platformy Entity Framework korzystają z LINQ, metody rozszerzenia są definiowane w zapytaniach IQueryable i IEnumerable. Jednak ze względu na to, że są one przeznaczone tylko do użycia z programem Entity Framework, może zostać wyświetlony następujący błąd, jeśli spróbujesz użyć ich w zapytaniu LINQ, które nie jest zapytaniem programu Entity Framework:
Źródłowy element IQueryable nie implementuje elementu IDbAsyncEnumerable{0}. W przypadku operacji asynchronicznych programu Entity Framework można używać tylko źródeł, które implementują element IDbAsyncEnumerable. Aby uzyskać więcej informacji, zobacz http://go.microsoft.com/fwlink/?LinkId=287068.
Chociaż metody asynchroniczne są obsługiwane tylko podczas uruchamiania względem zapytania Entity Framework, możesz chcieć użyć ich w teście jednostkowym podczas uruchamiania względem dublikatu testowego DbSet w pamięci.
Aby użyć metod asynchronicznych, musimy utworzyć obiekt DbAsyncQueryProvider w pamięci, aby przetworzyć zapytanie asynchroniczne. Chociaż można skonfigurować dostawcę zapytań przy użyciu narzędzia Moq, znacznie łatwiej jest utworzyć podwójną implementację testu w kodzie. Kod tej implementacji jest następujący:
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; }
}
}
}
Teraz, gdy mamy dostawcę zapytań asynchronicznych, możemy napisać test jednostkowy dla nowej metody GetAllBlogsAsync.
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);
}
}
}