پیاده سازی Integration test با docker در NET 7.

مشاهده ویدیو این مقاله.یکی از بزرگترین دغدغه های دنیای برنامه نویسی و تولید نرم‌افزار پیاده‌سازی تست صحیح می باشد.بسیاری از برنامه نویسان بر این تفکر هستند که پیاده‌سازی تست خارج از چرخه تولید نرم‌افزار میباشد، در صورتی که این پیاده‌سازی همراه و موازی با تولید نرم‌افزار است.در این مقاله میخواهم در مورد پیاده‌سازی صحیح Integration test توضیحاتی ارائه دهم، راههای متفاوتی برای استفاده از پایگاه داده و انجام تست ها وجود دارند، راههای همچون in memory database، استفاده از docker و موارد دیگر.اما قبل از ارائه توضیحات در خصوص پیاده‌سازی تست اجازه دهید تفاوت بین in memory database و همچنین استفاده از پایگاه داده عملیاتی را شرح دهم.مزایا و معایب in memory databaseیک in memory database دارای مزایا و هم معایب متفاوتی می باشد. تجزیه و تحلیل مزایا و معایب آن به تعیین اینکه در چه مواردی باید از آن استفاده کرد، کمک می‌کند.مزایاسرعت پردازش: بارزترین مزیت هنگام استفاده از این نوع پایگاه داده، سرعت عملیات خواندن و نوشتن است. از آنجایی که پایگاه داده مبتنی بر دیسک نیست، تکنیک های بهینه‌سازی ، مانند پردازش موازی، امکان‌پذیر است.استفاده Real-timeقابلیت اطمینان: این نوع پایگاه داده ها افزایش حجم کار را مدیریت می‌کنند. بازیابی اطلاعات به جای میلی ثانیه، نانوثانیه طول می کشد، که تفاوت زیادی در افزایش ناگهانی ترافیک ایجاد می کند.معایبخطر از دست دادن اطلاعات: از آنجایی که این نوع پایگاه داده از حافظه فرار استفاده می کنند، ذخیره‌سازی آن موقتی است و این پایگاه داده خطر از دست دادن تمام داده ها را دارد و به اقدامات ایمنی بیشتری نیاز دارد.هزینه بالا: RAM گران‌تر از دیسک های ذخیره‌سازی است، با توجه به اینکه پایگاه داده به ذخیره‌سازی داده های حافظه متکی است، هزینه های کلی مربوط به پایگاه های داده درون حافظه بیشتر از هزینه های یک پایگاه داده سنتی روی دیسک است.پیچیدگی معماری: پایگاه داده بر روی RAM قرار دارد که به طور خودکار RAM کمتری را برای سایر عملیات در دسترس قرار می دهد. یک راه حل، پیاده‌سازی محاسبات شبکه ای است که پیچیدگی معماری را افزایش می دهد.برای پیاده‌سازی In memory Database در NET. میتوانید به این ویدیو مراجعه کنید.پیاده‌سازی integration test توسط پایگاه داده واقعیراه دیگر برای پیاده‌سازی تست خود، استفاده از یک پایگاه داده واقعی است، ما میتوانیم با استفاده از test container پایگاه داده های خود را در زمان تست ایجاد کنیم و سپس تمامی تست خود را در آن پایگاه داده انجام دهیم.پیش نیازبرای اجرای این پروژه نیاز دارید تا Docker desktop در سیستم شما نصب باشد، برای دانلود آن میتوانید به این لینک مراجعه کنید.قدم 1: ایجاد پروژهیک پروژه class library به نام IT.Test.Integration ایجاد میکنیم.قدم 2: نصب پکیج های مورد نیازبرای ایجاد پایگاه داده و ایجاد image و همچنین container نیاز داریم تا package های زیر را نصب کنیم.همچنین در این مقاله برای تست از xUnit استفاده میکنیم.FluentAssertions Microsoft.AspNetCore.Mvc.Testing Microsoft.EntityFrameworkCore.SqlServer Microsoft.NET.Test.Sdk Testcontainers --version 2.4.0 xunitقدم 3: پیکربندی Docker و پایگاه دادهحال زمان آن رسیده است تا پیکربندی تست خود را انجام دهیم، برای این منظور ما به یک factory نیاز داریم تا API خود را در آن ثبت کنیم.بدین منظور یک کلاس به نام CustomItApiFactory ایجاد میکنیم، این کلاس generic می باشد:public class CustomItApiFactory<T> : WebApplicationFactory<T>, IAsyncLifetime where T:class { public Task InitializeAsync() { throw new NotImplementedException(); } public Task DisposeAsync() { throw new NotImplementedException(); } }همانطور که مشاهده می کنید این کلاس از دو کلاس WebApplicationFactory و IAsyncLifetime ارث بری کرده است.کلاس WebApplicationFactory یک factory است که برای راه‌اندازی برانامه ما در حافظه مورد استفاده قرار میگیرد تا بتوانیم تست خود را انجام در یک بازه زمانی انجام دهیم.کلاس IAsyncLifetime، دارای یک طول عمر است که در لحظه شروع، اقدام به ایجاد شی و در نهایت در لحظه نهایی اقدام به dispose کردن شی میکند.حال نوبت آن فرارسیده تا نیازمندی های یک docker container را پیکر بندی کنیم، لازم به ذکر است که من در این مقاله از پایگاه داده SQL Server استفاده خواهم کرد.ابتدا اجازه دهید پیکربندی و نصب پایگاه داده SQL Server را انجام دهیم.private MsSqlTestcontainer? DatabaseTestContainer { get; set; } public CustomItApiFactory() { SetupDatabaseTestContainer(); } private void SetupDatabaseTestContainer() { DatabaseTestContainer = new TestcontainersBuilder<MsSqlTestcontainer>() .WithDatabase(new MsSqlTestcontainerConfiguration { Password = &quotDevl!Fe013&quot, Database = Guid.NewGuid().ToString() }) .WithImage(&quotmcr.microsoft.com/mssql/server:2022-latest&quot) .WithCleanUp(true) .WithName($&quotIntegrationTestDatabaseServer-{Guid.NewGuid().ToString()}&quot) .Build(); }همانطور که ملاحظه میکنید، از MsSqlTestcontainer برای ایجاد یه پایگاه داده SQL Server استفاده کردیم، و سپس تنظیمات مربوط به این پایگاه داده را در کلاس SetupDatabaseTestContainer انجام دادیم، شما میتوانید از نسخه های مختلف image پایگاه داده SQL Server استفاده کنید، لازم به ذکر است شما میتوانید از پایگاه داده های دیگر نیز برای تست خود استفاده کنید و محدود به SQL Server نیست.سپس نیاز دارین تا webhost خود را نیز پیکربندی کنیم، بدین منظور بایدآن را override کنیم.private string DatabaseConnectionString => $&quot{DatabaseTestContainer?.ConnectionString} TrustServerCertificate=True; Encrypt=False;&quot protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { services.RemoveDbContext<ItDbContext>(); services.AddDbContext<ItDbContext>(options => { options.UseSqlServer(DatabaseConnectionString); }); }); // Add database migration and update var migrationsAssemblyName = typeof(ItDbContext).Assembly.GetName().Name; var optionsBuilder = new DbContextOptionsBuilder<ItDbContext>() .UseSqlServer(DatabaseConnectionString, x => x.MigrationsAssembly(migrationsAssemblyName)); using var dbContext = new ItDbContext(optionsBuilder.Options); dbContext.Database.Migrate(); }در این قسمت ابتدا اگر شی dbContext وجود داشته باشید آن را حذف میکنیم و سپس از طریق AddDbContext آن را ثبت میکنیم.سپس نیاز داریم تا عملیات migration پایگاه داده خود را نیز انجام دهیم تا مطابق یک پایگاه داده محیط عملیاتی تمامی جداول، Stored procedure های احتمالی و... را در پایگاه داده خود به‌صورت خودکار ایجاد کنیم.اگر در برنامه خود تنظیمات خاصی مانند دریافت اطلاعات از APIهای دیگر داریم میتوانیم از طریق IConfigurationRoot به آن دسترسی پیدا کنیم.private IConfigurationRoot Configuration { get; set; } private void LoadConfiguration() { var configBuilder = new ConfigurationBuilder() .SetBasePath(Path.Combine(AppContext.BaseDirectory)); Configuration = configBuilder.Build(); }تا اینجا تمامی پیکربندی های لازم برای تست API فراهم شده است، در انتها نیازی داریم تا طول عمر آن را نیز پیکربندی کنیم.public async Task InitializeAsync() => await DatabaseTestContainer?.StartAsync(); public new async Task DisposeAsync() =>await DatabaseTestContainer.StopAsync();فایل CustomItApiFactory در نهایت به شکل زیر خواهد بودpublic class CustomItApiFactory<T> : WebApplicationFactory<T>, IAsyncLifetime where T:class { private MsSqlTestcontainer? DatabaseTestContainer { get; set; } private IConfigurationRoot Configuration { get; set; } private string DatabaseConnectionString => $&quot{DatabaseTestContainer?.ConnectionString} TrustServerCertificate=True; Encrypt=False;&quot public CustomItApiFactory() { SetupDatabaseTestContainer(); LoadConfiguration(); } private void LoadConfiguration() { var configBuilder = new ConfigurationBuilder() .SetBasePath(Path.Combine(AppContext.BaseDirectory)); Configuration = configBuilder.Build(); } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { services.RemoveDbContext<ItDbContext>(); services.AddDbContext<ItDbContext>(options => { options.UseSqlServer(DatabaseConnectionString); }); }); // Add database migration and update var migrationsAssemblyName = typeof(ItDbContext).Assembly.GetName().Name; var optionsBuilder = new DbContextOptionsBuilder<ItDbContext>() .UseSqlServer(DatabaseConnectionString, x => x.MigrationsAssembly(migrationsAssemblyName)); using var dbContext = new ItDbContext(optionsBuilder.Options); dbContext.Database.Migrate(); } private void SetupDatabaseTestContainer() { DatabaseTestContainer = new TestcontainersBuilder<MsSqlTestcontainer>() .WithDatabase(new MsSqlTestcontainerConfiguration { Password = &quotDevl!Fe013&quot, Database = Guid.NewGuid().ToString() }) .WithImage(&quotmcr.microsoft.com/mssql/server:2022-latest&quot) .WithCleanUp(true) .WithName($&quotIntegrationTestDatabaseServer-{Guid.NewGuid().ToString()}&quot) .Build(); } public async Task InitializeAsync() => await DatabaseTestContainer?.StartAsync(); public new async Task DisposeAsync() =>await DatabaseTestContainer.StopAsync(); }قدم 4: تست APIحال نوبت آن فرارسیده است تا API و Endpoint های خود را تست کنیم.بدین منظور یک کلاس به نام EmployeeTests ایجاد میکنیم.public class EmployeeTests:IClassFixture<CustomItApiFactory<IApiMarker>> { }این کلاس از یک IClassFixture ارث بری میکند و سپس factory خود را به عنوان یک پارامتر به آن ارسال میکنیم، کلاس CustomItApiFactory دارای یک پارامتر است تا API خود را به آن ارسال کنیم.بدین منظور شما نیاز دارید تا یک Marker در API خود ایجاد کنید و سپس آن را در CustomItApiFactory ثبت کنید.public interface IApiMarker { }همانطور که میبینید IApiMarker یک interface خالی است که تنها به اسمبلی API ما اشاره میکند.تمامی موارد مربوط به پیکربندی ما انجام شده است و حالا میتوانیم تست خود را انجام دهیم.بدین منظور API توسعه داده شده من دارای یک Endpoint برای ایجاد یک Employee می باشد.[Fact] public async Task Create_An_Employee_Should_Be_Successful() { var request = new HttpRequestMessage(HttpMethod.Post, &quot/Employee&quot); var addEmployeeDto = new AddEmployeeInput(&quotvahid&quot, &quotcheshmy&quot, &quotv.cheshmi@gmail.com&quot); var json = JsonSerializer.Serialize(addEmployeeDto); var content = new StringContent(json, Encoding.UTF8, &quotapplication/json&quot); request.Content = content; var response=await _httpClient.SendAsync(request); response.StatusCode.Should().Be(HttpStatusCode.OK); }کد بالا یک تست برای ایجاد یک Employee را نشان میدهد، این متد از طریق HttpClient اطلاعات مورد نیاز را به endpoint ما ارسال می کند و در نهایت نتیجه این endpoint بازگردانده میشود.در نهایت تست ما به شکل زیر خواهد بود.using System.Net; using System.Text; using IT.API; using IT.API.Dto; using System.Text.Json; using FluentAssertions; using Xunit; namespace IT.Test.Integration.Employees; public class EmployeeTests:IClassFixture<CustomItApiFactory<IApiMarker>> { private readonly HttpClient _httpClient; public EmployeeTests(CustomItApiFactory<IApiMarker> factory) { _httpClient = factory.CreateClient(); } [Fact] public async Task Create_An_Employee_Should_Be_Successful() { var request = new HttpRequestMessage(HttpMethod.Post, $&quot/Employee&quot); var addEmployeeDto = new AddEmployeeInput(&quotvahid&quot, &quotcheshmy&quot, &quotvahid.cheshmy@myDomain.com&quot); var json = JsonSerializer.Serialize(addEmployeeDto); var content = new StringContent(json, Encoding.UTF8, &quotapplication/json&quot); request.Content = content; var response=await _httpClient.SendAsync(request); response.StatusCode.Should().Be(HttpStatusCode.OK); } }قدم 5: اجرای تست حال که تمامی موارد مربوط به تست ما پیاده‌سازی شده است می توانیم تست خود را اجرا کنیم. با اجرای تست خود باید یک پایگاه داده در docker شما ایجاد شود، تست مربوطه انجام شود و در نهایت پایگاه داده dispose شود.خلاصهدر این مقاله توانستیم با استفاده از Docker container یک integration test در پایگاه داده عملیاتی را ایجاد کنیم، همچنین پیکربندی docker و پایگاه داده را انجام دادیم و تفاوت میان in memory database را نیز مطالعه کردیم.برای مشاهده آموزش های بیشتر به این لینک مراجعه کنید.ممنونم بابت لایک، کامنت و دنبال کردن من، باعث میشه با قدرت بیشتری ادامه بدم :)از کدنویسی لذت ببرید!!!