یکی از بزرگترین دغدغه های دنیای برنامه نویسی و تولید نرمافزار پیادهسازی تست صحیح می باشد.
بسیاری از برنامه نویسان بر این تفکر هستند که پیادهسازی تست خارج از چرخه تولید نرمافزار میباشد، در صورتی که این پیادهسازی همراه و موازی با تولید نرمافزار است.
در این مقاله میخواهم در مورد پیادهسازی صحیح Integration test توضیحاتی ارائه دهم، راههای متفاوتی برای استفاده از پایگاه داده و انجام تست ها وجود دارند، راههای همچون in memory database، استفاده از docker و موارد دیگر.
اما قبل از ارائه توضیحات در خصوص پیادهسازی تست اجازه دهید تفاوت بین in memory database و همچنین استفاده از پایگاه داده عملیاتی را شرح دهم.
یک in memory database دارای مزایا و هم معایب متفاوتی می باشد. تجزیه و تحلیل مزایا و معایب آن به تعیین اینکه در چه مواردی باید از آن استفاده کرد، کمک میکند.
برای پیادهسازی In memory Database در NET. میتوانید به این ویدیو مراجعه کنید.
راه دیگر برای پیادهسازی تست خود، استفاده از یک پایگاه داده واقعی است، ما میتوانیم با استفاده از test container پایگاه داده های خود را در زمان تست ایجاد کنیم و سپس تمامی تست خود را در آن پایگاه داده انجام دهیم.
برای اجرای این پروژه نیاز دارید تا Docker desktop در سیستم شما نصب باشد،
برای دانلود آن میتوانید به این لینک مراجعه کنید.
یک پروژه class library به نام IT.Test.Integration ایجاد میکنیم.
برای ایجاد پایگاه داده و ایجاد image و همچنین container نیاز داریم تا package های زیر را نصب کنیم.
همچنین در این مقاله برای تست از xUnit استفاده میکنیم.
FluentAssertions Microsoft.AspNetCore.Mvc.Testing Microsoft.EntityFrameworkCore.SqlServer Microsoft.NET.Test.Sdk Testcontainers --version 2.4.0 xunit
حال زمان آن رسیده است تا پیکربندی تست خود را انجام دهیم، برای این منظور ما به یک factory نیاز داریم تا API خود را در آن ثبت کنیم.
بدین منظور یک کلاس به نام CustomItApiFactory ایجاد میکنیم، این کلاس generic می باشد:
همانطور که مشاهده می کنید این کلاس از دو کلاس 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 = "Devl!Fe013", Database = Guid.NewGuid().ToString() }) .WithImage("mcr.microsoft.com/mssql/server:2022-latest") .WithCleanUp(true) .WithName($"IntegrationTestDatabaseServer-{Guid.NewGuid().ToString()}") .Build(); }
همانطور که ملاحظه میکنید، از MsSqlTestcontainer برای ایجاد یه پایگاه داده SQL Server استفاده کردیم، و سپس تنظیمات مربوط به این پایگاه داده را در کلاس SetupDatabaseTestContainer انجام دادیم، شما میتوانید از نسخه های مختلف image پایگاه داده SQL Server استفاده کنید، لازم به ذکر است شما میتوانید از پایگاه داده های دیگر نیز برای تست خود استفاده کنید و محدود به SQL Server نیست.
سپس نیاز دارین تا webhost خود را نیز پیکربندی کنیم، بدین منظور بایدآن را override کنیم.
private string DatabaseConnectionString => $"{DatabaseTestContainer?.ConnectionString} TrustServerCertificate=True; Encrypt=False;" 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 => $"{DatabaseTestContainer?.ConnectionString} TrustServerCertificate=True; Encrypt=False;" 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 = "Devl!Fe013", Database = Guid.NewGuid().ToString() }) .WithImage("mcr.microsoft.com/mssql/server:2022-latest") .WithCleanUp(true) .WithName($"IntegrationTestDatabaseServer-{Guid.NewGuid().ToString()}") .Build(); } public async Task InitializeAsync() => await DatabaseTestContainer?.StartAsync(); public new async Task DisposeAsync() =>await DatabaseTestContainer.StopAsync(); }
حال نوبت آن فرارسیده است تا 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, "/Employee"); var addEmployeeDto = new AddEmployeeInput("vahid", "cheshmy", "v.cheshmi@gmail.com"); var json = JsonSerializer.Serialize(addEmployeeDto); var content = new StringContent(json, Encoding.UTF8, "application/json"); 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, $"/Employee"); var addEmployeeDto = new AddEmployeeInput("vahid", "cheshmy", "vahid.cheshmy@myDomain.com"); var json = JsonSerializer.Serialize(addEmployeeDto); var content = new StringContent(json, Encoding.UTF8, "application/json"); request.Content = content; var response=await _httpClient.SendAsync(request); response.StatusCode.Should().Be(HttpStatusCode.OK); } }
حال که تمامی موارد مربوط به تست ما پیادهسازی شده است می توانیم تست خود را اجرا کنیم. با اجرای تست خود باید یک پایگاه داده در docker شما ایجاد شود، تست مربوطه انجام شود و در نهایت پایگاه داده dispose شود.
در این مقاله توانستیم با استفاده از Docker container یک integration test در پایگاه داده عملیاتی را ایجاد کنیم، همچنین پیکربندی docker و پایگاه داده را انجام دادیم و تفاوت میان in memory database را نیز مطالعه کردیم.
ممنونم بابت لایک، کامنت و دنبال کردن من، باعث میشه با قدرت بیشتری ادامه بدم :)