MVVM و الگوی ViewModel Locator
مقایسه مجوزهای سورس باز
Open Source Initiative OSI - Common Public License Version 1.0:Licensing
[OSI Approved License]
Common Public License Version 1.0 (CPL)
چیزی ننوشته بودید
You want your local web development set up to reflect your production
reality as much as possible. URL parsing, routing, redirects, avoiding
mixed-content warnings, etc. It's very easy to accidentally find oneself
on http:// when everything in 2018 should be under https://.
I'm using ASP.NET Core 2.1 which makes local SSL super easy. After installing from http://dot.net I'll "dotnet new razor" in an empty folder to make a quick web app.
در این مطلب با توجه به سؤالات زیادی که در مورد صرفا نحوهی اجرای بدون خطای آن وجود داشت، ریز مراحل آنرا بررسی میکنیم.
پیشنیازهای توسعهی برنامه
- با توجه به استفاده از ویژگیهای C# 6 در این پروژه، حتما نیاز است برای کار و اجرای آن از VS 2015 استفاده کنید.
- همچنین این پروژه از قابلیت «فایل استریم» SQL Server استفاده میکند. بنابراین نیاز است نگارش متناسبی از SQL Server را پیشتر نصب کرده باشید (هر نگارشی بالاتر از SQL Server 2005).
- اگر از ReSharper استفاده میکنید، به صورت موقت آنرا به حالت تعلیق درآورید (منوی tools، گرینهی options و انتخاب resharper و سپس suspend کردن آن). این مورد سرعت بازیابی بستههای نیوگت را به شدت افزایش میدهد.
بازیابی وابستگیهای نیوگت پروژه
مرسوم نیست چند 10 مگابایت وابستگیهای پروژه را به صورت فایلهای باینری، به مخزن کدها ارسال کرد. از این جهت که نیوگت بر اساس مداخل فایلهای packages.config، قابلیت بازیابی و نصب خودکار آنها را دارد. بنابراین ابتدا package manger console را باز کنید؛ از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console
همانطور که در تصویر مشاهده میکنید، نیوگت تشخیص دادهاست که بستههایی برای نصب وجود دارند. بنابراین بر روی دکمهی restore کلیک کنید تا کار دریافت و نصب خودکار این بستهها از اینترنت شروع شود. البته اگر پیشتر این بستهها را در پروژههای دیگری نصب کرده باشید، نیوگت از کش موجود در سیستم استفاده خواهد کرد و برای دریافت آنها به اینترنت مراجعه نمیکند. ولی در هر حال اتصال به اینترنت ضروری است.
پس از پایان کار بازیابی بستهها، یکبار کل Solution را Build کنید تا مطمئن شوید که تمام بستههای مورد نیاز به درستی بازیابی و نصب شدهاند (Ctrl+Shift+B و یا همان منوی Build و انتخاب گزینهی Build Solution).
تنظیمات رشته اتصالی بانک اطلاعاتی برنامه
پس از Build موفق کل Solution در مرحلهی قبل، اکنون نوبت به برپایی تنظیمات بانک اطلاعاتی برنامه است. برای این منظور فایل web.config ذیل را باز کنید:
Decision\src\Decision.Web\Web.config
یک چنین تنظیمی را مشاهده میکنید:
<connectionStrings> <clear /> <add name="DefaultConnection" connectionString="Data Source=.\sqlexpress;Initial Catalog=DecisionDb;Integrated Security = true;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings>
<connectionStrings> <clear /> <add name="DefaultConnection" connectionString="Data Source=(local);Initial Catalog=DecisionDb;Integrated Security = true;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings>
در این حالت نیاز است بانک اطلاعاتی خالی DecisionDb را خودتان ایجاد کنید. علت آن به AutomaticMigrationsEnabled = false بر میگردد؛ که در ادامه توضیح داده شدهاست و همچنین وجود تنظیم ذیل در فایل Decision\src\Decision.Web\App_Start\ApplicationStart.cs
Database.SetInitializer<ApplicationDbContext>(null);
ایجاد بانک اطلاعاتی برنامه و تنظیمات آن
پس از آن، نوبت به ایجاد بانک اطلاعاتی برنامه است. چون این برنامه از EF Code first استفاده میکند، قادر است بانک اطلاعاتی ذکر شدهی در Initial Catalog فوق را به صورت خودکار ایجاد کند (با تمام جداول، روابط و تنظیمات آنها). این اطلاعات هم از پروژهی Decision.DataLayer و پوشهی Migrations آن تامین میشوند.
اگر به فایل Decision\src\Decision.DataLayer\Migrations\201602072159421_Initial.cs مراجعه کنید، یکسری تنظیمات دستی را هم علاوه بر کدهای خودکار EF، مشاهده خواهید کرد:
//. . . Sql("EXEC sp_configure filestream_access_level, 2"); Sql("RECONFIGURE", true); Sql("alter database DecisionDb Add FileGroup FileGroupApplicant contains FileStream", true); Sql("alter database DecisionDb add file ( name = 'ApplicantDocuements' , filename = 'C:\\FileStream\\ApplicantDocuements') to filegroup FileGroupApplicant", true); //. . .
بنابراین نیاز است در درایور C، پوشهی خالی FileStream از پیش تهیه شده باشد (نیازی به ایجاد پوشهی ApplicantDocuements نیست و این پوشه به صورت خودکار ایجاد میشود).
و در فایل Decision\src\Decision.DataLayer\Migrations\Configuration.cs مشخص شدهاست که AutomaticMigrationsEnabled = false. به این معنا که تنظیمات فوق به صورت خودکار به بانک اطلاعاتی اعمال نشده و باید چند دستور ذیل را به صورت دستی صادر کنیم:
الف) ابتدا package manager console را مجددا باز کنید و در اینجا default project را بر روی Decision.DataLayer قرار دهید. از این جهت که قرار است اطلاعات migration را از این پروژه دریافت کنیم:
در غیراینصورت پیام خطای No migrations configuration type was found in the assembly را دریافت خواهید کرد.
ب) سپس دستور ذیل را صادر کنید (با این فرض که بانک اطلاعاتی خالی DecisionDb ذکر شدهی در قسمت قبل را پیشتر ایجاد کردهاید):
PM> Update-Database -Verbose -ConnectionStringName "DefaultConnection" -StartUpProjectName "Decision.Web"
من در این حالت پیام خطای Update-Database : The term 'Update-Database' is not recognized as the name of a cmdlet را دریافت کردم.
راه حل: یکبار ویژوال استودیو را بسته و مجددا باز کنید تا کار نصب بستهها و بارگذاری تمام وابستگیهای آنها به درستی صورت گیرد. این خطا به این معنا است که هرچند NuGet کار نصب EF را انجام دادهاست، اما هنوز اسکریپتهای پاورشل آن که دستوراتی مانند Update-Database را اجرا میکنند، بارگذاری نشدهاند. راه حل آن بستن و اجرای مجدد ویژوال استودیو است.
پس از اجرای مجدد ویژوال استودیو و انتخاب default project صحیح (مطابق تصویر فوق)، مجددا دستور Update-Database فوق را صادر کنید (با پارامترهای ویژهی آن).
با صدور این دستور، پیام خطای ذیل را دریافت کردم:
The Entity Framework provider type 'System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework' registered in the application config file for the ADO.NET provider with invariant name 'System.Data.SqlClient' could not be loaded.
PM> Update-Package -Reinstall "EntityFramework" -ProjectName "Decision.DataLayer"
اینبار دستور update-database فوق (با پارامترهای ویژهی آن) بدون مشکل اجرا شد و بانک اطلاعاتی مربوطه تشکیل گردید.
اکنون برنامه قابل اجرا است و در این حالت است که میتوان دکمهی F5 را جهت اجرای برنامه فشرد. البته در این حالت بر روی پروژهی Decision.Web کلیک راست کرده و گزینهی set as startup project را نیز انتخاب کنید و سپس F5:
لطفا سؤالاتی را که مرتبط با «راه اندازی» این پروژه نیستند، در قسمت بازخوردهای اختصاصی آن مطرح کنید.
انتقال Context و Migrations به یک اسمبلی دیگر
تا اینجا اگر مثال بررسی شده را دنبال کرده باشید، دو پوشهی Entities و Migrations را به همراه فایلهای موجودیتها، Context برنامه و Migrations آنها، در همان پروژهی اصلی برنامه، خواهید داشت:
در ادامه قصد داریم بانک اطلاعاتی آزمایشی برنامه را drop کرده، پوشهی Migrations را حذف و صرفا دو فایل ApplicationDbContextSeedData و DBInitialization آنرا نگه داریم.
کلاس Person را به اسمبلی جدید Entities و کلاس ApplicationDbContext را به اسمبلی جدید DataLayer منتقل میکنیم:
اسمبلی جدید Core1RtmEmptyTest.Entities از نوع NET Core Class Library. است و صرفا حاوی کلاسهای موجودیتهای برنامهاست.
اسمبلی جدید Core1RtmEmptyTest.DataLayer نیز از نوع NET Core Class Library. بوده و حاوی تعاریف Context برنامه، به همراه Migrations و تنظیمات آن خواهد بود.
تا اینجا با این نقل و انتقالات، نیاز است وابستگیهای DataLayer را اصلاح کنیم. بنابراین فایل project.json آنرا گشوده و به نحو ذیل تکمیل نمائید:
{ "version": "1.0.0-*", "dependencies": { "Core1RtmEmptyTest.Entities": "1.0.0-*", "Microsoft.EntityFrameworkCore": "1.0.0", "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.Extensions.Configuration.Abstractions": "1.0.0", "NETStandard.Library": "1.6.0" }, "frameworks": { "netstandard1.6": { "imports": "dnxcore50" } } }
وابستگی Microsoft.Extensions.Configuration.Abstractions برای کار با IConfigurationRoot اضافه شدهاست (دسترسی به تنظیمات برنامه از طریق تزریق وابستگیها).
به علاوه اکنون به پروژهی وب اصلی مراجعه کرده و فایل project.json آنرا جهت افزودن ارجاعاتی به این دو اسمبلی جدید، ویرایش کنید:
{ "dependencies": { // same as before "Core1RtmEmptyTest.Entities": "1.0.0-*", "Core1RtmEmptyTest.DataLayer": "1.0.0-*" } }
فعال سازی Migrations و قرار دادن فایلهای آن در اسمبلی Core1RtmEmptyTest.DataLayer
در ادامه اگر مانند قسمت قبل بخواهیم مهاجرتها را اضافه کنیم، به خطای ذیل خواهیم رسید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase Your target project 'Core1RtmEmptyTest' doesn't match your migrations assembly 'Core1RtmEmptyTest.DataLayer'. Either change your target project or change your migrations assembly.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef migrations add InitialDatabase No executable found matching command "dotnet-ef"
{ // same as before "tools": { "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "imports": [ "portable-net45+win8" ] } }, // same as before }
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef migrations add InitialDatabase Could not invoke this command on the startup project 'Core1RtmEmptyTest.DataLayer'. This preview of Entity Framework tools does not support commands on class library projects in ASP.NET Core and .NET Core applications.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef --startup-project ../Core1RtmEmptyTest/ migrations add InitialDatabase Done. To undo this action, use 'dotnet ef migrations remove'
اعمال کلاسهای Migrations تولید شده به بانک اطلاعاتی
پس از تولید موفقیت آمیز فایلهای مهاجرت، برای اعمال آنها به بانک اطلاعاتی، اینبار نیز دستور را از همان پوشهی DataLayer با پارامتر پروژهی آغازین اجرا میکنیم:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef --startup-project ../Core1RtmEmptyTest/ database update Applying migration '13950527070105_InitialDatabase'. Done.
بنابراین به صورت خلاصه
- ابتدا قسمت tools تنظیمات پروژهی data layer را برای فعال سازی دستورات خط فرمان EF ویرایش کنید.
- سپس از طریق خط فرمان به پوشهی data layer وارد شوید. اینبار باید دستورات EF را از ریشهی این پوشه، بجای پوشهی اصلی برنامه صادر کرد.
- در اینجا دستورات افزودن مهاجرتها و به روز رسانی بانک اطلاعاتی، همانند قبل هستند. فقط ذکر محل واقع شدن پوشهی آغازین برنامه توسط پارامتر startup-project الزامی است.
Mocking چیست؟
فرض کنید برنامهای را داریم که از تعدادی کلاس تشکیل شدهاست. در این بین میخواهیم تعدادی از آنها را به صورت ایزولهی از کل سیستم آزمایش کنیم. البته باید درنظر داشت که این کلاسها در حین اجرای واقعی برنامه، از تعدادی وابستگی خاص در همان سیستم استفاده میکنند. برای مثال کلاسی در این بین برای بررسی میزان اعتبار مالی یک کاربر، نیاز دارد تا با یک وب سرویس خارجی کار کند. اما چون میخواهیم این کلاس را به صورت ایزولهی از کل سیستم آزمایش کنیم، اینبار بجای استفادهی از وابستگی واقعی این کلاس، آن وابستگی را با یک نمونهی تقلیدی یا Mock object در اینجا، جایگزین میکنیم.
بنابراین Mocking به معنای جایگزین کردن یک وابستگی واقعی سیستم که در زمان اجرای آن مورد استفاده قرار میگیرد، با نمونهی تقلیدی مختص زمان آزمایش برنامه، جهت بالابردن سهولت نوشتن آزمونهای واحد است.
دلایل و مزایای استفادهی از Mocking
- یکی از مهمترین دلایل استفادهی از Mocking، کاهش پیچیدگی تنظیمات اولیهی نوشتن آزمونهای واحد است. برای مثال اگر در برنامهی خود از تزریق وابستگیها استفاده میکنید و کلاسی دارای چندین وابستگی تزریق شدهی به آن است، برای آزمایش این کلاس نیاز به تدارک تمام این وابستگیها را خواهید داشت تا بتوان این کلاس را وهله سازی کرد و همچنین برنامه را نیز کامپایل نمود. اما در این بین ممکن است آزمایش متدی در همان کلاس، الزاما از تمام وابستگیهای تزریق شدهی در یک کلاس استفاده نکند. در این حالت، Mocking میتواند تنظیمات پیچیدهی وهله سازی این کلاس را به حداقل برساند.
- Mocking میتواند سبب افزایش سرعت اجرای آزمونهای واحد نیز شود. برای مثال با تقلید سرویسهای خارجی مورد استفادهی در برنامه (هر عملی که از مرزهای سیستم رد شود مانند کار با شبکه، بانک اطلاعاتی، فایل سیستم و غیره)، میتوان میزان I/O و همچنین زمان صرف شدهی به آنرا به حداقل رساند.
- از mock objects میتوان برای رهایی از مشکلات کار با مقادیر غیرمشخص استفاده کرد. برای مثال اگر در کدهای خود از DateTime.Now استفاده میکنید یا اعداد اتفاقی و امثال آن، هربار که آزمونهای واحد را اجرا میکنیم، خروجی متفاوتی را دریافت کرده و بسیاری از آزمونهای نوشته شده با مشکل مواجه میشوند. به کمک mocking میتوان بجای این مقادیر غیرمشخص، یک مقدار ثابت و مشخص را بازگشت دهد.
- چون به سادگی میتوان mock objects را تهیه کرد، میتوان کار توسعه و آزمایش برنامه را پیش از به پایان رسیدن پیاده سازی اصلی سرویسهای مدنظر، همینقدر که اینترفیس آن سرویس مشخص باشد، شروع کرد که میتواند برای کارهای تیمی بسیار مفید باشد.
- اگر وابستگی مورد استفاده ناپایدار و یا غیرقابل پیش بینی است، میتوان توسط mocking به یک نمونهی قابل پیش بینی و پایدار مخصوص آزمونهای برنامه رسید.
- اگر وابستگی خارجی مورد استفاده به ازای هر بار استفاده، هزینهای را شارژ میکند، میتوان توسط mocking، هزینهی آزمونهای برنامه را کاهش داد.
Unit test چیست؟
بدیهی است در کنار آزمایش ایزولهی قسمتهای مختلف برنامه توسط mocking، باید کل برنامه را جهت بررسی دستیابی به نتایج واقعی نیز آزمایش کرد که به این نوع آزمونها، آزمون یکپارچگی (Integration Tests)، API Tests ،UI Tests و غیره میگویند که در کنار Unit tests ما حضور خواهند داشت. بنابراین اکنون این سؤال مطرح میشود که یک Unit چیست؟
در برنامهای که از چندین کلاس تشکیل میشود، به یک کلاس، یک Unit گفته میشود. همچنین اگر در این سیستم، دو یا چند کلاس با هم کار میکنند (کلاسی که از چندین وابستگی استفاده میکند)، اینها با هم نیز یک Unit را تشکیل دهند. بنابراین تعریف Unit بستگی به نحوهی درک عملکرد یک سیستم و تعامل اجزای آن با هم دارد.
واژههای متناظر با Mock objects
در حین مطالعهی منابع مرتبط با آزمونهای واحد ممکن است با این واژههای تقریبا مشابه مواجه شوید: fakes ،stubs ،dummies و mocks. اما تفاوت آنها در چیست؟
- Fakes در حقیقت یک نمونه پیاده سازی واقعی، اما غیرمناسب محیط واقعی و اصلی پروژهاست. برای نمونه EF Core به همراه یک نمونه in-memory database هم هست که دقیقا با مفهوم Fakes تطابق دارد.
- از Dummies صرفا جهت تهیهی پارامترهای مورد نیاز برای اجرای یک آزمایش استفاده میشوند. این پارامترها، هیچگاه در آزمایشهای انجام شده مورد استفاده قرار نمیگیرند.
- از Stubs برای ارائهی پاسخهایی مشخص به فراخوانها استفاده میشود. برای مثال یک متد یا خاصیت، دقیقا چه چیزی را باید بازگشت دهند.
- از Mocks برای بررسی تعامل اجزای مختلف در حال آزمایش استفاده میشود. آیا متدی یا خاصیتی مورد استفاده قرار گرفتهاست یا خیر؟
باید درنظر داشت که زمانیکه یک شیء Mock را توسط کتابخانهی Moq تهیه میکنیم، هر سه مفهوم stubs ،dummies و mocks را با هم به همراه دارد. به همین جهت در این سری زمانیکه به یک mock object اشاره میشود، هر سه مفهوم مدنظر هستند.
واژهی دیگری که ممکن است در این گروه زیاد مشاهده شود، «Test double» نام دارد که ترکیب هر 4 مورد fakes ،stubs ،dummies و mocks میباشد. در کل هر زمانیکه یک شیء مورد استفادهی در زمان اجرای برنامه را جهت آزمایش سادهتر آن جایگزین میکنید، یک Test double را ایجاد کردهاید.
بررسی ساختار برنامهای که میخواهیم آنرا آزمایش کنیم
در این سری قصد داریم یک برنامهی وام دهی را آزمایش کنیم که قسمتهای مختلف آن دارای وابستگیهای خاصی میباشند. ساختار این برنامه را در ادامه مشاهده میکنید:
موجودیتهای برنامهی وام دهی
namespace Loans.Entities { public class Applicant { public int Id { set; get; } public string Name { set; get; } public int Age { set; get; } public string Address { set; get; } public decimal Salary { set; get; } } }
namespace Loans.Entities { public class LoanProduct { public int Id { set; get; } public string ProductName { set; get; } public decimal InterestRate { set; get; } } }
namespace Loans.Entities { public class LoanApplication { public int Id { set; get; } public LoanProduct Product { set; get; } public LoanAmount Amount { set; get; } public Applicant Applicant { set; get; } public bool IsAccepted { set; get; } } public class LoanAmount { public string CurrencyCode { get; set; } public decimal Principal { get; set; } } }
مدلهای برنامهی وام دهی
namespace Loans.Models { public class IdentityVerificationStatus { public bool Passed { get; set; } } }
سرویسهای برنامهی وام دهی
using Loans.Models; namespace Loans.Services.Contracts { public interface IIdentityVerifier { void Initialize(); bool Validate(string applicantName, int applicantAge, string applicantAddress); void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid); void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status); } }
namespace Loans.Services.Contracts { public interface ICreditScorer { int Score { get; } void CalculateScore(string applicantName, string applicantAddress); } }
using System; using Loans.Entities; using Loans.Services.Contracts; namespace Loans.Services { public class LoanApplicationProcessor { private const decimal MinimumSalary = 1_500_000_0; private const int MinimumAge = 18; private const int MinimumCreditScore = 100_000; private readonly IIdentityVerifier _identityVerifier; private readonly ICreditScorer _creditScorer; public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); } public bool Process(LoanApplication application) { application.IsAccepted = false; if (application.Applicant.Salary < MinimumSalary) { return application.IsAccepted; } if (application.Applicant.Age < MinimumAge) { return application.IsAccepted; } _identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address); if (!isValidIdentity) { return application.IsAccepted; } _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address); if (_creditScorer.Score < MinimumCreditScore) { return application.IsAccepted; } application.IsAccepted = true; return application.IsAccepted; } } }
using System; using Loans.Models; using Loans.Services.Contracts; namespace Loans.Services { public class IdentityVerifierServiceGateway : IIdentityVerifier { public DateTime LastCheckTime { get; private set; } public void Initialize() { // Initialize connection to external service } public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = DateTime.Now; Disconnect(); return isValidIdentity; } private void Connect() { // Open connection to external service } private bool CallService(string applicantName, int applicantAge, string applicantAddress) { // Make call to external service, interpret the response, and return result return false; // Simulate result for demo purposes } private void Disconnect() { // Close connection to external service } public void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid) { throw new NotImplementedException(); } public void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status) { throw new NotImplementedException(); } } }
هدف از این برنامه، درخواست یک وام جدید است. Application در اینجا به معنای درخواست یا فرم جدید است و Applicant نیز شخصی است که این درخواست را دادهاست.
در اینجا بیشتر تمرکز ما بر روی کلاس LoanApplicationProcessor است که دارای دو وابستگی تزریق شدهی به آن نیز میباشد:
public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); }
تمام این منطق نیز در متد Process آن قابل مشاهدهاست که هدف اصلی آن، بررسی قابل پذیرش بودن درخواست یک وام جدید است.
نوشتن اولین تست، برای برنامهی وام دهی
در اولین تصویر این قسمت، پروژهی class library دومی را نیز به نام Loans.Tests مشاهده میکنید. فایل csproj آن به صورت زیر برای کار با MSTest تنظیم شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Loans\Loans.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="MSTest.TestAdapter" Version="2.0.0" /> <PackageReference Include="MSTest.TestFramework" Version="2.0.0" /> </ItemGroup> </Project>
اکنون اولین آزمون واحد ما در کلاس جدید LoanApplicationProcessorShould چنین شکلی را پیدا میکند:
using Loans.Entities; using Loans.Services; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void DeclineLowSalary() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var processor = new LoanApplicationProcessor(null, null); processor.Process(application); Assert.IsFalse(application.IsAccepted); } } }
در این آزمایش، شخص درخواست کننده، حقوق کمی دارد و میخواهیم بررسی کنیم که آیا LoanApplicationProcessor میتواند آنرا بر اساس مقدار MinimumSalary، رد کند یا خیر؟
public class LoanApplicationProcessor { private const decimal MinimumSalary = 1_500_000_0;
در حین وهله سازی LoanApplicationProcessor، دو وابستگی آن به null تنظیم شدهاند؛ چون میدانیم که بررسی MinimumSalary پیش از سایر بررسیها صورت میگیرد و اساسا در این آزمایش، نیازی به این وابستگیها نداریم.
اما اگر سعی در اجرای این آزمایش کنیم (برای مثال با اجرای دستور dotnet test در خط فرمان)، آزمایش اجرا نشده و با استثنای زیر مواجه میشویم:
Test method Loans.Tests.LoanApplicationProcessorShould.DeclineLowSalary threw exception: System.ArgumentNullException: Value cannot be null. Parameter name: identityVerifier
public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); }
نصب کتابخانهی Moq جهت برآورده کردن وابستگیهای کلاس LoanApplicationProcessor
در این آزمایش چون وجود وابستگیهای در سازندهی کلاس، برای ما اهمیتی ندارند و همچنین ذکر آنها نیز الزامی است، میخواهیم توسط کتابخانهی Moq، دو نمونهی تقلیدی از آنها را تهیه کرده (همان dummies که پیشتر معرفی شدند) و جهت برآورده کردن بررسی صورت گرفتهی در سازندهی کلاس LoanApplicationProcessor، آنها را ارائه کنیم.
کتابخانهی بسیار معروف Moq، با پروژههای مبتنی بر NETFramework 4.5. و همچنین NETStandard 2.0. به بعد سازگار است و برای نصب آن، میتوان یکی از دو دستور زیر را صادر کرد:
> dotnet add package Moq > Install-Package Moq
اما چرا کتابخانهی Moq؟
کتابخانهی Moq این اهداف را دنبال میکند: سادهاست، به شدت کاربردیاست و همچنین strongly typed است. این کتابخانه سورس باز بوده و تعداد بار دانلود بستهی نیوگت آن میلیونی است.
پس از نصب آن، اولین آزمایشی را که نوشتیم، به صورت زیر اصلاح میکنیم:
using Loans.Entities; using Loans.Services; using Loans.Services.Contracts; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void DeclineLowSalary() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsFalse(application.IsAccepted); } } }
کار با ذکر new Mock شروع شده و آرگومان جنریک آنرا از نوع وابستگیهایی که نیاز داریم، مقدار دهی میکنیم. سپس خاصیت Object آن، امکان دسترسی به این شیء تقلید شده را میسر میکند.
اکنون اگر مجددا این آزمون واحد را اجرا کنیم، مشاهده خواهیم کرد که بجای صدور استثناء، با موفقیت به پایان رسیدهاست:
گاهی از اوقات جایگزین کردن یک وابستگی null با نمونهی Mock آن کافی نیست
در مثالی که بررسی کردیم، اشیاء mock، کار برآورده کردن نیازهای ابتدایی آزمایش را انجام داده و سبب اجرای موفقیت آمیز آن شدند؛ اما همیشه اینطور نیست:
using Loans.Entities; using Loans.Services; using Loans.Services.Contracts; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void Accept() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsTrue(application.IsAccepted); } } }
اگر این آزمایش را اجرا کنیم، با شکست مواجه خواهد شد. علت اینجا است که هرچند در حال استفادهی از دو mock object به عنوان وابستگیهای مورد نیاز هستیم، اما تنظیمات خاصی را بر روی آنها انجام ندادهایم و به همین جهت خروجی مناسبی را در اختیار LoanApplicationProcessor قرار نمیدهند. برای مثال مرحلهی بعدی بررسی اعتبار شخص در کلاس LoanApplicationProcessor، فراخوانی سرویس identityVerifier و متد Validate آن است که خروجی آن بر اساس کدهای فعلی، همیشه false است:
_identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MoqSeries-01.zip
Redis Fundamentals for .NET Developers
Redis is an open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and even a message broker.
In this live sessions, Stephen Lorello, Senior Field Engineer at Redis, joins us to show the the fundamental features .NET developers show know about using Redis