วิชา 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

Class ง่ายๆ 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”

Create Unit Tests

จะขึ้นหน้าต่าง Popup Template สำหรับสร้าง Unit Test ขึ้นมาให้เราเลือก โดยมีรายละเอียดดังนี้

Template Wizard

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)

New Test Project

ทำการตั้งชื่อ Test Project

Name

จะได้ Test Project ตามที่เราสร้างไว้

Test Project

จากนั้นเราจะทำการ Add Reference Project ที่ต้องการ Test ไปที่ Test Project

Add Project Reference

เลือก Project ที่ต้องการ

เลือก Project

จะได้ Test Project ที่พร้อมใช้งานแล้ว

หน้าตา Test Project ที่พร้อมใช้งาน

โดยทั้ง 2 วิธีควรจะมี Package อยู่ 4 ตัว คือ

coverlet.collector
Microsoft.NET.Test.Sdk
MSTest.TestAdapter
MSTest.TestFramework

ตามรูปข้างล่าง

Package ที่ต้องมีเมื่อสร้าง Project เสร็จ

จากนั้นเราจะมาสร้างไฟล์ให้หน้าตาเหมือนที่ generate มาจาก Template Wizard

เริ่มจากลบไฟล์ UnitTest1.cs ออก จากนั้นสร้างโฟลเดอร์ชื่อ Services และสร้างไฟล์ชื่อ DamageCaculatorServiceTests.cs แล้วใส่โค้ดนี้ลงไป

ตอนนี้เราจะได้ Test Project ที่มีโค้ดเหมือนกับ Project ที่สร้างโดย Template Wizard แล้ว

มันคืออะไรกันนะ งงเลย

มาอธิบายโค้ดกันเล็กน้อย ไปที่ไฟล์ DamageCaculatorServiceTests.cs เราจะเห็นโค้ดสไตล์แบบนี้ใน Test Project เป็น Pattern ปกติ

หน้าตาแรกเริ่มของ Test Class และ Test Method

เริ่มจากจะเห็นได้ว่าบนหัว 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 ทั้งหมดใน Solution

จะเจอ Test Class และ Test Method ที่เราสร้างขึ้นมา ในที่นี้เราลองใส่ Method DoSomeThing ที่ไม่ได้ใส่ Attribute TestMethod() เข้าไป

ในหน้าต่าง Test Explorer ก็จะไม่มี Method DoSomeThing แสดงขึ้นมา

จากนั้นให้ลอง Run Test สัก 1 รอบเพื่อทดสอบ โดยคลิกจากหน้าต่าง Test Explorer หรือคลิกจากตัว Test Method ก็ได้

ลองกด Run Test

จะพบว่าขึ้นผลการ Run Test คือ Fail ทั้งหมด เพราะเราใส่โค้ดไว้ว่าให้ Assert.Fail() เสมอ

Test Result จากการ 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 ต่อไป

แก้ไข code ใน 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 Result จะพบสีเขียวใสสะอาด แสดงว่า โค้ดของเราทำงานได้อย่างถูกต้องจริงๆ

ลองกับ Test Case ที่หลากหลายขึ้น

ในชีวิตจริงคงไม่มีใครเทสเท่านี้แล้วจบแน่ เพราะ Method นึง มันจะมี input เข้ามาหลากหลายรูปแบบ คราวนี้เรามาลองใส่ input data ที่แล้วอยากจะทดสอบกันดู โดยใช้ Attribute DataRow() เป็น input หลายๆแบบ ให้กับ Test Method แล้วเอาค่านั้นมาใส่ให้กับ method ที่จะ Test อีกทีนึง

เพิ่มชุดของข้อมูล Test Case ด้วยDataRow Attribute

จากตัวอย่างจะเห็นว่า DataRow แต่ละชุด มีตัวเลข อยู่ 3 ตัว ซึ่งจะพอดีกับจำนวน parameter ของ Method DamageFromNormalAttackTest พอดี ประกอบไปด้วย

ตัวที่ 1 def คือค่าสำหรับ service class constructor
ตัวที่ 2 atk คือค่าสำหรับ parameter ของ method DamageFromNormalAttack
ตัวที่ 3 expected คือค่าผลลัพธ์ที่เราคาดหวังเมื่อเรา call method ด้วยข้อมูลชุดนี้

จากนั้นลองกด Run Test ดู

Test Result หลังจากเพิ่ม Test Case เข้าไปหลายๆ Test Case

จะพบว่ามี case ที่ไม่ผ่านอยู่ 1 case คือ (10,5,0) นั่นคือใน case เมื่อมีการโจมตีมา ถ้าค่า defense ของฝ่ายป้องกันมากกว่า attack ของฝ่ายโจมตี ผลลัพธ์ damage ของฝ่ายป้องกันที่ได้ มันต้องเท่ากับ 0 ก็คือฝ่ายป้องกันจะต้องไม่เจ็บตัวเลย แต่ในผลการเทส ค่า actual ที่ได้มาคือ -5 ซึ่งผิด อาจจะกลายเป็น ฝ่ายป้องกันได้ HP เพิ่ม 5 เพราะ damage ติดลบ ซึ่งไม่ถูกต้อง

จะเห็นได้ว่าจากรณีนี้ เราสามารถหา bug จาก Unit Test ได้แล้ว 1 ตัว !!

เป็นยังไงครับ เริ่มเห็นประโยชน์ของมันกันหรือยัง

ทีนี้พอเราเจอ bug แล้วเราก็มาทำการแก้ไขโค้ดของเรา ให้รองรับตาม Test Case ที่เราเขียนไว้

เพิ่มโค้ดให้รองรับ Test Case

จากนั้นก็ Build และทำการ Run Test ดูอีกครั้ง

Test Result หลังจากแก้ไขโค้ดแล้ว

จะพบว่า Test ของเราผ่านหมดแล้ว เย้ๆๆๆๆ !!

Next Step…

จากนั้นเราก็จะเพิ่ม Test Case ไปอีกแล้วก็ค่อยตามไปแก้โค้ด ให้สามารถ Run Test ให้ผ่านให้ได้ แล้วก็ทำวนไปเรื่อยๆเพิ่มไปจนกว่าเราจะมั่นใจว่า มันครอบคลุมการทำงานที่เราต้องการจริงๆ ผลลัพธ์ที่ได้ก็คือ Software เราจะ strong ขึ้นมากๆเลยล่ะครับ

การทำแบบนี้ก็เกือบๆจะเป็น Test Driven Development (TDD) เลยก็คือเขียน Test ไว้ก่อน แล้วตามมาแก้โค้ดให้ Run Test ผ่านทุกข้อนั่นเอง แต่ถ้าจะให้เป็น TDD จริงๆก็คงจะต้องวางแผนกับทีมแล้วก็วาง spec ตกลงกันดีดีก่อน

แต่สำหรับเราได้เท่านี้ก็โอเคแล้วครับ อย่างน้อยโค้ดเราก็ปลอดภัยขึ้นมาอีกเยอะเลยทีเดียว

เดี๋ยวครั้งต่อไป จะมาแชร์เกี่ยวกับ Pattern การเขียน Unit Test ที่ดีแล้วก็วิธีการใช้ data จากหลายๆรูปแบบกันครับ

สำหรับวันนี้ โชคดี ไม่มี bug สวัสดีครับ

--

--

Aekasit Nakarad (IIwuDaFFK)
Arcadia Software Development

ติ่งเป็นงานหลัก devเป็นงานรอง ส่วนใหญ่จะ ITsupport เลี้ยงดู carry ให้เติบโต