Megosztás a következőn keresztül:


Tesztelés utánzó keretrendszerrel

Megjegyzés:

CSAK EF6 – Az ezen az oldalon tárgyalt funkciók, API-k stb. az Entity Framework 6-ban jelentek meg. Ha korábbi verziót használ, az információk egy része vagy egésze nem érvényes.

Az alkalmazás tesztjeinek írásakor gyakran célszerű elkerülni az adatbázis elérését. Az Entity Framework lehetővé teszi ezt egy olyan környezet létrehozásával – a tesztek által meghatározott viselkedéssel –, amely memórián belüli adatokat használ.

A tesztpárok létrehozásának lehetőségei

A környezet memórián belüli verziójának létrehozásához két különböző módszer használható.

  • Saját tesztkettő létrehozása – Ez a megközelítés magában foglalja a környezet és a dbSetek saját memóriabeli implementációjának megírását. Ez sok ellenőrzést biztosít az osztályok viselkedése felett, de magában foglalhatja az ésszerű mennyiségű kód írását és birtoklását.
  • Használjon mock keretrendszert tesztbábuk létrehozásához – A mock keretrendszer (például a Moq) használatával a környezete memóriabeli implementációit és a halmazokat dinamikusan létrehozhatja futásidőben.

Ez a cikk egy szimulálási keretrendszer használatával foglalkozik. A saját tesztpárok létrehozásához lásd a Saját tesztpárok tesztelése című témakört.

Ahhoz, hogy bemutathassuk az EF használatát egy szimulálási keretrendszerrel, a Moq-t fogjuk használni. A Moq beszerzésének legegyszerűbb módja, ha telepíti a Moq-csomagot a NuGetből.

Tesztelés EF6 előtti verziókkal

A cikkben bemutatott forgatókönyv a DbSet EF6-ban végrehajtott egyes módosításaitól függ. Az EF5-ös és a korábbi verzióval való tesztelésről lásd: Tesztelés hamis környezettel.

Az EF memórián belüli tesztjének korlátozásai

A memórián belüli tesztpárok jó módszerként szolgálhatnak az EF-t használó alkalmazás bitjeinek egységszintű lefedettségéhez. Ennek során azonban linq to Objects használatával hajt végre lekérdezéseket a memóriában lévő adatokon. Ez eltérő viselkedést eredményezhet, mint amikor az EF LINQ szolgáltatóját (LINQ to Entities) használják a lekérdezések SQL-re való lefordításához, amelyet az adatbázison futtatnak.

Ilyen különbség például a kapcsolódó adatok betöltése. Ha olyan blogokat hoz létre, amelyek mindegyike kapcsolódó bejegyzésekkel rendelkezik, akkor a memóriabeli adatok használatakor a kapcsolódó bejegyzések mindig betöltve lesznek minden bloghoz. Adatbázison való futtatáskor azonban az adatok csak akkor töltődnek be, ha az Include metódust használja.

Ezért javasoljuk, hogy mindig tartalmazzon valamilyen szintű végpontok közötti tesztelést (az egységteszteken kívül), hogy az alkalmazás megfelelően működjön az adatbázissal.

A cikk követése

Ez a cikk a Visual Studióba másolható teljes kódelemeket tartalmazza, amelyeket igény szerint követhet. A legegyszerűbben Unit Test projektet hozhat létre, és a .NET-keretrendszer 4.5 verzióját kell céloznia az aszinkron részek elkészítéséhez.

Az EF-modell

A tesztelni kívánt szolgáltatás a BloggingContext és a Blog és a Post osztályokból álló EF-modellt használja. Lehetséges, hogy ezt a kódot az EF Designer készítette, vagy egy Code First modell.

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

Virtual DbSet-tulajdonságok az EF Designerrel

Vegye figyelembe, hogy a környezet DbSet-tulajdonságai virtuálisként vannak megjelölve. Ez lehetővé teszi azt, hogy a tesztelési keretrendszer a környezetünkből származtasson, és ezeket a tulajdonságokat egy mokkált implementációval felülbíráljon.

