Author: wilaiwan.h

  • 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() แล้วส่งข้อมูลที่สำหรับทดสอบกลับไปได้ โดยไม่ต้องใช้ข้อมูลจริงจากฐานข้อมูล

  • การพัฒนา unit test โดย shim type

    Shims เป็นหนึ่งใน technology ที่อยู่ใน Microsoft Fakes Framework ใช้ในการพัฒนา unit testing เพื่อแยก component ที่ต้องการทดสอบออกมาจากปัจจัยแวดล้อมต่างรอบๆ component ในกระบวนการทดสอบ โดย shims จะทำการเปลี่ยนทิศทางการเรียกใช้ method ที่กำหนด ไปยัง code ที่เขียนขึ้นมาใช้ในการทดสอบ ส่วนใหญ่เราจะใช้ shims เพื่อแยก component ที่ต้องการทดสอบออกจาก assemblies ที่ไม่ได้เป็นส่วนหนึ่งของ solution ในการพัฒนา (กรณีที่ต้องการแยก component ที่ต้องการทดสอบออกจาก solution ของตัวเอง ควรจะใช้ stubs )

    method ที่พัฒนาส่วนใหญ่จะ return ผลการทำงานที่ต้องขึ้นกับเงื่อนไข ปัจจัยต่างๆจากภายนอก ในทางกลับกันสำหรับ shim  shim จะอยู่ภายใต้การควบคุมในกระบวนการทดสอบ สามารถที่จะ return ผลการทำงานตามที่กำหนดในทุกๆครั้งที่เรียกใช้งาน ซึ่งทำให้การเขียน unit testing ทำได้ง่ายขึ้นมาก

    ตัวอย่าง method การตรวจสอบวันที่เอกสารในปีงบประมาณ

    public static class Utility {
        public static bool IsInFiscalYear() {
            if (DateTime.Now < new DateTime(2015, 10, 1))
                return false;
            else
                return true;
        }
    }

    เมื่อต้องการทดสอบ method “IsInFiscalYear” จะพบว่าการทำงานของ method ขึ้นอยู่กับ DateTime.Now ซึ่งเป็นเวลาปัจุจบันที่ได้จากระบบ ซึ่งทำให้การทดสอบยุ่งยากขึ้น (เมื่อทำการทดสอบต้องเปลี่ยน DateTime เพื่อทำการทดสอบ ซึ่งอาจจะกระทบกับส่วนอื่นๆ ไม่สามารถทำการทดสอบแบบอัตโนมัติได้ ) ซึ่งการทดสอบ method ที่มีการเรียกใช้ database, web service ก็เช่นเดียวกัน เนื่องจากกลไกการทำงานขึ้นอยู่กับปัจจัยภายนอก ซึ่ง shim จะเข้ามาช่วยตรงจุดนี้

    Shim types จะให้กลไกการเปลี่ยนทิศทางการเรียกใช้ .NET method ไปยัง function ( หรือ user delegate) ที่เขียนขึ้น โดย shim types จะถูกสร้างโดย Fakes generator

    using (ShimsContext.Create()
    {
        ShimDateTime.NowGet = () => new DateTime(2016, 6, 1);
        var isIn = Utility.IsInFiscalYear();
    
        Assert.AreEqual(true, isIn);
    }

    ShimDateTime คือ shim type ที่ถูกสร้างโดย Fakes generator เพื่อใช้ในการกำหนดกลไกการทำงานแทน DateTime.Now

    การเพิ่ม Fakes Assemblies ใน solution ทำได้โดย

    1. ใน solution explorer ขยาย References ของ unit test project
    2. เลือก assembly ที่มี class ที่ต้องการสร้าง shim type (จากตัวอย่างนี้ต้องการสร้าง shim type ของ DateTime ให้เลือก System.dll)
    3. click ขวา เลือก Add Fakes Assembly

    ในการใช้ shim type ใน unit test framework จะต้องเขียน test code อยู่ใน ShimsContext เพื่อควบคุม lifetime ของ shim type (ถ้าไม่อยู่ภายใต้ ShimsContext, shim type จะคงอยู่จนกระทั่งปิดโปรแกรม) การสร้าง ShimsContext ทำได้โดยการเรียกใช้งาน static Create() ดังเช่นตัวอย่าง code ข้างต้น

    shim type สามารถใช้งานแทนที่ .NET method รวมทั้ง static method, instance method

    Static methods

    ShimMyClass.MyStaticMethod = () =>5;

    Instance methods สำหรับทุก instance

    ShimMyClass.AllInstances.MyMethod = () => 5;

    Instance methods แต่ละ instance

    var myClass1 = new ShimMyClass()
    {
        MyMethod = () => 5
    };

     

    อ้างอิง :

    • https://msdn.microsoft.com/en-us/library/hh549176.aspx