เขียน Unit test ทดสอบการทำงานกับฐานข้อมูลที่ใช้ Entity Framework Core

ในบทความนี้ จะนำเสนอการเขียน unit test เพื่อทดสอบการทำงานของ method ที่ใช้งาน Entity Framework Core ซึ่งตัว Entity Framework Core มาพร้อมความสามารถที่สามารถใช้งาน in-memory store ซึ่งสามารถใช้ในการทดสอบได้ หรือจะใช้ mock framework ในการทดสอบก็ได้ โดยในบทความนี้จะแสดงให้เห็นทั้งสองแนวทาง

In Memory Database

In Memory Database เป็นการสร้าง database จำลองขึ้นมาใช้บน memory แทนที่การใช้ database ตัวจริง ทำให้เราสามาถเขียน unit test โดยที่ไม่ไปกระทบกับ database จริงๆ โดยการเพิ่ม Microsoft.Entityframework.InMemory NuGet package เข้าไปใน test projet

ในส่วนของ DbContext class จะต้องมี constructor ที่รับ DbContectOption เป็น parameter

var options = new DbContextOptionsBuilder<MyDbContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString("N")).Options;

var dbContext = new MyDbContext(options);

จากตัวอย่างด้านบน เราใช้ UseInMemoryDatabase extension method -ของ DbContextOptionsBuilder ซึ่งรับ parameter 1 ตัวเพื่อกำหนดชื่อของ database โดยเราจะใช้ Guid จากนั้นก็สามารถเขียน unit test ได้ดังนี้

[TestClass]
public class MyRepositoryTest
{
        private MyDbContext CreateDbContext()
        {
            var options = new DbContextOptionsBuilder<MyDbContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString("N")).Options;

            var dbContext = new MyDbContext(options);
            return dbContext;
        }

       
        [TestMethod]
        public void It_should_add_a_product_successfully()
        {
            //Arrange
            var dbContext = CreateDbContext();
            var rep = new MyRepository(dbContext);
            var product = new Product 
            { 
               ProductName="Toyota Altis",
               UnitPrice=900000 
            };

            //Act
            var result = rep.Add(product);
           
            //Assert
            Assert.IsTrue(result.ProductID > 0);

            //Clean up
            dbContext.Dispose();
        }

        [TestMethod]
        public void It_should_update_a_product_successfully()
        {
            //Arrange
            var expected = 950000;
            var dbContext = CreateDbContext();
            var rep = new MyRepository(dbContext);
            var product = new Product 
            { 
               ProductName="Toyota Altis",
               UnitPrice=900000 
            };

            //Act
            var result = rep.Add(product);
            result.UnitPrice = expected;
            rep.Update(result);
            result = dbContext.Products.First(p=>p.ProductID == result.ProductID);

            //Assert
            Assert.AreEqual(expected,result.UnitPrice);

            //Clean up
            dbContext.Dispose();
        }

        [TestMethod]
        public void It_should_remove_a_product_successfully()
        {
            //Arrange
            var dbContext = CreateDbContext();
            var rep = new MyRepository(dbContext);
            var product = new Product 
            { 
               ProductName="Toyota Altis",
               UnitPrice=900000 
            };

            //Act
            var result = rep.Add(product);
            rep.Delete(result.ProductID);
            var isExists = dbContext.Products.Any(p => p.ProductID == result.ProductID);

            //Assert
            Assert.IsFalse(isExists);

            //Clean up
            dbContext.Dispose();
        }

    }

จากตัวอย่างด้านบน CreateDbContext method ทำหน้าที่ในการสร้าง DbContext object ที่ทำงานกับ in-memory database แทนที่จะใช้งาน database ตัวจริง หลังจากนั้นก็สามารถทำตามที่ต้องการได้ เหมือนกับการใช้งาน DbContext ปกติ

Using Mock Frameworks

อีกทางเลือกหนึ่ง เราสามารถใช้ mock frameworks ในการเขียน unit tests ซึ่งเราจะใช้ Moq mock frameworks ซึ่งเป็น mock frameworks ที่ได้รับความนิยมสำหรับ .Net platform โดยก่อนที่จะเริ่มเขียน unit test เราจะต้องกำหนดให้ DbSet pproperty ของ DbContext เป็น virtual ซึงจะทำให้ Moq framework สามารถสร้าง fake object และ override property นี้ได้ และดำเนินการดังนี้

  1. สร้าง generic collection สำหรับ DbSet property แต่ละตัว
  2. Config fake DbSet ใน mock framework
  3. Setup และ forward DbSet property เหล่านั้นไปที่ generic collection
    • Provider
    • Expression
    • ElementType
    • GetEnumerator()
[TestClass]
public class MyRepositoryTest
{
        private DbSet<T> CreateDbSet<T>(IQueryable<T> collection) where T:class
        {
            var stubDbSet = new Mock<DbSet<T>>();
            stubDbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(collection.Provider);
            stubDbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(collection.Expression);
            stubDbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(collection.ElementType);
            stubDbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(collection.GetEnumerator());
            return stubDbSet.Object;
        }

        [TestMethod]
        public void list_all_available_products_from_the_data_store()
        {
            //Arrange
            var expected = new List<Product>
            {
                new Product {ProductName ="Toyota yaris"},
                new Product {ProductName ="Toyota altis"},
                new Product {ProductName ="Toyota camry"}
            };

            var productDbSet = CreateDbSet(expected.AsQueryable());

            var stubContext = new Mock<MyDbContext>();
            stubContext.Setup(o=>o.Products).Returns(productDbSet);
            var rep = new MyRepository(stubContext.Object);

            //Act
            var actual = (List<Product>)rep.GetAll();

            //Assert
            CollectionAssert.AreEquivalent(expected, actual);

        }


        [TestMethod]
        public void return_a_product_for_given_product_id()
        {
            //Arrange
            var expected = "Toyota altis";
            var expected = new List<Product>
            {
                new Product {ProductName ="Toyota yaris"},
                new Product {ProductName ="Toyota altis"},
                new Product {ProductName ="Toyota camry"}
            };

            var productDbSet = CreateDbSet(collection.AsQueryable());

            var stubContext = new Mock<MyDbContext>();
            stubContext.Setup(o => o.Products).Returns(productDbSet);
            var rep = new MyRepository(stubContext.Object);

            //Act
            var actual = rep.GetById(2);

            //Assert
            Assert.AreEqual(expected, actual.ProductName);

        }        
    }

อ้างอิง : https://www.bitiniyan.com/2019/02/02/how-to-write-unit-tests-with-entity-framework-core/