نحوه شبیه سازی وابستگی های واقعی در تست های ادغام با استفاده از Testcontainers
تست یکپارچه سازی چیست؟
هدف از تستهای یکپارچهسازی تایید این است که اجزای مختلف نرمافزار، زیرسیستمها یا برنامههای کاربردی بهخوبی با یکدیگر ترکیب شدهاند.
این یک گام مهم در هرم آزمایش است که میتواند به شما کمک کند تا مشکلاتی را که هنگام ترکیب اجزا ایجاد میشود شناسایی کنید - برای مثال مشکلات سازگاری، ناسازگاری دادهها یا مشکلات ارتباطی.
این مقاله یک راهنمای عملی برای تست های یکپارچه سازی در Go با استفاده از Testcontainers است. ما تست های ادغام را به عنوان آزمایش های ارتباط بین یک برنامه کاربردی و اجزای خارجی مانند پایگاه داده و حافظه پنهان تعریف می کنیم.
فهرست مطالب
روش های مختلف برای اجرای تست های یکپارچه سازی
خدمات خوکچه هندی ما: یک کوتاه کننده URL ساده
تست های واحد با وابستگی های مسخره شده
تست های ادغام با وابستگی های واقعی
تست های یکپارچه سازی با Testcontainers
کانتینرهای تست چگونه کار می کنند
روش های مختلف برای اجرای تست های یکپارچه سازی
این نمودار تنها 3 نوع تست را نشان می دهد - اما انواع دیگری نیز وجود دارد: تست اجزا، تست سیستم، تست بار و غیره.
در حالی که تستهای واحد آسان اجرا میشوند (شما فقط تستها را همانطور که کد خود را اجرا میکنید اجرا میکنید)، تستهای یکپارچهسازی معمولاً به یک داربست نیاز دارند (محیط آزمایش موقت را با پایگاههای داده و سایر وابستگیها چرخانید). در شرکتهایی که در آنها کار کردهام، رویکردهای زیر را برای رسیدگی به مشکل محیط تست یکپارچهسازی دیدهام.
گزینه 1: استفاده از پایگاه داده های دور ریختنی و سایر وابستگی ها، که باید قبل از شروع آزمایش های یکپارچه سازی تهیه شده و پس از آن از بین بروند.
بسته به پیچیدگی برنامه شما، تلاش برای این گزینه می تواند بسیار زیاد باشد، زیرا باید اطمینان حاصل کنید که زیرساخت آماده و در حال اجرا است و داده ها از قبل در یک حالت دلخواه خاص پیکربندی شده اند.
گزینه 2: استفاده از پایگاه های داده مشترک موجود و سایر وابستگی ها. شما ممکن است یک محیط جداگانه برای تست های یکپارچه سازی ایجاد کنید یا حتی از محیط موجود (به عنوان مثال مرحله بندی) که تست های یکپارچه سازی می توانند استفاده کنند، استفاده کنید.
اما در اینجا معایب زیادی وجود دارد و من آن را توصیه نمی کنم. از آنجایی که یک محیط مشترک است، چندین آزمایش می توانند به صورت موازی اجرا شوند و داده ها را به طور همزمان تغییر دهند. پس ممکن است به دلایل متعدد با وضعیت داده ناسازگار مواجه شوید.
گزینه 3: استفاده از تغییرات درون حافظه یا جاسازی شده خدمات مورد نیاز برای آزمایش یکپارچه سازی. اگرچه این رویکرد خوبی است، اما همه وابستگیها دارای تغییرات درون حافظه نیستند، و حتی در صورت وجود، این پیادهسازیها ممکن است آپشن های مشابه پایگاه داده تولید شما را نداشته باشند.
گزینه 4: استفاده از Testcontainers برای بوت استرپ و مدیریت وابستگی های آزمایشی خود در داخل کد آزمایشی خود. این یک ایزوله کامل بین اجرای آزمایشی، تکرارپذیری و تجربه CI بهتر را تضمین می کند. ما در یک ثانیه به آن می پردازیم.
خدمات خوکچه هندی ما: یک کوتاه کننده URL ساده
برای نشان دادن تست ها، از یک API کوتاه کننده URL فوق العاده ساده که در Go نوشته شده است استفاده می کنیم. از MongoDB برای ذخیره سازی داده ها و Redis به عنوان کش خواندنی استفاده می کند. این دارای دو نقطه پایانی است که در آزمایشات خود آنها را آزمایش خواهیم کرد:
/create?url=
هش را برای یک URL داده شده تولید می کند و آن را در پایگاه داده ذخیره می کند.
/get?key=
URL اصلی را برای یک کلید مشخص برمی گرداند.
ما زیاد به جزئیات نقاط پایانی نمی پردازیم، اما می توانید کد کامل را در این مخزن Github بیابید. با این حال، بیایید ببینیم که چگونه ساختار "سرور" خود را تعریف می کنیم:
type server struct { DB DB Cache Cache } func NewServer (db DB, cache Cache) (*server, error) { if err := db.Init(); err != nil { return nil , err } if err := cache.Init(); err != nil { return nil , err } return &server{DB: db, Cache: cache}, nil }
تابع NewServer به ما اجازه می دهد تا یک سرور را با پایگاه داده و نمونه های کش که رابط های DB و Cache را پیاده سازی می کنند، مقداردهی اولیه کنیم.
type DB interface { Init() error StoreURL(url string , key string ) error GetURL(key string ) ( string , error) } type Cache interface { Init() error Set(key string , val string ) error Get(key string ) ( string , bool ) }
تست های واحد با وابستگی های مسخره شده
از آنجایی که ما همه وابستگیها را به عنوان واسط تعریف کردهایم، میتوانیم به راحتی با استفاده از تمسخر برای آنها ماک تولید کنیم و از آنها در تستهای واحد خود استفاده کنیم.
mockery --all --with-expecter go test -v ./...
با کمک تست های واحد، می توانیم اجزای سطح پایین برنامه خود را به خوبی پوشش دهیم: نقاط پایانی، منطق کلید هش و غیره. تنها چیزی که نیاز داریم این است که فراخوانی های تابع پایگاه داده و وابستگی های کش را مسخره کنیم.
unit_test.go:
func TestServerWithMocks (t *testing.T) { mockDB := mocks.NewDB(t) mockCache := mocks.NewCache(t) mockDB.EXPECT().Init().Return( nil ) mockDB.EXPECT().StoreURL(mock.Anything, mock.Anything).Return( nil ) mockDB.EXPECT().GetURL(mock.Anything).Return( "url" , nil ) mockCache.EXPECT().Init().Return( nil ) mockCache.EXPECT().Get(mock.Anything).Return( "url" , true ) mockCache.EXPECT().Set(mock.Anything, mock.Anything).Return( nil ) s, err := NewServer(mockDB, mockCache) assert.NoError(t, err) srv := httptest.NewServer(s) defer srv.Close() // actual tests happen here, see the code in the repository testServer(srv, t) }
mocks.NewDB(t)
و mocks.NewCache(t)
به طور خودکار توسط تمسخر تولید شده اند و ما از EXPECT()
برای تمسخر توابع استفاده می کنیم. توجه داشته باشید که ما یک تابع جداگانه testServer(srv, t)
ایجاد کردیم که بعداً در آزمایشهای دیگر نیز استفاده خواهیم کرد، اما ساختار سرور متفاوتی را ارائه میکنیم.
همانطور که قبلاً متوجه شده اید، این تست های واحد ارتباطات بین برنامه ما و پایگاه داده/کش ما را آزمایش نمی کنند و ممکن است برخی از اشکالات بسیار مهم را به راحتی از دست بدهیم.
برای اطمینان بیشتر در مورد برنامه خود، باید تست های یکپارچه سازی را همراه با تست های واحد بنویسیم تا اطمینان حاصل کنیم که برنامه ما کاملاً کاربردی است.
تست های ادغام با وابستگی های واقعی
همانطور که گزینه 1 و 2 در بالا ذکر شد، ما می توانیم وابستگی های خود را از قبل فراهم کنیم و آزمایش های خود را در برابر این نمونه ها انجام دهیم. یکی از گزینه ها داشتن یک پیکربندی Docker Compose با MongoDB و Redis است که قبل از آزمایش شروع می کنیم و بعد از آن خاموش می کنیم. دادههای اولیه میتوانند بخشی از این پیکربندی باشند یا بهطور جداگانه انجام شوند.
compose.yaml:
services: mongodb: image: mongodb/mongodb-community-server: 7.0 -ubi8 restart: always ports: - "27017:27017" redis: image: redis: 7.4 -alpine restart: always ports: - "6379:6379"
realdeps_test.go:
//go:build realdeps // +build realdeps package main func TestServerWithRealDependencies (t *testing.T) { os.Setenv( "MONGO_URI" , "mongodb://localhost:27017" ) os.Setenv( "REDIS_URI" , "redis://localhost:6379" ) s, err := NewServer(&MongoDB{}, &Redis{}) assert.NoError(t, err) srv := httptest.NewServer(s) defer srv.Close() testServer(srv, t) }
اکنون این تستها از mock استفاده نمیکنند، بلکه فقط به پایگاه داده و حافظه پنهان از قبل ارائه شده متصل میشوند. توجه: ما یک تگ ساخت "realdeps" اضافه کردیم، پس این تست ها باید با مشخص کردن این تگ به صراحت اجرا شوند.
docker-compose up -d go test -tags=realdeps -v ./... docker-compose down
تست های یکپارچه سازی با Testcontainers
با این حال، ایجاد وابستگی های سرویس قابل اعتماد با استفاده از Docker Compose نیاز به دانش خوب در مورد داخلی Docker و نحوه اجرای بهترین فناوری های خاص در یک کانتینر دارد. به عنوان مثال، ایجاد یک محیط تست یکپارچه سازی پویا ممکن است منجر به تداخل پورت، عدم اجرا و در دسترس نبودن کامل کانتینرها و غیره شود.
با Testcontainers، اکنون میتوانیم همین کار را انجام دهیم - اما در مجموعه آزمایشی خود، با استفاده از API زبان خود. این بدان معناست که ما میتوانیم وابستگیهای دور ریختنی خود را بهتر کنترل کنیم و مطمئن شویم که در هر اجرای آزمایشی ایزوله هستند. شما می توانید تقریباً هر چیزی را در Testcontainers اجرا کنید، به شرطی که دارای زمان اجرا کانتینر سازگار با Docker-API باشد.
integration_test.go:
//go:build integration // +build integration package main import ( "context" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/testcontainers/testcontainers-go/modules/mongodb" "github.com/testcontainers/testcontainers-go/modules/redis" ) func TestServerWithTestcontainers (t *testing.T) { ctx := context.Background() mongodbContainer, err := mongodb.Run(ctx, "docker.io/mongodb/mongodb-community-server:7.0-ubi8" ) assert.NoError(t, err) defer mongodbContainer.Terminate(ctx) redisContainer, err := redis.Run(ctx, "docker.io/redis:7.4-alpine" ) assert.NoError(t, err) defer redisContainer.Terminate(ctx) mongodbEndpoint, _ := mongodbContainer.Endpoint(ctx, "" ) redisEndpoint, _ := redisContainer.Endpoint(ctx, "" ) os.Setenv( "MONGO_URI" , "mongodb://" +mongodbEndpoint) os.Setenv( "REDIS_URI" , "redis://" +redisEndpoint) s, err := NewServer(&MongoDB{}, &Redis{}) assert.NoError(t, err) srv := httptest.NewServer(s) defer srv.Close() testServer(srv, t) }
این بسیار شبیه به آزمایش قبلی است: ما فقط دو ظرف را در بالای آزمایش خود مقداردهی کردیم.
اولین اجرا ممکن است مدتی طول بکشد تا تصاویر را دانلود کنید. اما اجراهای بعدی تقریباً فوری هستند.
را با استفاده از Testcontainers آزمایش کنید" width="700" height="562" loading="lazy">
کانتینرهای تست چگونه کار می کنند
برای اجرای آزمایشها با Testcontainers، به یک زمان اجرا کانتینر سازگار با Docker-API یا نصب Docker به صورت محلی نیاز دارید. سعی کنید موتور Docker خود را متوقف کنید و کار نخواهد کرد.
اما این نباید برای اکثر توسعه دهندگان مشکلی ایجاد کند، زیرا امروزه داشتن زمان اجرا Docker در CI/CD یا به صورت محلی یک روش بسیار رایج است. شما به راحتی می توانید این محیط را در Github Actions داشته باشید.
وقتی صحبت از زبانهای پشتیبانیشده به میان میآید، Testcontainers از فهرست بزرگی از زبانها و پلتفرمهای محبوب از جمله Java، .NET، Go، NodeJS، Python، Rust، Haskell و غیره پشتیبانی میکند.
همچنین یک فهرست رو به رشد از پیاده سازی های از پیش پیکربندی شده (موسوم به ماژول) وجود دارد که می توانید در اینجا پیدا کنید. اما همانطور که قبلاً اشاره کردم، می توانید هر تصویر Docker را اجرا کنید.
در Go، می توانید از کد زیر برای تهیه Redis به جای استفاده از یک ماژول از پیش پیکربندی شده استفاده کنید:
// Using available module redisContainer, err := redis.Run(ctx, "redis:latest" ) // Or using GenericContainer req := testcontainers.ContainerRequest{ Image: "redis:latest" , ExposedPorts: [] string { "6379/tcp" }, WaitingFor: wait.ForLog( "Ready to accept connections" ), } redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true , })
نتیجه گیری
در حالی که توسعه و نگهداری تستهای یکپارچهسازی نیازمند تلاش قابل توجهی است، آنها بخش مهمی از SDLC هستند که تضمین میکند اجزا، زیرسیستمها یا برنامهها به خوبی با یکدیگر به عنوان یک گروه کار میکنند.
با استفاده از Testcontainers، میتوانیم تهیه و حذف وابستگیهای دور ریختنی را برای آزمایش سادهسازی کنیم و اجرای آزمایشی را کاملاً ایزوله و قابل پیشبینیتر کنیم.
منابع
مقالات بیشتری را از packagemain.tech کشف کنید
ارسال نظر