Mock object ด้วย Moq library แบบ .NETๆ… Actions!

Moq มันคืออะไร

Moq (อ่านว่า Mock-you หรือ Mock เฉยๆก็ได้) เป็น library สำหรับ mocking ที่เกิดมาเพื่อ .NET โดยเฉพาะ แต่แรกเลยมันถูกพัฒนามาเพื่อให้ใช้ประโยชน์สูงสุดของ .NET 3.5 (เช่น Linq expression trees) และ คุณลักษณะใหม่ๆของ C# 3.0 (เช่น lambda expressions) จึงทำให้ Moq มีประสิทธิภาพมากที่สุดในโลกของ .NET Moq เป็น mocking library ที่ใช้ประโยชน์จาก type-safe และ refactoring-friendly และยังสนับสนุน mocking interfaces ไปจนถึง classe ใดๆอีกด้วย Moq มี API ที่เรียบง่าย และตรงไปตรงมาสุดๆดังนั้นจึงไม่จำเป็นต้องมีความรู้ หรือประสบการณ์เกี่ยวกับ mocking concept มาก่อนเเลยก็ได้ เพราะมันใช้งานได้ง่ายมาก

ถ้าหากคุณไม่เคยใช้ mocking library ใดๆ สำหรับเขียน Unit Test(UT) (ซึ่งเป็นหลักการสำคัญของ Test – Driven Development(TDD)) หรือต้องทำ mockup objects เพื่อทำ prototype UI นำเสนอลูกค้า แน่นอนว่าคุณจะต้องเขียน mock(fake, stub) objects เอง ซึ่งมันจะประกอบไปด้วย code mock objects ที่ต้องเขียนเองอย่างมากมาย(ซึงเป็นงานที่น่าเบื่อมาก) เพื่อที่จะแทนการจำลองเรื่องราว หรือพฤติกรรมตามที่คุณต้องการเพื่อทดสอบให้ครอบคลุม(coverage) หรือเพื่อทำ mockup prototype UI เพื่อนำเสนอลูกค้า ใน usecase ต่างๆอีกมากมาย ซึ่งนักพัฒนาโดยส่วนใหญ่ยังคงปฏิบัติในในแบบข้างต้นนี้อยู่ และยึดติดเอาไปเป็นหลักการปฏิบัติหลัก(หรือ classical) TDD ซึ่งเป็นผลทำให้เกิดเป็นอุปสรรคของการนำ mocking library ใดๆ ที่จะต้องเพิ่มเข้ามาอีกเล็กน้อย แต่จะให้วิธีการปฏิบัติแบบเดิมๆนั้นง่ายขึ้น ทำงานน้อยลง หรือทำงานเบาลง(more lightweight) และดูสวยงามอ่านง่ายขึ้น ซึ่ง mocking library เหล่านั้นสามารถทำได้ และทั้งหมดนี้ Moq ก็ได้จัดเตรียมไว้ให้เราหมดแล้วครับ และจะเรียกนักพัฒนาที่ใช้วิธีการ mock objects ว่า mockist TDD

Moq… Actions!

ก่อนอื่นไป downloads Moq มาไว้ที่เครื่องเราก่อนจาก Download หลังจากได้มาแล้วก็แตก Zip ออกมาครับ สร้าง projects visual studio Unit Test ขึ้นมาแล้ว Add refrences Moq.dll  library ซึ่งอยู่ใน folder ของ Moq library เข้ามาใน projects ได้เลย

ตอนนี้ก็พร้อมที่จะ Moq กันละครับโดยผมสมมุติ model Product และ interface IProductRepository สำหรับ access model Product ดังนี้ (ตัวอย่างอาจจะง่ายๆนะครับ ผมแค่อยากนำเสนอให้เห็นการใช้งานเบื้องต้นเท่านั้น)

public interface IProductRepository
{
    Product GetById(int id);
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

ต่อมาผมต้องการทดสอบ model Basket ของผมซึ่งเป็น object-under test ของผม code ของ Basket

public class Basket
{
    List<Product> _myBasket;
    public Basket()
    {
        _myBasket = new List<Product>();
    }

    public void AddProduct(Product p)
    {
        _myBasket.Add(p);
    }

    public int CountBasket { get { return _myBasket.Count; } }

