วิชา MSTest 101 — มาเริ่มเขียน Unit Test ด้วย Microsoft Test Framework กันเถอะ
Quality of software อยู่ที่ Developer … ไม่ใช่ QA
คำนี้ หัวหน้าผมเคยบอกไว้เสมอ กล่าวคือ Developer มีหน้าที่ทำยังไงก็ได้ ให้ software ออกมามีคุณภาพดีที่สุด bug น้อยที่สุด ก่อนที่จะส่งไปถึงมือ QA
จะรู้ได้ยังไงว่า function หรือ method ที่เราเขียนขึ้นมานั้นทำงานถูกต้อง?
จะมั่นใจได้ยังไงว่า function ที่เราเขียน ถ้าส่งค่า แปลกๆเข้าไปแล้วมันจะไม่ return อะไรที่แปลกกว่าออกมา?? (อ่าว… งงดิ ผมก็งง)
เราจึงต้องมีสิ่งที่มาช่วยเรานั้นคือ Unit Test จะมาช่วยเสริมความมั่นใจให้โค้ดเราไม่หวั่นแม้วันมามากได้ ช่วยกรอง bug ออกไปให้น้อยยยยยที่สุด ก่อนถึงมือ QA และ Production
วันนี้เราจึงมาเริ่มเขียน Unit Test กันแบบง่ายๆ ให้ทุกคนนำไปต่อยอดกันต่อไป
โดย Framework ที่เราจะใช้ในวันนี้ก็คือเจ้า Microsoft Test Framework v2 หรือเรียกสั้นๆย่อๆว่า MSTest v2 ซึ่งทาง Microsoft ก็ได้มีการพัฒนา Test Framework นี้มาตลอด จนกระทั่งมายกเครื่องใหม่กลายเป็น v2 แบบที่เราใช้กันอยู่ในปัจจุบัน โดยมี Template การสร้างต่างๆติดมากับ Visual Studio 2017 ด้วยเลย ทำให้เราเริ่มเขียนง่ายขึ้นไปอีก
มาเริ่มกันเลย !!
เริ่มจากสร้างโปรเจคของเราขึ้นมาก่อน หรือถ้ามีอยู่แล้วก็ใช้โปรเจคเดิมก็ได้ครับ โดยเป้าหมายเราคือเริ่มเขียน test ที่ function ง่ายๆก่อน ลองเลือกมาสัก 1 function
ในตัวอย่างโปรเจคนี้เกี่ยวกับเกมส์ ซึ่งจะมีการคำนวณค่าพลังต่างๆ เราจะเลือก function ที่ชื่อว่า DamageFromNormalAttack ใน class DamageCaculatorService
function นี้ทำหน้าที่คำนวณ damage ที่ได้รับจากการโจมตี โดยที่มี parameter 1 ตัวคือ atk:attack power เป็นค่าพลังโจมตีที่จะมาทำความเสียหายให้ตัวละครที่ได้รับการโจมตี โดยมี def: defense คือค่าพลังป้องกัน เอาไว้หักลบกับพลังโจมตีที่อีกฝ่ายโจมตีเข้ามา โดยที่ค่า def จะรับเข้ามาจาก constructor ตอนที่ประกาศ new object
ขั้นตอนต่อไปให้เราสร้าง Test Project ขึ้นมา โดยทำได้หลายวิธี
วิธีที่ 1 สร้างจาก Template Wizard
ง่ายที่สุดก็คือการคลิกขวาที่ function ที่เราต้องการทำ Unit Test แล้วเลือกคำสั่ง “Create Unit Test”
จะขึ้นหน้าต่าง Popup Template สำหรับสร้าง Unit Test ขึ้นมาให้เราเลือก โดยมีรายละเอียดดังนี้
Test Framework: มีให้เลือกหลายตัวเลย แต่ในที่นี้เราจะเลือกของ MSTestv2
Test Project: เป็นตัวเลือกว่า function Unit Test ที่จะสร้าง จะให้ไปอยู่ใน Test Project ไหน ตรงนี้เอาไว้เลือกในกรณีที่เรามี Test Project หลายๆตัว แต่ในกรณีไม่เคยมีมาก่อน ก็เลือก <New Test Project> ระบบจะทำการสร้างให้เราใหม่
Name Format for Test Project: เป็นชื่อ Test Project ที่เราจะสร้าง
Namespace: ของ Test Project
Output File: คือไฟล์ของ Test Method ที่เราต้องการให้ไปอยู่
Name Format for Test Class: Format ชื่อของ Test Class — กรณีที่สร้างใหม่ให้ก็เลือกแบบ Template ไว้
Name Format for Test Method: Format ชื่อของ Test Method — กรณีที่สร้างใหม่ให้ก็เลือกแบบ Template ไว้
Code for Test Method: อันนี้เลือกอะไรก็ได้ไปก่อน เดี๋ยวเราจะไปเขียนเพิ่มอยู่แล้ว ไม่ต้องห่วง
พอเราเลือกเสร็จแล้ว ก็ให้กด OK แล้ว Visual Studio จะทำการ Generate Test Project , Class รวมไปถึง Test Method มาให้เราตามที่เราได้ตั้งค่าไว้
หรือ
วิธีที่ 2 แบบ Manual สร้างเอง
ให้ทำการ Add New Project ไปที่ Solution โดยเลือก MSTest Test Project เลือกสำหรับ .NET Core (ถ้า App ที่จะ Test เป็น .NET Framework ก็เลือกแบบ .NET Framework)
ทำการตั้งชื่อ Test Project
จะได้ Test Project ตามที่เราสร้างไว้
จากนั้นเราจะทำการ Add Reference Project ที่ต้องการ Test ไปที่ Test Project
เลือก Project ที่ต้องการ
จะได้ Test Project ที่พร้อมใช้งานแล้ว
โดยทั้ง 2 วิธีควรจะมี Package อยู่ 4 ตัว คือ
coverlet.collector
Microsoft.NET.Test.Sdk
MSTest.TestAdapter
MSTest.TestFramework
ตามรูปข้างล่าง
จากนั้นเราจะมาสร้างไฟล์ให้หน้าตาเหมือนที่ generate มาจาก Template Wizard
เริ่มจากลบไฟล์ UnitTest1.cs ออก จากนั้นสร้างโฟลเดอร์ชื่อ Services และสร้างไฟล์ชื่อ DamageCaculatorServiceTests.cs แล้วใส่โค้ดนี้ลงไป
ตอนนี้เราจะได้ Test Project ที่มีโค้ดเหมือนกับ Project ที่สร้างโดย Template Wizard แล้ว
มันคืออะไรกันนะ งงเลย
มาอธิบายโค้ดกันเล็กน้อย ไปที่ไฟล์ DamageCaculatorServiceTests.cs เราจะเห็นโค้ดสไตล์แบบนี้ใน Test Project เป็น Pattern ปกติ
เริ่มจากจะเห็นได้ว่าบนหัว Class DamageCaculatorServiceTests จะมี Attribute TestClass() ติดอยู่ ซึ่งตัวนี้จะทำหน้าที่บอก compiler ว่า Class นี้เป็น Test Class นะ เพื่อที่ระบบมันจะได้จัดการต่อไป
ต่อมาคือ Method DamageFromNormalAttackTest จะมี Attribute TestMethod() ติดอยู่ ซึ่งตัวนี้ก็จะเป็นตัวบอกว่า Method นี้เป็น Test Method นะ
ซึ่งบางครั้งใน Test Class เดียวกัน ก็อาจมี Method ที่เราสร้างมาเพื่อช่วยในการทำ Unit Test พวกเช่นพวกเตรียม Data หรือ Config ต่างๆ ซึ่งเราไม่ได้ต้องการ Test Method พวกนี้ จึงต้องมี Attribute TestMethod() มาช่วยให้ compiler เข้าใจได้นั่นเอง (รวมไปถึง Test Class ด้วยนะ)
วิธีเช็คว่าทำถูกไหมก็คือให้เปิด Test Explorer ขึ้นมา (View -> Test Explorer หรือ Ctrl+E,T)
จะเจอ Test Class และ Test Method ที่เราสร้างขึ้นมา ในที่นี้เราลองใส่ Method DoSomeThing ที่ไม่ได้ใส่ Attribute TestMethod() เข้าไป
ในหน้าต่าง Test Explorer ก็จะไม่มี Method DoSomeThing แสดงขึ้นมา
จากนั้นให้ลอง Run Test สัก 1 รอบเพื่อทดสอบ โดยคลิกจากหน้าต่าง Test Explorer หรือคลิกจากตัว Test Method ก็ได้
จะพบว่าขึ้นผลการ Run Test คือ Fail ทั้งหมด เพราะเราใส่โค้ดไว้ว่าให้ Assert.Fail() เสมอ
เริ่มแก้ไข Test Method
ทีนี้เราจะเข้ามาถึงส่วนสำคัญ เนื้อๆกันบ้างละ ที่ผ่านมาคือ น้ำเยอะมาก 555
คือเราจะมาแก้ไข Test Method ให้ Test อย่างถูกต้องกัน โดยให้แบ่งเป็น 3 ส่วนคือ
Prepare: ส่วนของการเตรียมข้อมูล คือตั้งค่า เตรียม input environment ต่างๆ
Act: คือการเรียกใช้ Method ที่ต้องการจะ Test
Assert: คือการตรวจสอบค่า โดยจะมี 2 ค่าที่เราสนใจคือ
Expected — คือค่าที่เราคาดหวังจะให้มันเป็น
Actual — คือค่าจริงๆที่ได้จากการ Act ของ Method
ในโค้ดเราจะทำการสร้างตัวแปร Type DamageCaculatorService ที่ชื่อว่า service เอาไว้ก่อน แล้วจึงค่อยไป assign value ใน Test Method ต่อไป
Prepare: ทำการ new service object พร้อมใส่ parameter เข้าไปใน constructor ด้วย
Act: เรียกใช้ method DamageFromNormalAttack พร้อมใส่ parameter ที่ต้องการเข้าไปใน method โดยเก็บผลที่ได้ไว้ในตัวแปร ชื่อ actual
Assert: เราต้องการตรวจสอบว่าค่าจากการ call method ด้วย input ที่เราใส่ไป ว่ามีค่าเท่ากับ value ที่เราคาดหวังให้มันเป็นหรือไม่ จึงใช้คำสั่ง Assert.AreEqual(expected,actual) ทำการตรวจสอบค่า 2 ค่า โดย expected คือ 5 และ actual คือ ค่าที่ได้จากการ call method
จากนั้นลองกด Run Test ดู
จาก Test Result จะพบสีเขียวใสสะอาด แสดงว่า โค้ดของเราทำงานได้อย่างถูกต้องจริงๆ
ลองกับ Test Case ที่หลากหลายขึ้น
ในชีวิตจริงคงไม่มีใครเทสเท่านี้แล้วจบแน่ เพราะ Method นึง มันจะมี input เข้ามาหลากหลายรูปแบบ คราวนี้เรามาลองใส่ input data ที่แล้วอยากจะทดสอบกันดู โดยใช้ Attribute DataRow() เป็น input หลายๆแบบ ให้กับ Test Method แล้วเอาค่านั้นมาใส่ให้กับ method ที่จะ Test อีกทีนึง
จากตัวอย่างจะเห็นว่า DataRow แต่ละชุด มีตัวเลข อยู่ 3 ตัว ซึ่งจะพอดีกับจำนวน parameter ของ Method DamageFromNormalAttackTest พอดี ประกอบไปด้วย
ตัวที่ 1 def คือค่าสำหรับ service class constructor
ตัวที่ 2 atk คือค่าสำหรับ parameter ของ method DamageFromNormalAttack
ตัวที่ 3 expected คือค่าผลลัพธ์ที่เราคาดหวังเมื่อเรา call method ด้วยข้อมูลชุดนี้
จากนั้นลองกด Run Test ดู
จะพบว่ามี case ที่ไม่ผ่านอยู่ 1 case คือ (10,5,0) นั่นคือใน case เมื่อมีการโจมตีมา ถ้าค่า defense ของฝ่ายป้องกันมากกว่า attack ของฝ่ายโจมตี ผลลัพธ์ damage ของฝ่ายป้องกันที่ได้ มันต้องเท่ากับ 0 ก็คือฝ่ายป้องกันจะต้องไม่เจ็บตัวเลย แต่ในผลการเทส ค่า actual ที่ได้มาคือ -5 ซึ่งผิด อาจจะกลายเป็น ฝ่ายป้องกันได้ HP เพิ่ม 5 เพราะ damage ติดลบ ซึ่งไม่ถูกต้อง
จะเห็นได้ว่าจากรณีนี้ เราสามารถหา bug จาก Unit Test ได้แล้ว 1 ตัว !!
เป็นยังไงครับ เริ่มเห็นประโยชน์ของมันกันหรือยัง
ทีนี้พอเราเจอ bug แล้วเราก็มาทำการแก้ไขโค้ดของเรา ให้รองรับตาม Test Case ที่เราเขียนไว้
จากนั้นก็ Build และทำการ Run Test ดูอีกครั้ง
จะพบว่า Test ของเราผ่านหมดแล้ว เย้ๆๆๆๆ !!
Next Step…
จากนั้นเราก็จะเพิ่ม Test Case ไปอีกแล้วก็ค่อยตามไปแก้โค้ด ให้สามารถ Run Test ให้ผ่านให้ได้ แล้วก็ทำวนไปเรื่อยๆเพิ่มไปจนกว่าเราจะมั่นใจว่า มันครอบคลุมการทำงานที่เราต้องการจริงๆ ผลลัพธ์ที่ได้ก็คือ Software เราจะ strong ขึ้นมากๆเลยล่ะครับ
การทำแบบนี้ก็เกือบๆจะเป็น Test Driven Development (TDD) เลยก็คือเขียน Test ไว้ก่อน แล้วตามมาแก้โค้ดให้ Run Test ผ่านทุกข้อนั่นเอง แต่ถ้าจะให้เป็น TDD จริงๆก็คงจะต้องวางแผนกับทีมแล้วก็วาง spec ตกลงกันดีดีก่อน
แต่สำหรับเราได้เท่านี้ก็โอเคแล้วครับ อย่างน้อยโค้ดเราก็ปลอดภัยขึ้นมาอีกเยอะเลยทีเดียว
เดี๋ยวครั้งต่อไป จะมาแชร์เกี่ยวกับ Pattern การเขียน Unit Test ที่ดีแล้วก็วิธีการใช้ data จากหลายๆรูปแบบกันครับ
สำหรับวันนี้ โชคดี ไม่มี bug สวัสดีครับ