در این قسمت ابتدا نحوهی فعال سازی فریم ورک آزمونهای واحد مایکروسافت و سپس نحوهی فعال سازی این تامین کنندهی بانک اطلاعاتی درون حافظهای را بررسی خواهیم کرد. به علاوه برای سرویس بلاگهای قسمت قبل نیز آزمون واحد خواهیم نوشت.
نحوهی فعالسازی فریم ورک MSTest در یک پروژهی Class library از نوع NET Core.
تنها نکتهی مهم فعالسازی MSTest در یک پروژهی Class library جدید که برای نوشتن آزمونهای واحد مورد استفاده قرار خواهیم داد، تنظیمات فایل project.json آن است که در ذیل آمده است:
{ "version": "1.0.0-*", "testRunner": "mstest", "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" }, "dotnet-test-mstest": "1.1.1-preview", "MSTest.TestFramework": "1.0.1-preview", "NETStandard.Library": "1.6.0", "Microsoft.EntityFrameworkCore": "1.0.0", "Microsoft.EntityFrameworkCore.InMemory": "1.0.0", "Core1RtmEmptyTest.DataLayer": "1.0.0-*", "Core1RtmEmptyTest.Entities": "1.0.0-*", "Core1RtmEmptyTest.Services": "1.0.0-*", "Core1RtmEmptyTest.ViewModels": "1.0.0-*" }, "frameworks": { "netcoreapp1.0": { "imports": [ "dnxcore50", "portable-net45+win8" ] } } }
- به علاوه در اینجا ارجاعاتی را به اسمبلیهای موجودیتها، Services و DataLayer که در قسمت «شروع به کار با EF Core 1.0 - قسمت 14 - لایه بندی و تزریق وابستگیها» بررسی شدند نیز ملاحظه میکنید.
- همچنین وابستگی جدید Microsoft.EntityFrameworkCore.InMemory نیز در اینجا قابل ملاحظه است. این وابستگی را تنها به پروژهی آزمونهای واحد خود اضافه میکنیم. از این جهت که تنظیمات آن صرفا در این قسمت جدید قید میشوند و نه در سایر قسمتهای برنامه.
پس از آن، کار با این فریم ورک، همانند سایر نگارشهای دات نت خواهد بود:
using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EFCore.MsTests { [TestClass] public class CoreTests { [TestMethod] public void Test1() { Assert.IsTrue(true); } } }
پس از نوشتن اولین آزمون واحد، یکبار پروژه را build کرده و سپس از منوی Test، گزینهی Windows را انتخاب کرده و در اینجا گزینهی Test Explorer را انتخاب کنید. اندکی صبر کنید تا آزمونهای واحد شما شناسایی شوند و سپس گزینهی Run All را انتخاب کنید:
تغییرات Context برنامه جهت استفادهی از تامین کنندهی داخل حافظهای
در مورد نحوهی تعریف و افزودن وابستگیهای EF Core در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» پیشتر بحث شد و همچنین در مطلب «شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرتها به یک اسمبلی دیگر»، اطلاعات Context برنامه را به اسمبلی دیگری منتقل کردیم.
اگر از روش بازنویسی متد OnConfiguring برای تنظیم تامین کنندهی بانک اطلاعاتی مورد نظر استفاده میکنید، متد OnConfiguring کلاس Context برنامه چنین شکلی را پیدا میکند:
public class ApplicationDbContext : DbContext, IUnitOfWork { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer( _configuration["ConnectionStrings:ApplicationDbContextConnection"] , serverDbContextOptionsBuilder => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; serverDbContextOptionsBuilder.CommandTimeout(minutes); }); } }
الف) اضافه شدن سازندهی دومی که <DbContextOptions<ApplicationDbContext را دریافت میکند. از آن در سمت کدهای آزمون واحد برنامه جهت ثبت ()options.UseInMemoryDatabase استفاده میشود.
ب) به متد OnConfiguring، بررسی optionsBuilder.IsConfigured هم اضافه شدهاست. چون در سمت کدهای آزمون واحد، تامین کنندهی بانک اطلاعاتی درون حافظهای اضافه میشود، مقدار optionsBuilder.IsConfigured به true تنظیم خواهد شد و دیگر از تامین کنندهی SQL Server استفاده نمیشود.
اگر از متد OnConfiguring به این شکل استفاده نمیکنید، تنها ذکر سازندهی دوم ضروری است. از این جهت که در آزمونهای واحد، از تنظیمات متد ConfigureServices کلاس آغازین برنامه استفاده نخواهد شد.
نوشتن آزمونهای واحد مخصوص EF Core
پس از برپایی پیشنیازهای نوشتن آزمونها واحد، شامل تنظیمات فریم ورک MSTest و همچنین افزودن وابستگیهای مرتبط با فایل project.json ایی که در ابتدای بحث عنوان شد و اصلاح سازنده و متد OnConfiguring کلاس Context برنامه جهت آماده سازی آنها برای پذیرش تامین کنندههای دیگر، اکنون یک نمونه از آزمونهای واحد درون حافظهای EF Core، چنین شکلی را خواهد داشت:
using System; using System.Linq; using Core1RtmEmptyTest.DataLayer; using Core1RtmEmptyTest.Entities; using Core1RtmEmptyTest.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Core1RtmEmptyTest.MsTests { [TestClass] public class CoreTests { private readonly IServiceProvider _serviceProvider; public CoreTests() { var services = new ServiceCollection(); services.AddEntityFrameworkInMemoryDatabase() .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase()); services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IBlogService, BlogService>(); _serviceProvider = services.BuildServiceProvider(); } [TestMethod] public void Find_searches_url() { // Insert seed data into the database using one instance of the context using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) { context.Set<Blog>().Add(new Blog { Url = "http://sample.com/cats" }); context.Set<Blog>().Add(new Blog { Url = "http://sample.com/catfish" }); context.Set<Blog>().Add(new Blog { Url = "http://sample.com/dogs" }); context.SaveAllChanges(); } } // Use a separate instance of the context to verify correct data was saved to database using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) { Assert.AreEqual(3, context.Set<Blog>().Count()); Assert.AreEqual("http://sample.com/cats", context.Set<Blog>().First().Url); } } // Use a clean instance of the context to run the test using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var blogService = serviceScope.ServiceProvider.GetRequiredService<IBlogService>(); var results = blogService.GetPagedBlogsAsNoTracking(pageNumber: 0, recordsPerPage: 10); Assert.AreEqual(3, results.Count); } } } }
همانطور که در قسمت «تغییرات Context برنامه جهت استفادهی از تامین کنندهی داخل حافظهای» فوق عنوان شد، در حین انجام آزمونهای واحد، دیگر به کلاس آغازین برنامه و تنظیمات آن مراجعه نمیشود. بنابراین باید شبیه به عملکرد متد ConfigureServices آنرا در اینجا پیاده سازی کرد. نمونهای از انجام اینکار را در سازندهی کلاس انجام آزمونهای واحد مشاهده میکنید:
private readonly IServiceProvider _serviceProvider; public CoreTests() { var services = new ServiceCollection(); services.AddEntityFrameworkInMemoryDatabase() .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase()); services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IBlogService, BlogService>(); _serviceProvider = services.BuildServiceProvider(); }
سپس همانند قبل، باید تمام سرویسهای مدنظر تنظیم شوند تا بتوان از آنها استفاده کرد.
نکتهی مهم دیگری را که باید به آن دقت داشت، ایجاد scope و سپس دسترسی به سرویسها از طریق این Scope است. از این جهت که چون خارج از طول عمر یک درخواست وب قرار داریم، دیگر Scopeها برای ما به صورت خودکار ایجاد و تخریب نمیشوند و باید همانکاری را که ASP.NET Core در پشت صحنه انجام میدهد، به صورت دستی پیاده سازی کنیم:
using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) {
یک نکتهی تکمیلی
EF Core به همراه تامین کنندهی بانک اطلاعاتی SQLite نیز هست. یکی از نکات ویژهی بانک اطلاعاتی SQLite، امکان تنظیم پارامتری است در رشتهی اتصالی آن، که آنرا نیز تبدیل به یک «بانک اطلاعاتی درون حافظهای» میکند. این روش سالها است که جهت انجام آزمونهای واحد ORMها مورد استفاده قرار میگیرد. بنابراین میتوان آنرا به عنوان جایگزینی برای مطلب جاری نیز درنظر گرفت.
var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqliteConnection(connectionString); services.AddEntityFrameworkSqlite().AddDbContext<CmsDbContext>(options => options.UseSqlite(connection));