چرا به تستهای واحد نیاز داریم؟
برنامه های ما امکان دارد به طور غیرمنتظره ای در پاسخ به تغییرات با مشکل روبه رو شوند. از این رو، تست خودکار بعد از تغییرات در تمام برنامه ها مورد نیاز است.
تست دستی، کندترین، کم اعتبارترین و گرانترین راه برای آزمایش یک برنامه است. اما اگر برنامه ها به گونه ای طراحی نشده اند که قابل تست باشند، تست دستی ممکن است تنها راه در دسترس ما باشد.
بنابراین اولین قدم این است که مطمئن شوید برنامه به گونه ای طراحی شده است که قابل آزمایش باشد. برنامه هایی که از اصول معماری خوب پیروی می کنند مانند تفکیک نگرانی ها (Seperation of Concerns)، وارونگی وابستگی، مسئولیت منفرد (Single Responsibility)، Don’t repeat yourself (DRY) و غیره به راحتی قابل آزمایش هستند. ASP.NET Core از واحد خودکار، یکپارچه سازی و تست عملکرد پشتیبانی می کند.
یک تست واحد به معنی تست یک قسمت واحد از منطق برنامه ما است. کد ما امکان دارد وابستگیهایی بر روی لایه های پایین ترو کامپوننتهای خارجی داشته باشد. اما آنها با تستهای واحد اعتبارسنجی نمیشوند. تست واحد به طور کامل در حافظه و در فرآیند اجرا می شود. با سیستم فایل، شبکه یا پایگاه داده ارتباط برقرار نمی کند.
تست های واحد فقط باید کد ما را تست کنند.
با توجه به این دلایل، تست های واحد باید بسیار سریع اجرا شوند و ما باید بتوانیم آنها را به طور مکرر اجرا کنیم. در حالت ایدهآل، ما باید تستهای واحد را قبل از ارسال هر تغییر به source control repository و همچنین با هر build خودکار روی build server اجرا کنیم.
راه اندازی Framework تست واحد
framework های تست زیادی در market امروزی وجود دارد. با این حال، برای این مقاله، قصد دادریم از xUnit استفاده کنیم که یک framework تست خیلی معروفی است. حتی تستهای ASP.NET Core و EF Core توسط آن نوشته شده اند.
ما میتوانیم با استفاده از xUnit Test Project template یک پروژه تستxUnit در ویژوال استودیو اضافه کنیم که در ویژوال استودیو در دسترس میباشد:
ما همیشه باید تستهای خود را به سبکی ثابت نامگذاری کنیم، با نامهایی که مشخص کند که هر تست چه کاری انجام میدهد. یک رویکرد خوب این است که کلاس ها و متدهای آزمایشی را با توجه به کلاس و متدی که در حال آزمایش هستند نامگذاری کنید. این کاملاً مشخص می کند که هر آزمایش چه مسئولیتی دارد.
ما می توانیم رفتاری را که در حال آزمایش است را به نام هر test method اضافه کنیم. این باید شامل رفتار مورد انتظار و هر ورودی یا فرضیه هایی باشد که باید این رفتار را نشان دهد.
بنابراین، هنگامی که یک یا چند تست با شکست مواجه می شوند، از نام آنها مشخص است که چه مواردی شکست خورده اند. ما زمانیکه تستهای واحد را در قسمت بعدی ایجاد میکنیم از این قواعد نام گذاری پیروی خواهیم کرد.
بنابراین یک NuGet package reference برای Moq اضافه کنیم که یک فریم ورک آبجکت ساختگی است. این امر، آبجکتهای آزمایشی که آبجکتهای ساختگی یا مجموعه ای از ویژگی ها و رفتارهای متد از پیش تعیین شده برای آزمایش هستند را در اختیار ما قرار می دهد.
تستهای واحد در متدهای کنترلر
بگوییم که یکEmployeeController با یک متد ()Index و ()Add تعریف کرده ایم:
public class EmployeeController : Controller
{
private readonly IDataRepository<Employee> _dataRepository;
public EmployeeController(IDataRepository<Employee> dataRepository)
{
_dataRepository = dataRepository;
}
public IActionResult Index()
{
IEnumerable<Employee> employees = _dataRepository.GetAll();
return View(employees);
}
[HttpPost]
public IActionResult Add(Employee employee)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_dataRepository.Add(employee);
return RedirectToAction(actionName: nameof(Index));
}
}
حالا نحوه نوشتن تستهای واحد برای این اکشن متدها را بررسی کنیم. کنترلر ما از تزریق وابستگی برای گرفتن مقدار برای dataRepository_ استفاده میکند. این باعث میشود که این امکان را برای unit testing framework فراهم کند تا یک آبجکت ساختگی را ارائه داده و متد ها را تست کند.
الگوی (Arrange, Act, Assert) AAA یک شیوه ی رایج برای نوشتن تستهای واحد است و ما همین الگو را اینجا دنبال خواهیم کرد.
قسمت Arrange یک متد تست واحد، آبجکتها را مقداردهی اولیه میکند و مقادیر را برای ورودیهایی که به متد مورد آزمایش پاس داده میشوند set میکند.
قسمت Act، متد مورد آزمایش را به همراه پارامترهای Arrange فراخوانی میکند.
قسمت Assert تأیید می کند که اکشن متد مورد آزمایش، طبق انتظار عمل می کند.
آزمایش متد ()Index
حالا تستها را برای متد ()Index بنویسیم:
[Fact]
public void Index_ReturnsAViewResult_WithAListOfEmployees()
{
// Arrange
var mockRepo = new Mock<IDataRepository<Employee>>();
mockRepo.Setup(repo => repo.GetAll())
.Returns(GetTestEmployees());
var controller = new EmployeeController(mockRepo.Object);
// Act
var result = controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<List<Employee>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
private IEnumerable<Employee> GetTestEmployees()
{
return new List<Employee>()
{
new Employee()
{
Id = 1,
Name = "John"
},
new Employee()
{
Id = 2,
Name = "Doe"
}
};
}
ما اول با استفاده از متد ()GetTestEmployees ، سرویس IDataRepository<Employee> را mock کردیم. ()GetTestEmployees یک لیست از دو آبجکت ساختگی را ایجاد کرده و برمیگرداند.
سپس متد Index() اجرا شده و موارد زیر را روی نتیجه assert میکند:
یک ViewResult برمیگرداند
نوع مدل برگشتی List<Employee> میباشد
دو آبجکت Employee در مدل برگشتی وجود دارد
هر test method با یک ویژگی [Fact] مزین شده است که نشان می دهد این یک متد واقعی است که باید توسط test runner اجرا شود.
آزمایش متد ()Add
حالا تستها را برای متد ()Add بنویسیم:
[Fact]
public void Add_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IDataRepository<Employee>>();
var controller = new EmployeeController(mockRepo.Object);
controller.ModelState.AddModelError("Name", "Required");
var newEmployee = GetTestEmployee();
// Act
var result = controller.Add(newEmployee);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public void Add_AddsEmployeeAndReturnsARedirect_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IDataRepository<Employee>>();
mockRepo.Setup(repo => repo.Add(It.IsAny<Employee>()))
.Verifiable();
var controller = new EmployeeController(mockRepo.Object);
var newEmployee = GetTestEmployee();
// Act
var result = controller.Add(newEmployee);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
private Employee GetTestEmployee()
{
return new Employee()
{
Id = 1,
Name = "John"
};
}
اولین تست تأیید می کند که زمانیکه ModelState.IsValid برابر با false است، اکشن متد یک ViewResult 400 Bad Request را با داده های مناسب برمی گرداند. میتوانیم با اضافه کردن خطاها با استفاده از متد ()AddModelError وضعیت مدل نامعتبر را آزمایش کنیم.
تست دوم تأیید میکند که وقتی ModelState.IsValid برابر با true است، متد ()Add در repository فراخوانی میشود و RedirectToActionResult با آرگومانهای صحیح بازگردانده میشود.
متدهای Mock که ما آنها را فراخوانی نمی کنیم معمولا نادیده گرفته می شوند، اما با فراخوانی ()Verifiable همراه با تنظیمات، می توانیم تایید کنیم که متدهای Mock فراخوانی شده اند. ما میتوانیم این را با استفاده از mockRepo.Verify تأیید کنیم، که اگر متد مورد انتظار فراخوانی نشده باشد، آزمایش مورد نظر شکست میخورد.
اجرای تستها
ما میتوانیم تستها را با استفاده از Test Explorer در Visual Studio اجرا کنیم:
در Test Explorer میتوانیم تمام تستهای موجود را بهصورت گروهبندی شده بر اساس Solution، Project، Class و غیره مشاهده کنیم. ما می توانیم تست ها را با اجرا یا debug کردن آن ها انتخاب و اجرا کنیم. هنگامی که تست ها را اجرا می کنیم، می توانیم یک علامت تیک سبز یا یک علامت متقاطع قرمز را در سمت چپ نام متد تست ببینیم که نشان دهنده موفقیت یا شکست در آخرین اجرای متد است. در سمت راست، میتوانیم زمان صرف شده برای اجرای هر تست را ببینیم.