Udostępnij za pośrednictwem


Testowanie przy użyciu własnych podwojeń testu

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ę tworzeniem własnego testu dwukrotnie. Aby uzyskać informacje na temat korzystania z platformy pozorowania, zobacz Testowanie za pomocą platformy Mocking Framework.

Testowanie przy użyciu wersji pre-EF6

Kod przedstawiony w tym artykule jest zgodny z platformą 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 .

Tworzenie interfejsu kontekstowego

Przyjrzymy się testowaniu usługi, która korzysta z modelu EF. Aby móc zastąpić nasz kontekst EF wersją w pamięci na potrzeby testowania, zdefiniujemy interfejs, który będzie implementować nasz kontekst EF (i jest w pamięci dwukrotnie).

Usługa, która będziemy testować, będzie wykonywać zapytania i modyfikować dane przy użyciu właściwości DbSet naszego kontekstu, a także wywołać metodę SaveChanges, aby wypchnąć zmiany do bazy danych. Dlatego dołączamy te elementy członkowskie do interfejsu.

using System.Data.Entity;

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

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

Implementowanie interfejsu kontekstowego za pomocą Projektant EF

Pamiętaj, że nasz kontekst implementuje interfejs IBloggingContext.

Jeśli używasz funkcji Code First, możesz edytować kontekst bezpośrednio w celu zaimplementowania interfejsu. 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 w sposób pokazany.

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

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

Tworzenie testu w pamięci podwaja się

Teraz, gdy mamy rzeczywisty model EF i usługę, która może jej używać, nadszedł czas, aby utworzyć test w pamięci dwukrotnie, którego możemy użyć do testowania. Utworzyliśmy test TestContext dwukrotnie dla naszego kontekstu. W testach dwukrotnie wybieramy zachowanie, które chcemy obsługiwać testy, które będziemy uruchamiać. W tym przykładzie po prostu przechwytujemy liczbę wywołań funkcji SaveChanges, ale możesz uwzględnić dowolną logikę wymaganą do zweryfikowania testowego scenariusza.

Utworzyliśmy również zestaw TestDbSet, który zapewnia implementację zestawu DbSet w pamięci. Udostępniliśmy pełną implementację dla wszystkich metod w zestawie DbSet (z wyjątkiem funkcji Find), ale wystarczy zaimplementować tylko elementy członkowskie, których będzie używać twój scenariusz testowy.

Zestaw TestDbSet korzysta z innych klas infrastruktury, które zostały uwzględnione, aby upewnić się, że zapytania asynchroniczne mogą być przetwarzane.

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

Implementowanie znajdowania

Metoda Find jest trudna do zaimplementowania w sposób ogólny. Jeśli musisz przetestować kod, który korzysta z metody Find, najłatwiej jest utworzyć testowy zestaw DbSet dla każdego z typów jednostek, które muszą obsługiwać wyszukiwanie. Następnie możesz napisać logikę, aby znaleźć ten konkretny typ jednostki, jak pokazano poniżej.

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

Pisanie niektórych testów

To wszystko, co musimy zrobić, aby rozpocząć testowanie. Poniższy test tworzy element TestContext, a następnie usługę opartą na tym kontekście. Usługa jest następnie używana do tworzenia nowego bloga — przy użyciu metody AddBlog. Na koniec test sprawdza, czy usługa dodała nowy blog do właściwości Blogs kontekstu i o nazwie SaveChanges w kontekście.

Jest to tylko przykład typów rzeczy, które można przetestować za pomocą testu w pamięci podwójnej i można dostosować logikę podwaja testu i weryfikacji, aby spełnić wymagania.

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

Oto kolejny przykład testu — tym razem, który wykonuje zapytanie. Test rozpoczyna się od utworzenia kontekstu testowego z niektórymi danymi we właściwości bloga — zwróć uwagę, że dane nie są w kolejności alfabetycznej. Następnie możemy utworzyć usługę BlogService na podstawie naszego kontekstu testowego i upewnić się, że dane, które otrzymujemy z usługi GetAllBlogs, są uporządkowane według nazwy.

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

Na koniec napiszemy jeszcze jeden test, który używa naszej metody asynchronicznej, aby upewnić się, że infrastruktura asynchronizna dołączona do zestawu TestDbSet działa.

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