Test con un framework fittizio

Nota

Solo EF6 e versioni successive: funzionalità, API e altri argomenti discussi in questa pagina sono stati introdotti in Entity Framework 6. Se si usa una versione precedente, le informazioni qui riportate, o parte di esse, non sono applicabili.

Quando si scrivono test per l'applicazione, è spesso consigliabile evitare di raggiungere il database. Entity Framework consente di ottenere questo risultato creando un contesto, con un comportamento definito dai test, che usa i dati in memoria.

Opzioni per la creazione di double di test

Esistono due approcci diversi che possono essere usati per creare una versione in memoria del contesto.

  • Creare due test personalizzati: questo approccio prevede la scrittura di un'implementazione in memoria personalizzata del contesto e dei DbSet. In questo modo è possibile controllare il comportamento delle classi, ma può comportare la scrittura e la proprietà di una quantità ragionevole di codice.
  • Usare un framework fittizio per creare double di test: usando un framework fittizio ,ad esempio Moq, è possibile avere le implementazioni in memoria del contesto e i set creati in modo dinamico in fase di esecuzione.

Questo articolo illustra l'uso di un framework fittizio. Per la creazione di due test personalizzati, vedere Test con i valori double di test personalizzati.

Per illustrare l'uso di Entity Framework con un framework fittizio, verrà usato Moq. Il modo più semplice per ottenere Moq consiste nell'installare il pacchetto Moq da NuGet.

Test con versioni precedenti a EF6

Lo scenario illustrato in questo articolo dipende da alcune modifiche apportate a DbSet in EF6. Per i test con EF5 e la versione precedente, vedere Test con un contesto falso.

Limitazioni dei doppi test in memoria di Entity Framework

I valori double dei test in memoria possono essere un buon modo per fornire la copertura del livello di unit test dei bit dell'applicazione che usano Entity Framework. Tuttavia, quando si usa LINQ to Objects per eseguire query su dati in memoria. Questo può comportare un comportamento diverso rispetto all'uso del provider LINQ di EF (LINQ to Entities) per convertire le query in SQL eseguite nel database.

Un esempio di tale differenza è il caricamento di dati correlati. Se crei una serie di blog con post correlati, quando usi i dati in memoria, i post correlati verranno sempre caricati per ogni blog. Tuttavia, quando si esegue su un database, i dati verranno caricati solo se si utilizza il metodo Include.

Per questo motivo, è consigliabile includere sempre un certo livello di test end-to-end (oltre agli unit test) per garantire che l'applicazione funzioni correttamente su un database.

Seguendo questo articolo

Questo articolo fornisce elenchi di codice completi che è possibile copiare in Visual Studio per seguire la procedura, se lo si desidera. È più semplice creare un progetto unit test ed è necessario specificare come destinazione .NET Framework 4.5 per completare le sezioni che usano async.

Modello di Entity Framework

Il servizio che verrà testato usa un modello ef costituito dalle classi BloggingContext e Blog e Post. Questo codice potrebbe essere stato generato da Ef Designer o essere un modello 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; }
    }
}

Proprietà DbSet virtuali con Entity Framework Designer

Si noti che le proprietà DbSet nel contesto sono contrassegnate come virtuali. In questo modo il framework fittizio può derivare dal contesto ed eseguire l'override di queste proprietà con un'implementazione fittizia.

Se si usa Code First, è possibile modificare direttamente le classi. Se si usa Entity Framework Designer, sarà necessario modificare il modello T4 che genera il contesto. Aprire il <file model_name.Context.tt> annidato nel file edmx, trovare il frammento di codice seguente e aggiungere la parola chiave virtuale come illustrato.

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

Servizio da testare

Per illustrare i test con i doppi test in memoria, verrà scritto un paio di test per un BlogService. Il servizio è in grado di creare nuovi blog (AddBlog) e restituire tutti i blog ordinati in base al nome (GetAllBlogs). Oltre a GetAllBlogs, è stato fornito anche un metodo che otterrà in modo asincrono tutti i blog ordinati in base al nome (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();
        }
    }
}

Test di scenari non di query

È sufficiente iniziare a testare i metodi non di query. Il test seguente usa Moq per creare un contesto. Crea quindi un blog> DbSet<e lo collega fino a essere restituito dalla proprietà Blogs del contesto. Viene quindi usato il contesto per creare un nuovo BlogService che viene quindi usato per creare un nuovo blog, usando il metodo AddBlog. Infine, il test verifica che il servizio ha aggiunto un nuovo blog e chiamato SaveChanges nel contesto.

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

Test degli scenari di query

Per poter eseguire query sul doppio test di DbSet, è necessario configurare un'implementazione di IQueryable. Il primo passaggio consiste nel creare alcuni dati in memoria, usando un blog> di elenco<. Successivamente, creiamo un contesto e un blog> DBSet<e quindi si collega l'implementazione IQueryable per DbSet, ma si sta semplicemente delegando al provider LINQ to Objects che funziona con List<T>.

È quindi possibile creare un BlogService in base al test double e assicurarsi che i dati restituiti da GetAllBlogs vengano ordinati in base al nome.

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

Test con query asincrone

Entity Framework 6 ha introdotto un set di metodi di estensione che possono essere usati per eseguire in modo asincrono una query. Esempi di questi metodi includono ToListAsync, FirstAsync, ForEachAsync e così via.

Poiché le query di Entity Framework usano LINQ, i metodi di estensione vengono definiti in IQueryable e IEnumerable. Tuttavia, poiché sono progettati solo per essere usati con Entity Framework, è possibile che venga visualizzato l'errore seguente se si tenta di usarli in una query LINQ che non è una query di Entity Framework:

L'IQueryable di origine non implementa IDbAsyncEnumerable{0}. Solo le origini che implementano IDbAsyncEnumerable possono essere usate per le operazioni asincrone di Entity Framework. Per informazioni dettagliate, vedere http://go.microsoft.com/fwlink/?LinkId=287068.

Anche se i metodi asincroni sono supportati solo quando vengono eseguiti su una query EF, è possibile usarli nello unit test quando vengono eseguiti su un test in memoria double di un DbSet.

Per usare i metodi asincroni, è necessario creare un oggetto DbAsyncQueryProvider in memoria per elaborare la query asincrona. Sebbene sia possibile configurare un provider di query usando Moq, è molto più semplice creare un'implementazione doppia del test nel codice. Il codice per questa implementazione è il seguente:

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

Ora che è disponibile un provider di query asincrono, è possibile scrivere uno unit test per il nuovo metodo 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);
        }
    }
}