Ha a Code First szolgáltatást használja, akkor közvetlenül szerkesztheti az osztályokat. Ha az EF Designert használja, akkor szerkesztenie kell a környezetet létrehozó T4-sablont. Nyissa meg az <edmx fájl alá beágyazott model_name.Context.tt> fájlt, keresse meg a következő kódrészletet, és adja hozzá a virtuális kulcsszóhoz az ábrán látható módon.

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

Tesztelendő szolgáltatás

A memóriabeli tesztpárokkal való tesztelés bemutatásához néhány tesztet fogunk írni egy BlogService-hez. A szolgáltatás képes új blogok létrehozására (AddBlog) és a név szerint rendezett blogok (GetAllBlogs) visszaadására. A GetAllBlogs mellett egy módszert is biztosítottunk, amely aszinkron módon lekéri az összes blogot név szerint (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();
        }
    }
}

Nem lekérdezéses forgatókönyvek tesztelése

Csak ennyit kell tennünk a nem lekérdezési metódusok tesztelésének megkezdéséhez. Az alábbi teszt a Moq használatával hoz létre egy környezetet. Ezután létrehoz egy DbSet<Blog>-ot, és összekapcsolja, hogy a kontextus Blogs tulajdonságából vissza lehessen adni. Ezután a környezet egy új BlogService létrehozására szolgál, amelyet aztán egy új blog létrehozásához használnak – az AddBlog metódus használatával. Végül a teszt ellenőrzi, hogy a szolgáltatás hozzáadott-e egy új blogot, és meghívta-e a SaveChanges metódust a környezeten.

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

Lekérdezési forgatókönyvek tesztelése

Ahhoz, hogy a dbSet-teszten duplán lehessen lekérdezéseket végrehajtani, be kell állítanunk az IQueryable implementációját. Az első lépés néhány memóriabeli adat létrehozása – listablogot<> használunk. Ezután létrehozunk egy környezetet és a DBSet-blogot<>, majd lekötjük a DbSet IQueryable implementációját – csak a LINQ-t a T< listával>működő objektumszolgáltatóra delegáljuk.

Ezután létrehozhatunk egy BlogService-t a teszt duplái alapján, és biztosíthatjuk, hogy a GetAllBlogsból visszaérkezett adatok név szerint legyenek rendezve.

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

Tesztelés aszinkron lekérdezésekkel

Az Entity Framework 6 bővítménymetalógusokat vezetett be, amelyek a lekérdezés aszinkron végrehajtására használhatók. Ilyen módszerek például a ToListAsync, a FirstAsync, a ForEachAsync stb.

Mivel az Entity Framework-lekérdezések a LINQ-t használják, a bővítmény metódusai az IQueryable és az IEnumerable rendszeren vannak definiálva. Mivel azonban csak az Entity Frameworkhez való használatra lettek kialakítva, a következő hibaüzenet jelenhet meg, ha olyan LINQ-lekérdezésben próbálja használni őket, amely nem entitás-keretrendszer-lekérdezés:

A forrás IQueryable nem implementálja az IDbAsyncEnumerablet{0}. Csak az IDbAsyncEnumerable-t implementáló források használhatók az Entity Framework aszinkron műveleteihez. További információ: http://go.microsoft.com/fwlink/?LinkId=287068.

Bár az aszinkron metódusok csak EF-lekérdezésen való futtatáskor támogatottak, érdemes lehet használni őket az egységtesztben, amikor a dbSet memórián belüli tesztjének duplája fut.

Az aszinkron metódusok használatához létre kell hoznunk egy memórián belüli DbAsyncQueryProvidert az aszinkron lekérdezés feldolgozásához. Bár a Moq használatával be lehetne állítani egy lekérdezésszolgáltatót, a kódban sokkal egyszerűbb a kettős tesztelési implementáció létrehozása. Az implementáció kódja a következő:

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

Most, hogy rendelkezünk egy aszinkron lekérdezésszolgáltatóval, megírhatunk egy egységtesztet az új GetAllBlogsAsync metódushoz.

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