Unit test สำหรับ method ที่ใช้ Entity Framework

กลไกการทำงานของ .NET method โดยส่วนใหญ่  จะมีกลไกการทำงานที่ต้องขึ้นกับเงื่อนไข ปัจจัยต่างๆจากภายนอกอย่างเช่น method ที่มีเรียกใช้ database หรือ web service ทำให้การทดสอบจำเป็นต้องแยก component ที่ต้องการทดสอบออกมาจากปัจจัยแวดล้อมต่างรอบๆ component ซึ่งสามารถทำได้โดยใช้ shim ที่อยู่ภายใต้การควบคุมในกระบวนการทดสอบ สามารถที่จะควบคุมผลลัพธ์ที่ได้การทำงานตามที่กำหนดในทุกๆครั้งที่เรียกใช้งาน ซึ่งทำให้การเขียน unit testing ทำได้ง่ายขึ้นมาก

การพัฒนา unit test สำหรับ method ที่ใช้ Entity Framework เพื่อเข้าถึงฐานข้อมูล ก็สามารถใช้ shim type ในการกำหนดชุดของข้อมูลเพื่อทำการทดสอบ ซึ่งการ query จะกระทำกับ property ของ DbContext ซึ่ง return IDbSet<T>

public partial class Entities : DbContext
{
   public Entities(): base("name=Entities")
   {
   }

    public IDbSet<CONFIG> CONFIG { get; set; }
   ...
}

ในการพัฒนา unit test จะต้องสร้าง shim type สำหรับ class “Entities” และแทนที่ property ที่ดึงข้อมูลจากฐานข้อมูลจริงแยกออกจากการทดสอบ ด้วยข้อมูลสำหรับทดสอบ จากตัวอย่างข้างบนคือ property “CONFIG” ซึ่งเป็นประเภท IDbSet<CONFIG> โดยจะทำการ returm DbSet<CONFIG> ที่เตรียมข้อมูลไว้สำหรับการทดสอบ

using (ShimsContext.Create())
{
     ShimEntities.AllInstances.CONFIGGet =
              (e) =>  {
                         return ... DbSet<CONFIG>
                      };
}

แต่ใน DbSet ไม่มี public constructor ทำให้ไม่สามารถสร้าง instance ของ DbSet ได้ จึงจำเป็นต้องสร้าง class ใหม่ที่ implement interface IDbSet<T> แทนการใช้ DbSet

public class TestDbSet<T> : IDbSet<T>, IQueryable, IEnumerable<T>
    where T : class
 {
     ObservableCollection<T> _data;
     IQueryable _query;

     public TestDbSet() //: base()
     {
        _data = new ObservableCollection<T>();
        _query = _data.AsQueryable();
     }

     public virtual T Find(params object[] keyValues)
     {
        throw new NotImplementedException("Derive from TestDbSet<T> and override Find");
     }

     public T Add(T item)
     {
         _data.Add(item);
         return item;
     }

     public T Remove(T item)
     {
         _data.Remove(item);
         return item;
     }

     public T Attach(T item)
     {
         _data.Add(item);
         return item;
     }

     public T Create()
     {
         return Activator.CreateInstance<T>();
     }

     public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
     {
         return Activator.CreateInstance<TDerivedEntity>();
     }

     public ObservableCollection<T> Local
     {
         get { return new ObservableCollection<T>(_data); }
     }

     Type IQueryable.ElementType
     {
         get { return _query.ElementType; }
     }

     System.Linq.Expressions.Expression IQueryable.Expression
     {
         get { return _query.Expression; }
     }

     IQueryProvider IQueryable.Provider
     {
         get { return _query.Provider; }
     }

     System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
     {
         return _data.GetEnumerator();
     }

     IEnumerator<T> IEnumerable<T>.GetEnumerator()
     {
         return _data.GetEnumerator();
     }
 }

หลังจากนั้นสร้าง class อีกหนึ่ง class ที่ inherit มาจาก TestDbSet<T> ระบุ generic type เป็น model class ที่ต้องการในที่นี้คือ CONFIG  และเขียน code override Find() method ของTestDbSet<T> เพื่อทำหน้าที่ค้นหา object ตาม key ที่ส่งมา ( สาเหตุ ต้องสร้าง class ที่ inherit มาจาก TestDbSet<T> เนื่องจาก แต่ละ model class อาจจะมี key ที่ไม่ตรงกัน ดังนั้นใน  TestDbSet<T>.Find() method จึงไม่สามารถ implement code ที่ต้องการได้ )

 public class ConfigDbSet : TestDbSet<Models.CONFIG>
 {
    public override Models.CONFIG Find(params object[] keyValues)
    {
        return this.SingleOrDefault(s => s.ID == (decimal)keyValues.Single());
    }
 }

จากนั้นกลับมาที่ unit test method ก็จะทำการ new ConfigDbSet() แล้วส่งข้อมูลที่สำหรับทดสอบกลับไปได้ โดยไม่ต้องใช้ข้อมูลจริงจากฐานข้อมูล

Leave a Reply