💡 بهترین شیوهها برای تست یکپارچهسازی با Testcontainers در NET.
تستهای یکپارچهسازی با Testcontainers قدرتمند هستند، اما اگر از الگوهای صحیح پیروی نکنید، به سرعت میتوانند به یک کابوس نگهداری تبدیل شوند. 😫
من تیمهایی را دیدهام که با تستهای شکننده (flaky)، مجموعه تستهای کند، و سردردهای پیکربندی دست و پنجه نرم میکنند که با رعایت شیوههای بهتر از همان ابتدا، قابل اجتناب بودند.
امروز الگوهایی را به شما نشان خواهم داد که تستهای Testcontainers را قابل اعتماد، سریع و آسان برای نگهداری میکنند.
Testcontainers چگونه تست یکپارچهسازی را تغییر میدهد؟
تستهای یکپارچهسازی سنتی اغلب به دیتابیسهای تست اشتراکی یا جایگزینهای درون-حافظهای (in-memory) تکیه میکنند که با رفتار محیط پروداکشن مطابقت ندارند. شما یا با آلودگی تست بین اجراها سر و کار دارید یا واقعگرایی را فدای سرعت میکنید.
تست کانتینرها این مشکل را با بالا آوردن کانتینرهای واقعی Docker 🐳 برای وابستگیهای شما حل میکند. تستهای شما در برابر PostgreSQL، Redis یا هر سرویس دیگری که در پروداکشن استفاده میکنید، اجرا میشوند. وقتی تستها کامل شدند، کانتینرها از بین میروند و هر بار یک محیط تمیز در اختیار شما قرار میدهند.
این جادو 🪄 از طریق API داکر اتفاق میافتد. Testcontainers کل چرخه حیات را مدیریت میکند: دریافت ایمیجها، شروع کانتینرها، انتظار برای آماده شدن، و پاکسازی. کد تست شما فقط باید بداند چگونه به آنها متصل شود.
پیشنیازها 📦
اول، مطمئن شوید که پکیجهای لازم را دارید:
Install-Package Microsoft.AspNetCore.Mvc.Testing
Install-Package Testcontainers.PostgreSql
Install-Package Testcontainers.Redis
ساختن کانتینرهای تست 🏗
در اینجا نحوه راهاندازی کانتینرهای خود با پیکربندی مناسب آمده است:
PostgreSqlContainer _postgresContainer = new PostgreSqlBuilder()
.WithImage("postgres:17")
.WithDatabase("devhabit")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
RedisContainer _redisContainer = new RedisBuilder()
.WithImage("redis:latest")
.Build();
برای شروع و توقف تمیز کانتینرها در سراسر مجموعه تست خود، IAsyncLifetime را در WebApplicationFactory خود پیادهسازی کنید:
public sealed class IntegrationTestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
// ... تعریف کانتینرها ...
public async Task InitializeAsync()
{
await _postgresContainer.StartAsync();
await _redisContainer.StartAsync();
// وابستگیهای دیگر را اینجا شروع کنید
}
public async Task DisposeAsync()
{
await _postgresContainer.StopAsync();
await _redisContainer.StopAsync();
}
}
این کار تضمین میکند که کانتینرها قبل از اجرای تستها آماده و پس از آن پاکسازی شوند. این یعنی هیچ وضعیت باقیمانده از داکر یا شرایط رقابتی (race conditions) وجود نخواهد داشت.
📌 نکته: نسخههای ایمیج خود را پین کنید (مانند postgres:17) تا از غافلگیریهای ناشی از تغییرات بالادستی جلوگیری کنید.
انتقال پیکربندی به اپلیکیشن شما ✅
بزرگترین اشتباهی که میبینم، هاردکد کردن connection stringها است. Testcontainers پورتهای داینامیک اختصاص میدهد. هیچ چیز را هاردکد نکنید.
در عوض، مقادیر را از طریق WebApplicationFactory.ConfigureWebHost تزریق کنید:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseSetting("ConnectionStrings:Database", _postgresContainer.GetConnectionString());
builder.UseSetting("ConnectionStrings:Redis", _redisContainer.GetConnectionString());
}
📍نکته کلیدی استفاده از متد UseSetting برای انتقال داینامیک connection stringها است. این کار همچنین از هرگونه شرایط رقابتی یا تداخل با تستهای دیگری که ممکن است به صورت موازی اجرا شوند، جلوگیری میکند.
اشتراکگذاری تنظیمات پرهزینه با xUnit Collection Fixtures
💡فیکسچر تست (test fixture) چیست؟
یک فیکسچر یک زمینه اشتراکی برای تستهای شماست که به شما اجازه میدهد منابع پرهزینه مانند دیتابیسها یا message brokerها را یک بار راهاندازی کرده و در چندین تست از آنها استفاده مجدد کنید.
اینجاست که اکثر تیمها دچار مشکل میشوند. انتخاب بین class و collection fixtures بر عملکرد و ایزولهسازی تست تأثیر میگذارد.