    public decimal GetValue()
    {
        return (from p in _myBasket select p.Price).Sum();//หรือ _myBasket.Sum(p => p.Price)
    }
}

จะเห็นว่า Basket มี method AddProduct และ GetTotalValue มี property CountBasket แต่ Product นั้นเอามาจาก IProductRepository ซึ่งจะต้องทำการ mock มันขึ้นมาก่อน โดยผมสมติเรื่องราวไว้ ดังนี้

ฉากที่ 1: เริ่มต้น มี product อยู่ 3 ชนิด ราคาแตกต่างกันคือ 10, 20 และ 30 อยู่ใน ProductRepository และเมื่อ get Product ออกมาอย่างละ 2 ชิ้น เอามาเพิ่มเข้าไปใน Basket อันหนึ่ง ดังนั้นผลลัพย์ที่คาดหวังจาก Basket ที่ method GetTotalValue ต้องเท่ากับ 120 และ CountBasket ต้องเท่ากับ 6

จากเรื่องราวข้างบนผมเขียน Unit Test หรือ Test Fixture โดยใช้ Moq library ได้ดังนี้

using Moq; 

[TestMethod]
public void TestMethod1()
{
    var mock = new Mock<IProductRepository>();
    mock.Setup(m => m.GetById(1)).Returns(new Product { Id = 1, Name = “One”, Price = 10});
    mock.Setup(m => m.GetById(2)).Returns(new Product { Id = 2, Name = “Two”, Price = 20});
    mock.Setup(m => m.GetById(3)).Returns(new Product { Id = 3, Name = “Tree”, Price = 30});

    IProductRepository productRepository = mock.Object;

    var basket =new Basket();
    basket.AddProduct(productRepository.GetById(1));
    basket.AddProduct(productRepository.GetById(1));
    basket.AddProduct(productRepository.GetById(2));
    basket.AddProduct(productRepository.GetById(2));
    basket.AddProduct(productRepository.GetById(3));
    basket.AddProduct(productRepository.GetById(3));

    Assert.AreEqual(6, basket.CountBasket);
    Assert.AreEqual(120, basket.GetValue());
}

จาก code ผมได้ให้ section แบ่งไว้เป็นสีๆ ก็คือ

สีเขียว คือส่วนของการ setup mock objects ทั้งหมด ที่เกียวกับเรื่องราวฉากที่ 1 เล่าว่า “มี product อยู่ 3 ชนิด ราคาแตกต่างกันคือ 10, 20 และ 30 อยู่ใน ProductRepository”

สีน้ำเงิน คือส่วนของการ excercise object-under test Basket ของผมให้มัทำงานตามเรื่องราวฉากที่ 1 เล่าว่า “get Product ออกมาอย่างละ 2 ชิ้น แล้ว เพิ่มเข้าไปใน Basket อันหนึ่ง” และส่วนสุดท้าย

สีแดง คือส่วนของการ verify หรือ assert ก็คือการตรวจสอบผลลัพย์จากการ excercise  นั่นเองครับว่ามีความถูกต้องตรงตามความคาดหวังที่เราต้องการจาก object-under test นี้หรือไม่ ได้จากเรื่อวราวฉากที่ 1 ที่กล่าวว่า “ดังนั้นผลลัพย์ที่คาดหวังจาก Basket ที่ method GetTotalValue ต้องเท่ากับ 120 และ CountBasket ต้องเท่ากับ 6”

OK ครับ… ผมขอจบบทความ Unit Test ด้วย Moq library แบบ .NETๆ… Actions! ไว้เพียงเท่านี้ครับ จริงๆมันยังมีลูกเล่น และความน่าสนใจกว่านี้อีกเยอะครับ ที่ผมทำเป็นตัวอย่างนี้มันแค่จิ๊บๆเท่านั้น อันนี้ก็ขึ้นอยู่กับการ mockup หรือการจำลองเรื่องราวของคุณเอง ยิ่งเหมือนจริงมากเท่าไร software application ของคุณก็ยิ่งแสดงได้สมจริงสมจัง และมีคุณภาพมากขึ้นเท่านั้น นั่นก็หมายถึงความซับซ้อนในการทำ mock object ของคุณก็มากขึ้นตามไปด้วย ก็ลองเอาไปใช้ปฏิบัติดูนะครับ อย่างน้อยๆมันก็ดีกว่าเขียน class ที่ต้อง implement  ตาม interface ที่ object-under test ของเราจำเป็นต้องติดต่อสื่อสาร(communicate) เพื่อทำงานร่วมกันขึ้นมาเองตรงๆอย่างแน่นอนครับ

แถมให้อีกหน่อย

Test Fixture ข้างล่างนี้ ผมได้ทำการ mock Message Processor เพื่อจำลองการทำงานง่ายๆให้กับมันไปก่อนที่จะ implements เจ้า Message Processor กันจริงๆ วัตถุประสงค์ที่ทำมันก็เพื่อทดลองความเป็นไปได้ ทดลองแนวคิด หรือจำลองเพื่อทำอะไรก็แล้วแต่ครับ เชิญชมครับ

using Moq; 

[TestMethod]
public void TestMockMessageProcessor()
{
    var mock = new Mock<IMessageProcessor>();

    mock.Setup(processor => processor
        .Process(It.IsAny<string>()))
        .Returns<string>( msg => string.Format(“คุณ: {0}. AI: ไม่เข้าใจข้อความของคุณครับ.”, msg));
    mock.Setup(processor => processor
        .Process(“แกเป็นใคร?”))
        .Returns<string>(msg => string.Format(“คุณ: {0}. AI: ฉันคือ Message Processor ของคุณไง.”, msg));
    mock.Setup(processor => processor
        .Process(“แกทำไรได้บ้างอะ?”))
        .Returns<string>(msg => string.Format(“คุณ: {0}. AI: ทำความเข้าใจข้อความของคุณแล้วตอบกลับ เท่าที่คุณโปรแกรมให้ฉันรู้.”, msg));

    var mockMessageProcessor = mock.Object;

    Console.WriteLine(mockMessageProcessor.Process(“แกเป็นใคร?”));
    Console.WriteLine(mockMessageProcessor.Process(“แกทำไรได้บ้างอะ?”));
    Console.WriteLine(mockMessageProcessor.Process(“ไหนแกกระโดดซิ”));
}
}

public interface IMessageProcessor
{
string Process(string msg);
}

และนี่คือ Output จาก Console

คุณ: แกเป็นใคร?. AI: ฉันคือ Message Processor ของคุณไง.
คุณ: แกทำไรได้บ้างอะ?. AI: ทำความเข้าใจข้อความของคุณแล้วตอบกลับ เท่าที่คุณโปรแกรมให้ฉันรู้.
คุณ: ไหนแกกระโดดซิ. AI: ไม่เข้าใจข้อความของคุณครับ.

ขอบคุณครับ 🙂

ถ้าคุณอยากเก่งอะไรสักอย่าง ก็ให้จิตนาการมันขึ้นมา แล้วซ้อมกับมันบ่อยๆ

— Chav:P —

แหล่งข้อมูล

Advertisements

#unit-test