یونیت تست در یونیتی (قسمت دوم)

در قسمت قبل با مفاهیم ابتدایی Unit Test و نوشتن تست های ابتدایی در یونیتی آشنا شدیم. در این قسمت قصد دارم تا به صورت پیشرفته‌تری یونیت تست نوشتن رو یاد بگیریم. به خاطر همین خیلی سریع میرم سر اصل و مطلب و معطل نمی‌کنیم.نحوه استفاده از Assembly Definitionاسمبلی دفینیشن ها برای تست منوها و ادیتور کاربرد دارن. برای اینکه با نحوه کارشون آشنا بشیم به این صورت عمل می‌کنیم. یک کلاس C# به نام CustomMenu تو فولدر Main میسازیم و از اتریبیوت MenuItem برای نمایش در منوها استفاده می‌کنیم.using UnityEditor; using UnityEngine; public class : MonoBehaviour { [MenuItem(&quotCustom Menu/Hello World&quot)] public static void HelloWorld() { Debug.Log(&quotHello World!&quot); } }خوب همونطور که میبینید کدی که زدیم باعث میشه یک منو به نام Custom Menu در ادیتور یونیتی تولید بشه و با زدن روی دکمه Hello World کلمه Hello World! در کنسول یونیتی چاپ می‌شه.در پروژه Test Runner رو اجرا کنید و Create EditMode Test Assembly Folder رو بزنید تا فولدری به نام Tests در پروژه تولید بشه.همونطور که می‌بینید فولدر ایجاد می‌شه و یک فایل asmdef در فوبدر Tests هم ایجاد می‌شه.یک کلاس تست تو این فولدر به نام CustomMenuTests می‌سازیم تا تست کلاس CustomMenu رو انجام بده.using System.Collections; using NUnit.Framework; using UnityEngine.TestTools; namespace Tests { public class CustomMenuTests { [Test] public void CustomMenuTestsSimplePasses() { CustomMenu.HelloWorld(); } } }همانطور که میبینید ما یک تست ساده نوشتیم که فقط از کلاس CustomMenu متد HelloWorld رو اجرا می‌کنه.حالا برگردید و تست رو ران کنید.دینگ! ارورچه اتفاقی افتاده!کلاس CustomMenu رو پیدا نکرده.چرا و راه حل چیه؟اینجاست که Assembly Definition به کمک ما میاد. Assembly Definition یک فایل با فرمت .asmdef هست که توی این فایل با فرمت JSON پراپرتی های مربوط به کامپایل تمامی اسکریپت های مربوط به فولدری که asmdef توش هست قرار گرفته.دلیل استفاده از Assembly Definition اینه که وقتی اسکریپتی رو توی فولدر خاصی که Asmdef قرار داره تغییر بدید فقط اسکریپت‌های همون مجموعه کامپایل می‌شن و زمان کامپایل شدن به شدت کاهش پیدا می‌کنه.بدون استفاده از Assembly Definition، یونیتی باید با هر تغییر کوچکی تمامی فایل‌های شما رو مدیریت کنه.به صورت معمول یونیتی تمامی اسکریپت های پروژه رو در Assembly-CSharp.dll مدیریت می‌کنه. این مثال نشون میده به جای اینکه به پنج مجموعه مجزا یعنی Main و Stuff و ... تقسیم بشه همه در Assembly-CSharp مدیریت میشه به این معنا که وقتی Main.dll تغییر میکنه یونیتی نیازی به ریکامپایل کردن Stuff.dll یا بقیه چیزا نداره و خیلی سریع این کار انجام میشه.برمی‌گردیم به مشکلمون.به فولدر Main و جایی که CustomMenu.cs هست می‌ریم و با رایت کلیک یه Assembly Definition تولید می‌کنیم.بعد از اینکار به فولدر Tests می‌ریم و روی فایل Tests.asmdef میزنیم و Assembly خاص خودمون رو بهش اد میکنیماینجا من اسمش رو به همون صورت دیفالت NewAssembly گذاشتم.خوب حالا وقت تست کردنهروی Run All بزنید تا پاس شدن تست رو مشاهده کنید.برای اینکه بفهمیم یونیتی چه Assembly هایی رو استفاده می‌کنه هم می‌تونیم به کلاس CustomMenu بریم و به این شکل کد بزنیم.using UnityEditor; using UnityEditor.Compilation; using UnityEngine; public class CustomMenu : MonoBehaviour { [MenuItem(&quotCustom Menu/Hello World&quot)] public static void HelloWorld() { Debug.Log(&quotHello World!&quot); } [MenuItem(&quotCustom Menu/List Player Assemblies in Console&quot)] public static void PrintPlayerAssemblyNames() { UnityEngine.Debug.Log(&quot== Player Assemblies ==&quot); Assembly[] playerAssemblies = CompilationPipeline.GetAssemblies(AssembliesType.Player); foreach (var assembly in playerAssemblies) { UnityEngine.Debug.Log(assembly.name); } } [MenuItem(&quotCustom Menu/List Editor Assemblies in Console&quot)] public static void PrintEditorAssemblyNames() { UnityEngine.Debug.Log(&quot== Editor Assemblies ==&quot); Assembly[] editorAssemblies = CompilationPipeline.GetAssemblies(AssembliesType.Editor); foreach (var assembly in editorAssemblies) { UnityEngine.Debug.Log(assembly.name); } } [MenuItem(&quotCustom Menu/List Player Without Test Assemblies Assemblies in Console&quot)] public static void PrintPlayerWithoutTestAssembliesAssemblyNames() { UnityEngine.Debug.Log(&quot== Player Without Test Assemblies Assemblies ==&quot); Assembly[] playerWithoutTestAssemblies = CompilationPipeline.GetAssemblies(AssembliesType.PlayerWithoutTestAssemblies); foreach (var assembly in { UnityEngine.Debug.Log(assembly.name); } } }حالا به محیط یونیتی برگردید و از منوی Custom Menu روی List Player Assemblies in console کلیک کنید تا نتیجه رو مشاهده کنید.اگر میخواید اطلاعات کامل تری در این زمینه کسب کنید حتما به داکیومنت خود یونیتی مراجعه کنید.یونیت تست ترکیبی (Combinatorial)اگر قصد دارید به صورت ترکیبی و داده‌های پیش فرض، تست خودتون رو انجام بدید، از این اتریبیوت می‌‎تونید استفاده کنید. روش استفاده هم خیلی ساده است.در فولدر Editor یک فایل یونیت تست به نام MyTest.cs ایجاد کنید و به شکل زیر کد بزنید.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] [Combinatorial] public void CombinatorialTest([Values(1,2)] int a,[Values(&quotA&quot,&quotB&quot)] string b,[Values(true,false)] bool c) { Debug.LogWarningFormat(&quotCombinatorialTest a={0} b={1} c={2}&quot, a, b, c); } } }بعد از ران شدن تست نتیجه زیر قابل مشاهده است.همانطور که میبینید تمامی حالت هایی که ممکن بود، تست شده و ترتیب از چپ و راست تغییر کرده به این صورت که اول 1 و بعد A و سپس True تست شده. در تست دوم 1 A False و ...یونیت تست جفتی (Pairwise)این تست به صورت دوتا دوتا عمل می‌کنه و دیتاها رو به صورت جفتی دریافت می‌کنه. این اتریبیوت برای مواردی استفاده می‌شه که قصد داریم از ازدیاد تست های پی در پی جلوگیری کنیم.حالا مطابق تست قبلی از کلاس MyTest.cs استفاده می‌کنیم و به همان‌صورت عمل می‌کنیم تا تفاوت بین این اتریبیوت با ترکیبی مشخص بشه.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] [Combinatorial] public void CombinatorialTest([Values(1,2)] int a,[Values(&quotA&quot,&quotB&quot)] string b,[Values(true,false)] bool c) { Debug.LogWarningFormat(&quotCombinatorialTest a={0} b={1} c={2}&quot, a, b, c); } [Test] [Pairwise] public void PairwiseTest([Values(1, 2)] int a, [Values(&quotA&quot, &quotB&quot)] string b, [Values(true, false)] bool c) { Debug.LogWarningFormat(&quotPairwiseTest a={0} b={1} c={2}&quot, a, b, c); } } }نتیجه هم بعد از تست به این صورت به نمایش در میاد.همونطور که می‌بینید 4 تست انجام شده و تفاوتش با حالت قبل که همه حالات رو در نظر می‌گرفت کاملاً مشهوده.این روش به این صورت کار می‌کنه که برای مثال 1, “A”, True در تست ها قرار ندارد چون “A” ,True در تست آخر 2, “A”, True مورد تست قرار گرفته و از تکرار تست “A”, Trueجلوگیری شده. این روش جفت، جفت تست ها رو چک می‌کنه تا از ازدیاد یونیت تست جلوگیری کنه.یونیت تست پی در پی (Sequential)در این اتریبیوت هم به ترتیب یعنی ابتدا داده های اول و سپس داده های دوم مورد تست قرار می‌گیره.برای مشاهده این روش هم به شکل زیر عمل می‌کنیم.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] [Combinatorial] public void CombinatorialTest([Values(1,2)] int a,[Values(&quotA&quot,&quotB&quot)] string b,[Values(true,false)] bool c) { Debug.LogWarningFormat(&quotCombinatorialTest a={0} b={1} c={2}&quot, a, b, c); } [Test] [Pairwise] public void PairwiseTest([Values(1, 2)] int a, [Values(&quotA&quot, &quotB&quot)] string b, [Values(true, false)] bool c) { Debug.LogWarningFormat(&quotPairwiseTest a={0} b={1} c={2}&quot, a, b, c); } [Test] [Sequential] public void SequentialTest([Values(1, 2)] int a, [Values(&quotA&quot, &quotB&quot)] string b, [Values(true, false)] bool c) { Debug.LogWarningFormat(&quotSequentialTest a={0} b={1} c={2}&quot, a, b, c); } } }و نتیجه تست به صورت زیر خواهد بود.تست تصادفی (Random)برای تست به صورت تصادفی به صورت زیر عمل می‌کنیم.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] public void RandomTest([Random(0,100,5)] int a) { Debug.LogWarningFormat(&quotRandomTest a={0}&quot, a); } } }در این کد 5 عدد بین 0 تا 100 انتخاب کردیم و چاپ میکنیمتوجه داشته باشید که با هر بار ران کردن تست نتیجه متفاوتی دریافت خواهید کردتست دامنه ای (Range)برای اینکه دامنه ای از اعداد رو مورد تست قرار بدیم می‌تونیم از اتریبیوت Range استفاده کنیم.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] public void RangeTest([NUnit.Framework.Range(0,100,5)] int a) { Debug.LogWarningFormat(&quotRangeTest a={0}&quot, a); } } }در این تست از عدد 0 تا 100، 5تا 5 تا جلو می‌ریم و تست می‌کنیم.یونیت تست با TestCaseاگر بخوایم موارد مختلف از اعداد رو به صورت پیش‌فرض به یونیت تست بدیم می‌تونیم از اتریبیوت TestCase استفاده کنیم. روش کار هم به صورت زیر هست.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] [TestCase(1, 2, 2)] [TestCase(2, 5, 10)] [TestCase(7, 8, 56)] public void TestCaseTest(int a, int b, int c) { Debug.LogWarningFormat(&quotTestCaseTest {0}x{1}={2}&quot, a,b,c); Assert.AreEqual(a * b, c); } } }در تست بالا 3 کیس مختلف به تستمون دادیم و قصد داریم مطمئن شیم که عدد اول ضرب در عدد دوم به ما عدد سوم رو می‌ده.بعد از ران شدن تست تمامی کیس‌های ما مورد تست قرار می‌گیرن و نتیجه پاس شدنشون رو مشاهده می‌کنیم.یونیت تست با TestCaseSourceما می‌تونیم برای دادن کیس تست از اتریبیوت TestCaseSource هم استفاده کنیم. این اتربیوت به ما اجازه میده تا بتونیم از یک سورس مشخص کیس‌ها رو دریافت کنیم. برای استفاده از این اتریبیوت هم به روش زیر عمل می‌کنیم.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] [TestCaseSource(&quotCaseSources&quot)] public void TestCaseSourceTest(int a, int b, int c) { Debug.LogWarningFormat(&quotTestCaseSourceTest {0}x{1}={2}&quot, a, b, c); Assert.AreEqual(a * b, c); } static object[] CaseSources = { new object[] {1,2,2}, new object[] {2,5,10}, new object[] {7,8,56}, }; } }در این حالت یک آرایه از آبجکت ها به نام CaseSources تولید کردیم و درون این آرایه، آرایه هایی از آبجکت نوشتیم تا مورد تست قرار بگیرن.نتیجه هم به صورت بالا قابل مشاهده است.اتریبیوت Theory و DatapointSourceمیشه گفت اتریبیوت Theory یکی از پرکاربرترین و محبوب ترین اتربیوت‌های یونیت تست به شمار می‌ره.برای استفاده از این اتریبیوت‌ها به صورت زیر عمل می‌کنیم.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [DatapointSource] private float[] values = new float[] { -1f, 0f, 1f, 20f, 8f, 7f, 15f, 10f }; [Test] [Theory] public void TheoryTest(float value) { Assume.That(value >= 0); Debug.LogWarningFormat(&quotTheoryTest {0}&quot, value); float root = Mathf.Sqrt(value); Assert.That(root >= 0); Assert.That(root * root, Is.EqualTo(value).Within(0.000001f)); } } }در این تست یک دیتاپوینت داریم که سورس value های ماست و در تستمون گفتیم اگر دیتایی که وارد میشه از 0 بزرگتر بود اجازه ورود به تست رو داشته باشه. در خط های بعدی جذر value رو حساب کردیم و دوباره به توان 2 رسونیدم تا مطمئن بشیم که مربع مجذور عدد با خود عدد برابره.نتیجه تست هم به این صورت قابل مشاهده است.همانطور که میبینید عدد 1- اجازه وارد شده به تست رو نداشته و تستش شکست خورده.اتریبیوت ValueSourceاین اتریبیوت هم به ما اجازه میده تا سورس Value ها رو از جای مشخصی به تستمون بدیم.برای این کار به روش زیر عمل می‌کنیم.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { private static float[] values = new float[] { -1f, 0f, 1f, 20f, 8f, 7f, 15f, 10f }; [Test] public void ValueSourceTest([ValueSource(&quotvalues&quot)] float value) { Assume.That(value >= 0); Debug.LogWarningFormat(&quotValueSourceTest {0}&quot, value); float root = Mathf.Sqrt(value); Assert.That(root >= 0); Assert.That(root * root, Is.EqualTo(value).Within(0.000001f)); } } }توجه داشته باشید که در این روش حتما باید سورس ما Static باشه.نوشتن اتریبیوت‌های سفارشی (Custom)برای اینکه اتریبیوت‌های خاص خودمون رو بنویسیم باید از کلاس NUnit.Framework.PropertyAttribute استفاده کنیم.برای این کار به روش زیر عمل می‌کنیم.using NUnit.Framework; using UnityEngine; namespace Tests { public class MyTest { [Test] [CustomTestAttribute(&quotHello&quot)] public void CustomTestAttributeTest() {} } public class CustomTestAttribute : NUnit.Framework.PropertyAttribute { public CustomTestAttribute(string data) : base() { Debug.LogWarningFormat(&quotdata {0}&quot, data); } } }در این اتربیوت ما یک دیتا به عنوان ورودی در کانستراکتور کلاس دریافت میکنیم و چاپ میکنیم.خوب تا اینجای کار می‌شه گفت تمامی مواردی برای یونیت تست نوشتن در یونیتی لازمه رو فرا گرفتیم و برای حرفه ای تر شدن کار میشه از CLI یونیتی برای تست کردن در محیط بیرون نرم‌افزار استفاده کرد.امیدوارم این مطلب مورد توجه شما قرار گرفته باشه.