در قسمت قبل به صورت یک مثال ساده برای یک کلاس تست نوشتیم و خطا های که وجود داشت را رفع کردیم در این مقاله میخواهیم ادامه مقاله قبل جزیی تر xunit را برسی کنیم.قبل از شروع افزونه زیر را در vs code نصب کنید و پروژه را باز کنید.
.NET Core Test Explorer
اگه افزونه به درستی نصب شده باشد تصویری شبیه بالا باید داشته باشید. وقتی بر روی دکمه run در بالا گوشه راست کادر کلیک کنید تست ها به صوت گرافیکی اجرا میشوند همچنین در قسمت کد میتوانید در مد دیباگ یا به صوت جدا تست را اجرا کنید. نکته دیگر اگر از ویژگی inline data استفاده کرده باشید به ازای هر inline data به صورت یک منو کشویی یک تست ایجاد کرده است.اگر با ویژوال استادیو کا میکنید از این لینک میتوانید افزونه مربوطه را دانلود کنید .البته افزونه های مختلفی بسته به نیاز میتوانید استفاده کنید
دسته بندی تست ها
فرض کنید جدا از چهار عمل اصلی دون CalculatorTest دو تابع برای تشخیص اول و زوج بودن عدد نوشته ایم ، توابع تست را هم طبق چیزی که یاد گرفتیم پیاده سازی میکنیم .حالتی را تصور کنید که توابع تست روز به روز زیاد تر می شوند و بخواهیم فقط یک بخش از کلاس را تست کنیم! برای اینکه درون کلاس دسته بندی داشته باشیم از ویژگی Trait استفاده میکنیم .در این مثال برای چهار عمل اصلی یک دسته بندی با نام BaseAction ایجاد میکنیم و میخواهیم توابع تست که محدود به این دسته بندی هستند را اجرا کنیم .
[Trait([name],[value])]
برای اجرا تست از پارامتر filter استفاده میکنیم و میتوانیم شرط های مختلفی اعمال کنیم
dotnet test --filter "Category=BaseAction"
البته اگه با ویژوال استادیو کار میکنید تنظیمات بیشتری را میتوانید اعمال کنید
؛ log گرفتن در xunit :
؛log گرفتن جزو یکی از کارای متداول برنامه نویسا و جز یکی از قوانین clean code هم به شمار میاید. برای log گرفتن از اینترفیس ITestOutputHelper استفاده میکنیم ، که فقط کافیست به کلاس مربوطه inject کنید.
private readonly ITestOutputHelper output; public MyTestClass(ITestOutputHelper output) { this.output = output; } [Fact] public void MyTest() { var temp = "my class!" output.WriteLine("This is output from {0}", temp); }
به اشتراک گزاری object در کلاس های تست
هنگامی که تست ها را اجرا کنید شاید پی برده باشید مدت زمانی که طول می کشد تست اجرا شود از حد انتظار بیشتر است. اگه به کدها دقت کرده باشید در هر تابع تست یک instance از کنترلر ساخته میشود . به طور مثال اگر 50 تابع تست داشته باشیم 50 با کنترلر ساخته و از بین میرود که هزینه زمانی زیادی بر روی سیستم دارد. مشکل فوق را در قالب یک مثال بیان میکنیم. وارد پروژه api شوید و درون model یک کلاس به نام SuperHeavyWeight ایجاد کنید. اگر وارد ریپازیتوری شده باشید در سازنده کلاس یک Thread.Sleep دو ثانیه قرار دادیم که بتوانیم یک تست سنگین را شبیه سازی کنیم. وارد پروژه تست شوید و کلاس تست SuperHeavyWeight به نام SuperHeavyWeightTests ایجاد کنید. توابع تست مربوطه به هر کدام را نیز پیاده سازی کنید
public class SuperHeavyWeightTests { private readonly SuperHeavyWeight _sut; private readonly ITestOutputHelper output; public SuperHeavyWeightTests(ITestOutputHelper output) { this.output = output; output.WriteLine("run constructor"); _sut = new SuperHeavyWeight(); } [Fact] public void CalculationOne_WhenCalled_ReturnsTheCorrectResult() { var result = _sut.CalculationOne(2); Assert.Equal(Math.PI * 2, result); } . . .
اگه دقت کرده باشید این بار به صورت سراسری object را ایجاد کرده ایم همچنین درون سازنده یک log مبنی بر اجرا سازنده نوشته ایم. انتظار میرود که هنگام اجرا کردن تست یک بار Thread.Sleep اجرا شود سپس تست های دیگه اجرا شوند. با دستور زیر تست را مجددا اجرا کنید
dotnet watch test --logger:"console;verbosity=detailed"
با توجه به تست بالا سازنده کلاس 3 بار اجرا شده و به ازای هر بار تابع تست یک بار Thread.Sleep اجرا شده است!!!برای حل کردن مشکل فوق از IClassFixture استفاده میکنیم. یک کلاس جدید به نام SuperHeavyWeightFixture ایجاد کنید .
public class SuperHeavyWeightFixture : IDisposable { public SuperHeavyWeight Sut { get; private set; } public SuperHeavyWeightFixture() { Sut = new SuperHeavyWeight(); } public void Dispose() { Sut.Dispose(); } }
در کلاس بالا یک object از کنترلر مورد نظر ایجاد میکنیم همچنین با استفاده از متد Dispose بعد از اتمام کار کلاس را از بین میبریم. یک کلاس جدید دیگر به نام SuperHeavyWeightTestsWithFixtute ایجاد کنید .این کلاس کلاس تست ما خواهد بود
public class SuperHeavyWeightTestsWithFixtute : IClassFixture<SuperHeavyWeightFixture> { private readonly SuperHeavyWeightFixture _fixture; private readonly ITestOutputHelper output; public SuperHeavyWeightTestsWithFixtute(SuperHeavyWeightFixture fixture, TestOutputHelper output) { _fixture = fixture; this.output = output; output.WriteLine("run constructor method 2"); } [Fact] public void CalculationOne_WhenCalled_ReturnsTheCorrectResult() { var result = _fixture.Sut.CalculationOne(2); Assert.Equal(Math.PI * 2, result); } . . .
کلاس بالا از اینترفیس IClassFixture ارث بری کرده و در متد جنریک ان SuperHeavyWeightFixture (قبلا ایجاد کردیم)را به ان پاس میدهیم .همچنین SuperHeavyWeightFixture را درون سازنده inject میکنیم و هنگام فراخوانی از کلاس inject شده استفاده میکنیم
var result = _fixture.Sut.CalculationOne(2);
یک بار دیگر با دستور dotnet watch test --logger:"console;verbosity=detailed برنامه را اجرا کنید
همانطور که در تصویر بالا ملاحظه میکنید در روش فعلی زمان اجرای تست از یک مقدار شروع شده و در تست های بعد به کمتر از 1 میلی ثانیه رسیده است.میتوانید برای پیاده سازی آسانتر کلاس fixture را به صورت جنریک پیاده سازی کنید.
؛ data driven tests :
به تستی که بتوان با مجموعه داده های مختلف تست را انجام داد DDT گفته میشود .داده های مختلف میتوان از یک منبع خارجی از یک فایل ،دیتابیس و ... باشد. میتوان گفت inline data ساده ترین حالا از DDT میباشد که میتوان به صورت مجموعه ای inline data های مختلف باشد ولی همچنان inline data دارای یکسری محدودیت ها است به طور مثال وقتی داده ها زیاد شود کد شکل کثیفی خواهد گرفت و نمیتوان داده را از یک منبع خارجی دیگر بگیرد. وارده پروژه اصلی شوید و درون Calculator یک تابع جدید ایجاد کنید
public int Calculate(int n1, int n2, Action action) { int result = 0; if (action == Action.Division) result = n1 / n2; else if (action == Action.Multiplication) result = n1 * n2; else if (action == Action.Sub) result = n1 - n2; lse if (action == Action.Sum) result = n1 + n2; return result; } } public enum Action { Sum = 0, Sub = 1, Division = 2, Multiplication = 3 }
تابع بالا 3 پارامتر ورودی گرفته و بسته به پارامتر سوم بر روی مقادیر n1,n2 محاسبات (*/-+) را انجام میدهد .در پروژه تست یک کلاس به نام CalculatorData ایجاد کنید .قصد دایم داده را درون این کلاس به تابع تست پاس بدهیم.
public class CalculatorData { // Sum = 0, // Sub = 1, // Division = 2, // Multiplication = 3 public static IEnumerble<object[]> GetNumbers { get { //n1,n2,action,result yield return new object[] { 5, 5, 1, 100 }; yield return new object[] { 5, 4, 3, 20 }; yield return new object[] { 50, 10, 2, 5 }; yield return new object[] { 70, 50, 0, 20 }; } } . . . }
با استفاده از یک Property استاتیک از نوع IEnumerable<object[]> داده ها را بسته به تعدادی که میخواهیم yield return میکنیم .توجه کنید مقدار اول و دون n1,n2 مقدار سوم action و مقدار چهارم خروجی است که انتظار داریم. به کلاس CalculatorTest برگشته و تابع تست را به صورت زیر پیاده سازی کنید.
[Theory] [MemberData(nameof(CalculatorData.GetNumbersFromTxtFile), MemberType = typeof(CalculatorData))] public void Calculate_Test_By_Action_From_Extrnal_File(int n1, int n2, Action action, int esult) { int resultCalculate = _fixture.Sut.Calculate(n1,n2,action); ssert.Equal(result,resultCalculate); }
با استفاده از Theory و MemberData توانستم داده را از یک کلاس دیگر فراخوانی کنیم ولی همچنان داده به صورت استاتیک میباشد و باید درون کد داده جدید را اعمال کرد.
خواندن دیتا از یک منبع خارجی
دیتای میتواند از یک دیتابیس ، وب سرویس یا حتی یک فایل txt باشد. این کار باعث میشود کد و داده از هم جدا شوند. برای سادگی کار از یک فایل txt استفاده میکنیم. در مسیر جاری یک فایل به نام data.txt ایجاد کنید و چند داده دیگر اضافه کنید. به کلاس CalculatorData برگشته و یک Property جدید به شکل زیر اضافه کنید.
public static IEnumerable<object[]> GetNumbersFromTxtFile { get { var allLine = System.IO.File.ReadAllLines("./data.txt"); return allLine.Select(x => { var lineSplit = x.Split(","); return new object[] { int.Parse(lineSplit[0]), int.Parse(lineSplit[1]), nt.Parse(lineSplit[2]), int.Parse(lineSplit[3]) }; }); } }
در get به صورت خط ب خط داده را خوانده و بر اساس ',' split میکنیم در اخر هم داده ها را بازگشت میدهیم. تابع تست هم مشابه قبل خواهد بود
[Theory] [MemberData(nameof(CalculatorData.GetNumbersFromTxtFile), MemberType = typeof(CalculatorData))] public void Calculate_Test_By_Action_From_Extrnal_File(int n1, int n2, Action action, int result) { int resultCalculate = _fixture.Sut.Calculate(n1,n2,action); Assert.Equal(result,resultCalculate); }
دقت کنید برای اینکه data.txt به صورت اتوماتیک هنگام build پروژه کپی شود کد زیر را در csproj. کپی کنید
<ItemGroup> <None Update="data.txt"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup>
اگه از مقاله راضی بودید میتونید از لینک زیر برام قهوه بخرید : )