แนวทางการเขียน Unit Tests ที่ดีสำหรับ C # (2)

ในบทความก่อนหน้า ได้พูดถึงการเลือก Testing framework ที่เหมาะสม การเขียน unit test โดยใช้แนวคิดแบบ “AAA” (Arrange, Acr, Assert) และการกำหนดชื่อ method ที่สื่อความหมายเข้าใจได้ง่าย

Test Initialize & Cleanup

เมื่อ unit tests ทำการทดสอบ methods ภายใน class ก็จำเป็นต้องสร้าง instance ของ class นั้นๆขึ้นมาก่อน ซึ่งจะเกิดขึ้นหลายๆครั้งใน unit test เพื่อประหยัดเวลาและทรัพยากรของระบบ เราสามารถใช้ [TestInitialize] attribute ของ MSTest เพื่อบอกว่า method นั้นใช้สำหรับกำหนดค่าเริ่มต้น สร้าง instance ของ class ก่อนที่จะเริ่ม run unit tests (หรือ [SetUp] ใน NUnit หรือถ้าใช้ xUnit.net ให้สร้างใน constructor )

เมื่อการทดสอบจบลง เราจะต้องทำการ cleanup object ต่างๆที่ใช้ในการทดสอบ โดยการใช้ [TestCleanup] attribute ในกรณีที่ใช้ MSTest ([TearDown ใน NUnit หรือ implement IDisposable interface สำหรับกรณีที่ใช้ xUnit.net)

ตัวอย่างด้านล่าง จะกำหนดให้ method “Initialize” เป็น method ที่ใช้สำหรับสร้าง instance ของ class ที่จะใช้ในการทดสอบ ซึ่งจะถูกเรียกใช้ก่อนการทดสอบจะเริ่มทำงาน

ILogger _log;
ICalc _calc;

[TestInitialize]
public void Initialize()
{
	_log = Mock.Of<ILogger<Calc>>();
	_calc = new Calc(_log);
}

[TestMethod]
public void Divide_ShouldThrowArgumentException_IfDivideByZero()
{
	double result = _calc.Divide(10, 5);
	result.ShouldBe(2);
}

[TestCleanup]
public void Cleanup()
{
	// Optionally dispose or cleanup objects
        _calc = null;
	...
}

Shouldly Assertion Framework

Shouldly framework จะช่วยให้ unit test ทำได้ง่ายและเข้าใจได้ง่ายขึ้น มันจะช่วยผู้พัฒนาในการเขียนการยืนยันผลการทำงานของ unit tests ซึ่งเมื่อกลับมาดู unit test อีกครั้งสามารถอ่าน code แล้วเข้าใจวัตถุประสงค์และความคาดหวังของการทดสอบ

เปรียบเทียบความแตกต่างระหว่าง การใช้ Shouldly กับ ไม่ใช่ Shouldly

With Shouldly

result.ShouldBe(2);

Without Shouldly

Assert.AreEqual(2, result);

Shouldly สามารถใช้ในการตรวจสอบ exception ว่าเกิดตามที่คาดหวังไว้หรือไม่ และสามารถตรวจสอบ meesage ของ exception ได้

[TestMethod]
public void Divide_ShouldThrowArgumentException_IfDivideByZero()
{
	Should.Throw<ArgumentException>(() => _calcs.Dicide(10, 0))
	      .Message
              .ShouldBe("Divide by zero.");
}

Moq Mocking Framework

การสร้าง object จำลองสำหรับ object ที่ไม่ใช่ object หลักที่จะทำการทดสอบ แทนที่การเรียกใช้ object จริงๆ เช่น logging utility หรือ database จะทำให้การทดสอบทำได้อย่างมีประสิทธิภาพขึ้น ซึ่งการทำแบบนี้ เราต้องใช้ Mocking framework มาช่วย โดยตัวที่ได้รับความนิยมตัวนึงก็คือ Moq framework

การใช้ Moq framework จะช่วยให่เราสามารถ mock class ต่างๆโดยใช้เพียง interface. ตัวอย่างเช่น การใช้ mock ของ logging utility ใน unit test แทนที่จะสร้างและเรียกใช้ logging utility ตัวจริง

ILogger log = Mock.Of<ILogger<Calc>>();

We can also verify if a mock object was invoked inside a method and the amount of times it was invoked:

[TestMethod]
public void Divide_LogInformationMethod()
{
	ILogger log = Mock.Of<ILogger<Calc>>();
	ICalc calc = new Calc(log);
	double result = calc.Divide(10,5);
	Mock.Get(log).Verify(x => x.Log(
		LogLevel.Information, 
    	It.IsAny<EventId>(), 
    	It.IsAny<FormattedLogValues>(), 
    	It.IsAny<Exception>(), 
    	It.IsAny<Func<object, Exception, string>>()), 
    	Times.Once);
}

อ้างอิง : https://kiltandcode.com/2019/06/16/best-practices-for-writing-unit-tests-in-csharp-for-bulletproof-code/