Testowanie za pomocą pozorowania platformy

Uwaga

Tylko rozwiązanie EF6 i nowsze wersje — Funkcje, interfejsy API itp. omówione na tej stronie zostały wprowadzone w rozwiązaniu Entity Framework 6. Jeśli korzystasz ze starszej wersji, niektóre lub wszystkie podane 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 podwajań testów

Istnieją dwa różne podejścia, których można użyć do utworzenia wersji kontekstu w pamięci.

  • Tworzenie własnych podwajań testów — takie podejście polega na pisaniu własnej implementacji kontekstu i zestawów DbSet w pamięci. Zapewnia to dużą kontrolę nad zachowaniem klas, ale może obejmować pisanie i posiadanie rozsądnej ilości kodu.
  • Użyj platformy pozorowania, aby utworzyć dublety testowe — przy użyciu platformy pozorowania (takiej jak Moq) możesz mieć implementacje kontekstu w pamięci i zestawy tworzone dynamicznie w czasie wykonywania.

Ten artykuł zajmie się używaniem pozornej struktury. Aby utworzyć własny test, zobacz Testowanie za pomocą własnych podwajań testów.

Aby zademonstrować korzystanie z platformy EF z pozorowania, 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 zestawie DbSet w programie 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ę

Testy w pamięci mogą być dobrym sposobem zapewnienia pokrycia poziomu testu jednostkowego bitów aplikacji korzystającej z platformy EF. 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) w celu tłumaczenia zapytań na bazę danych SQL.

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 w przypadku uruchamiania względem bazy danych dane zostaną załadowane tylko w przypadku użycia 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.

Poniżej przedstawiono 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. Aby ukończyć sekcje korzystające z asynchronicznego środowiska .NET Framework, należy użyć programu .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 Projektant 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 zestawu danych z Projektant EF

Należy pamiętać, że właściwości DbSet w kontekście są oznaczone jako wirtualne. Pozwoli to na pozorowanie struktury pochodzącej z naszego kontekstu i zastąpienie tych właściwości pozorowaną implementacją.

Jeśli używasz funkcji Code First, możesz edytować klasy bezpośrednio. Jeśli używasz Projektant 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 go do wirtualnego słowa kluczowego, 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ą testu w pamięci, będziemy pisać kilka testów dla blogservice. 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 blog> DbSet<i podłącza go do zwracania z właściwości Blogi kontekstu. Następnie kontekst służy do tworzenia nowej usługi BlogService, która następnie służy do tworzenia nowego bloga — przy użyciu metody AddBlog. Na koniec test sprawdza, czy usługa dodała nowy blog i nazwała ciąg SaveChanges w kontekście.

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 niektórych danych w pamięci — używamy bloga> listy<. Następnie utworzymy kontekst i blog> DBSet<, a następnie połączymy implementację IQueryable dla zestawu dbSet — są one po prostu delegowanie 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 w przypadku uruchamiania względem zapytania EF, możesz użyć ich w teście jednostkowym podczas uruchamiania względem testu w pamięci dwukrotnie zestawu dbSet.

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