ทำความรู้จัก Test Runner ของ Unity
รู้หรือไม่ว่า Test Runner (ร่างสุดยอด) เป็นฟีเจอร์ใหม่ของ Unity 5.6? หลายๆคนอาจจะคุ้นๆว่ามันมีมานานแล้วไม่ใช่เหรอ คือก่อนหน้านี้มันเคยมั่วมานานมาก
พาเริ่มต้นเขียน test ใน Unity 5.6+ ตั้งแต่ต้น พร้อมพูดไปด้วย
กำลังอัดวิดิโออยู่ ถ้าไม่ขี้เกียจเดี๋ยวคงมา
ประวัติศาสตร์
ตอนแรก ต้องโหลด Unity Test Tools จาก Asset Store แล้วทำ Unit Test ได้ (รันจาก editor แล้วผลออกมาเลย) แล้วก็ทำ integration test ได้ โดยต้องสร้าง scene พิเศษขึ้นมาเอง แล้วเอาสคริปต์กำกับไปแปะ แล้วพอรัน มันจะค่อยๆทำตามโดยที่ห้ามทำอะไรกับ GameObject ที่มันกำกับเทสอยู่ไม่งั้นเทสจะหยุด 555
Unity-Technologies / UnityTestTools
This is the repository for the Unity Test Tools project.bitbucket.org
Unity-Technologies / UnityTestTools / wiki / IntegrationTestsRunner - Bitbucket
A Test Object is a GameObject on the scene that has TestComponent attached to it. Everything under the Test Object in…bitbucket.org
หน้าตา integration test ของเดิมประมาณนี้
ปัญหาที่ตามมาก็คือถ้าเขียน load scene ไว้ในเทสโดยใช้ฟังก์ชั่นเดียวกับในเกมอาจจะทำให้ของหายหมดเพราะดันใช้ LoadSceneMode.Single ต้องมานั่งแยกเวอร์ชั่นเทสให้มันเป็น additive ด้วยเหตุผลที่อยากจะรักษาของเทสไว้ ยากกว่าตอนที่จะเดินทางจาก scene ที่ว่าไป scene ถัดไป ในเกมปกติกะว่าจะให้ .Single ให้มันล้างบางให้สะอาดไง ทีนี้ใช้ .Single ก็เทสหาย จะใช้ .Additive ก็เหลือ scene เก่า ทำให้ต้องมานั่ง .Unload ทุก scene ยกเว้น integration scene เอาเองถึงจะได้ฟังก์ชั่นเหมือน .Single (ผมมีเมธอด .UnloadExcept เขียนไว้เพื่อทำ integration test โดยเฉพาะ)
ต่อจากนั้นไม่นานใน Unity 5. เท่าไหร่ไม่รู้ มันก็มีเมนู Test Runner ขึ้นมาตรงนี้
แล้ว unit test ก็โดนเอาออกจาก Unity Test Tools แต่ถ้าอยากเทส Integration ก็ยังต้องเก็บ Unity Test Tools ไว้อยู่ กลายเป็นเกมมีระบบเทส 2 อันแยกกัน
ร่างสุดยอด
ใน Unity 5.6 ในที่สุด integration test ก็ไปอยู่ใน Test Runner ในชื่อ Play mode test ในที่สุดก็เอา Unity Test Tools ออกจากโปรเจคได้ แต่การเขียนเทสก็เปลี่ยนไปด้วย ไปอ่านเอาเองที่
Unity - Manual: Unity Test Runner
The Unity Manual helps you learn and use the Unity engine. With the Unity engine you can create 2D and 3D games, apps…docs.unity3d.com
หลักๆก็คือไม่ต้องมี scene เฉพาะเวลาจะทำ play mode test แล้ว มันเสกให้เราเอง เมธอดไหนอยากให้เป็นเทสก็แค่ใส่ [Test] (แบบเดิม ใช้ได้กับ unit/edit mode test) กับ [UnityTest] (แบบใหม่ ใช้ yield ได้ ใช้กับ integration/play mode test)
ใครยังไม่เข้าใจเรื่อง yield กับ IEnumerator ลองดูตัวอย่างนี้ได้ครับ (รันเทสแล้วจะ log 1 2 3 4 เรียงกันทั้งคู่ แล้วเทสถึง pass)
ทดสอบ Attribute เดิมของ NUnit
ผลก็คือยังใช้ได้เหมือนเดิม
ทดสอบ Attribute ใหม่
เห็นในนี้ https://docs.unity3d.com/Manual/testing-editortestsrunner.html มีแนะนำให้ implement interface IPrebuildSetup
ผลคือ ทำไมมันไม่เห็นรันเลย
ลองแปะ attribute เพิ่มที่เขาบอกว่าไม่ต้องแปะก็ได้
มันก็ยังไม่รันอยู่ดี
สรุปคือใช้อันเดิมไปนั่นแหละ
Integration Test ต้องแก้อะไรมั่ง
ตอนแรกมันทำสไตล์เอา MonoBehaviour ไปแปะ game object ใน integration scene แล้วเทสก็จะเริ่มจาก Start ตามปกติ
คราวนี้มันไม่มี scene ให้แปะแล้ว และเทสจะเริ่มจากเมธอดที่ใส่ [UnityTest] ไว้แทน ก็แค่ลบ : MonoBehaviour inheritance ออกไปแล้วก็เอา [UnityTest] ไปแปะไว้ที่ชอบๆ ออ แล้วก็ไฟล์นี้ห้ามเอาไว้ในโฟลเดอร์ Editor ไม่งั้นมันจะไปโผล่ใน edit mode test
จริงๆถ้าไม่ลืมเอาออกจะเจอแบบนี้ เพราะตัวเทสจะพยายาม new คลาสเทสของเรา
เวลาเริ่มเทส Scene เราจะกลายเป็นแบบนี้ครับ
อันนั้นคือ Scene ใหม่ที่ Unity เสกมาให้ พร้อม game object ปริศนา 1 อัน ซึ่งเป็นตัวที่รัน coroutine เมธอดที่เราแปะ [UnityTest] ไว้ ดังนั้นระวังด้วยถ้าเทสเรามี LoadScene แบบล้างบางเมื่อไหร่ อันนี้หายไป ก็จะกลายเป็น coroutine หยุด 55555
แบบภาพนี้ Debug.Log("2") จะไม่รันเพราะตัวรัน coroutine ระเบิดเละไปแล้ว และ Assert ก็ไปไม่ถึงด้วย กลายเป็น load scene เสร็จก็ค้างอยู่แค่นั้น จบ (แถมถ้ากดออก play mode ตอนนี้ scene ขยะจะคาอยู่ใน project เราให้เราต้องเก็บกวาดเอง)
ดังนั้นเรามีวิธีโกงความตายโดยการคุ้ยหาอีตัว Test Runner นั่นมาเข้า DontDestroyOnLoad ซึ่งใช้ได้แม้จะไม่อยู่ใน MonoBehaviour ก็ตาม ท่าก็ประมาณนี้
เพียงเท่านี้ไอ้ตัวนั้นมันก็จะถึก ไม่โดนล้างบาง
ท่า NUnit ที่ใช้ได้
[SetUp] กับ [TearDown] ใช้ได้เหมือนเดิม อ้อ แล้วก็ ใช้หลายอันก็ได้ครับโดยลำดับคือ จากบนลงล่าง และจาก class แม่มาลูก (ดังนั้นทำ [Setup] ที่ใช้ด้วยกันหลายๆเทสไว้เป็นคลาสแม่ อะไรงี้ก็ได้)
[TestCaseData] ใช้ได้กับ [Test](ใช้กำหนด parameterized test จากคลังข้อมูลแยก)
Assert สไตล์ NUnit 3.0 (Constraint model) ใช้ได้ ( Assert.That(____ , Is.GreaterOrEqual(____)); ) อันเก่าก็ยังใช้ได้ ( Assert.GreaterOrEqual(___, __); )
ถ้าลองใช้กับ [UnityTest] จะเกิดเออเร่อปริศนา
ทาง Unity บอกว่าถ้าอยากทำ parameterize กับ [UnityTest] ตอนนี้ต้องใช้สไตล์ [ValueSource] อย่างเดียว ท่าในการทำคือแบบนี้
เฮ่เฮ้
จำไว้ด้วยว่าถ้าใช้ ValueSource หลายๆ parameter แล้วมันจะไล่จับคู่ให้ทุกกรณีเลยน่ะครับ ออแล้วก็ source ข้อมูลต้องเป็น static variable/method/property น่ะ แต่จะ private ก็ได้ (แต่เมธอดเทสต้องไม่ private น่ะ)
[TestFixtureData] ใช้ไม่ได้ (ให้ NUnit เสก test instance เพิ่มอีกหลายชุดโดยให้แต่ละชุดมี constructor ต่างๆกันได้ แปะใส่คลาส ไม่ใช่ใส่เทส)
MonoBehaviourTest
เขามีคลาสวิเศษ MonoBehaviourTest<T> มาให้ ซึ่งเขาว่าถ้า new แล้ว yield return ใส่มัน มันจะ instantiate ตัวคลาส T ให้แล้วก็รัน Awake Start Update ประหนึ่งเป็นของชิ้นนึงจริงๆ แล้วจะ yield รอถึงเมื่อไหร่ คลาส T ของเราที่ว่าต้อง implement IMonoBehaviourTest ด้วย จากนั้นก็ implement property bool IsTestFinished เอาไว้บอกมัน ทีนี้ข้างใน mono ของเรานี้ก็สามารถ StartCoroutine อะไรงี้ได้ด้วย หรือเทสอะไรอื่นๆตามชอบได้ ปกติ MonoBehavior นี้ new ไม่ได้ คิดว่านี่คงมีประโยชน์พอควรสำหรับเทสแค่ MonoBehavior อันเดียวเดี่ยวๆว่าทำงานถูกมั้ย
yield return new MonoBehaviourTest<MyMonoBehaviourTest>();
public class MyMonoBehaviourTest : MonoBehaviour, IMonoBehaviourTest {
private int frameCount;
public bool IsTestFinished { get { return frameCount > 10; } }
void Update() { frameCount++; }
}
ปรับแต่งรายการ Test Case
ถ้าเป็น [Test] เฉยๆ เท่าที่ลองสามารถใส่ Descriptionใน attribute ได้ มันจะไปขึ้นเล็กๆตรง console ของ Test Runner แล้วก็ถ้าใส่ TestName จะเปลี่ยนชื่อได้ จากที่แต่ก่อนอิงตามชื่อ method ส่วนอันอื่นๆอย่าง Author หรือ Category ไม่มีผลอะไรกับ Unity
แต่ถ้าเป็น [UnityTest] อย่างว่า ใช้ได้แค่ [ValueSource] ก็เลยเปลี่ยนชื่อไม่ได้ ถ้า args เป็นอะไรที่ดูง่ายเช่น string หรือ int ก็โอเคอยู่ แต่ถ้าเป็น object อาจจะดูไม่ค่อยดี เพราะกลายเป็นเหมือนๆกันหมดเลย
แต่เราสามารถ override ToString ของ class เราเพื่อขุดเอาข้อมูลของคลาสมาแสดง ก็พอแก้ขัดได้ครับ
รันในเครื่องจริงได้ด้วยนะ!
จะเห็นว่ามีปุ่ม Run all in player ที่มุมขวาบน กดปุ้บเครื่องมือถือที่ต่อกับคอมอยู่ ก็จะโดน Unity โยน Scene เสกใหม่ให้ไปรันทันที เหมาะจะเทสอะไรที่มันเวิคบนเครื่องจริงเท่านั้น เช่น Firebase หรือ Ads อะไรงี้ครับ
แต่มัน Run all บางอันเออเร่อบนเครื่องด้วย
ใช้ attribute UnityPlatform แบบนี้แล้วจะยกเว้นได้ครับ เช่น
แบบนี้จะกลายเป็นรันบนเครื่องคอมไม่ได้ เทสกลายเป็น skip ถ้าจะให้มันไปรันบนมือถือแล้ว skip ก็เปลี่ยนเป็น exclude หรือว่าจะใส่ RuntimePlatform เป็นของคอมๆอย่างเดียวก็ได้ หรือถ้าใครขี้เกียจใส่ของใน attribute แบบนั้นก็ทำ subclass มาให้ความสะดวกตัวเองแบบนี้ก็ได้ครับ (เพราะ attribute เป็น compile time เราจะใช้ตัวแปรไม่ได้นะ)
แล้วก็แปะใส่ TestFixture (หัวคลาส) ได้เลยจะได้คลุมไปให้หมดทุกเทส
หรือถ้าอยากยกเลิกทุกที่ชั่วคราว ใช้ [Ignore("reason")] ได้ครับซึ่ง ignore แค่ TestCase หรือทั้ง Fixture เลยก็ได้เหมือนกัน
สิ่งที่จะเพิ่มในอนาคต
มีลุงคนนึงจาก Unity มาบอกว่าเดี๋ยวคงจะเพิ่มระบบ integration test แบบตาม scene กลับมาทีหลัง ตอนนี้ก็เรียกคำสั่งโหลด scene เองไปก่อน https://forum.unity3d.com/threads/integration-testing-support-in-5-6.460745/#post-2991665
อยากเทพต้องไปเรียน NUnit
ที่นี่เลย
nunit/docs
docs - Documentation for all active NUnit projectsgithub.com