تا اینجای کار ما از دیتابیس h2 برای تست های یکپارچمون استفاده میکردیم که چون in memory هست،سرعت بالایی از استارت تا انجام دادن کوئری ها داره. اما این دیتابییس برای پروداکشن مناسب نیست. و اگه میخوایم که بهترین شبیه سازی پروداکشن رو در تست ها داشته باشیم بهتره از دیتابیس های واقعی استفاده کنیم. ممکنه ما یک کدی داشته باشیم که از یک ویژگی خاص مربوط به دیتابیس postgresql استفاده میکنه و دیتابیس h2 از این پشتیبانی نمیکنه. برای اینکار یک راه اینه که یه دیتابیس واقعی رو تو سیستم خودمون بسازیم و تنظیمات مربوطه ش رو انجام بدیم ولی ممکنه که بخواهید نوع دیتابیس رو به یکباره عوض کنید، که باید دیتابیس جدید رو نصب کنید تو سیستمون و دوباره کانفیگش کنید.
کتابخونه Testcontainer به کمک ما اومده که این مراحل رو حذف کنیم و فقط با نوشتن نام و ورژن دیتابیس، دیتابیس مارو بالا بیاره تا بتونیم در تست ها ازش استفاده کنیم
تست کانتینر همونطور که از اسمش میشه استنباط کرد، از داکر برای اینکار استفاده میکنه و حتما باید داکر در سیستمتون نصب باشه
برای پروژه این قسمت از پروژه قبلی استفاده خواهیم کرد فقط لازمه که این وابستگی هارو به پروژه اضافه کنیم:
gradle:
testImplementation "org.testcontainers:testcontainers:1.16.3" testImplementation "org.testcontainers:junit-jupiter:1.16.3" testImplementation 'org.testcontainers:postgresql:1.16.3'
maven:
<dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.16.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.16.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.16.3</version> <scope>test</scope> </dependency>
دپندسی آخری مربوط به دیتابیسی که قراره استفاده کنیم هست و میتونین برای دیتابیس های دیگه از خود سایت Testcontainers از تب مربوط به Modules/databases اقدام کنید.
برای پیاده کردن تست کانتینر میخوایم که از اول با راه هایی موجود شروع کنیم و به یه نقطه ایده آل از پیاده سازی برسیم. در گیت برنچ هایی ایجاد کردم که میتونید تموم این روش ها رو به ترتیب ببنید و در نهایت پیاده سازی ایده آل رو در برنچ main قرار داده ام:
ابتدا اول با استفاده از دستور زیر ایمیج دیتابیس مورد نظر رو از داکر دانلود کنید. برای مثال ما این هست:
docker pull postgres:13.1-alpine
برای اینکه روند کلی رو در این پست نشون بدم فقط تغییرات رو در یک کلاس نشون میدیم و بقیه تست کلاس ها به عهده خودتون هست یا میتونید در برنچ مربوطه این تغییرات رو برای کلاس های دیگه ببینید.
در تست کلاس ProductControllerTest این موارد رو مینویسیم:
... @Testcontainers class ProductControllerTest { ... @Container private final PostgreSQLContainer container = new PostgreSQLContainer("postgres:13.1-alpine") .withDatabaseName("test") .withUsername("username") .withPassword("password"); ... }
اولین انوتیشنی که میبینیم Testcontainers هست که وظیفش اینه که فیلد هایی که درون کلاس با Container انوتیت شدن رو پیدا کنه و کانتینری که در این فیلد تعریف شده است رو اجرا کنه.
در این فیلد یه کانتینر پوستگره ایجاد کردیم که داخل سازنده ش نام ایمیج و ورژنش رو مشخص کرده ایم. در ادامه بصورت استاتیک، نام دیتابیس، یوزرنیم و رمز دیتابیس رو به کانتکس اسپرینگ فهمونده ایم
اگه تست کلاس رو اجرا کنیم خیلی زمان میبره تا کل تست ها پس بشن و برای سیستم من حدود ۳۰ ثانیه هست. دلیلش این هست که یک کانتینر برای هر متد تست ایجاد و حذف میشه. این عمل سرعت رو پایین میاره.راه حل چیه؟ به فیلد کانتینر کلیدواژه static اضافه کنیم. در این حالت این کانتینر فقط یکبار اونم برای این کلاس ایجاد میشه و برای سیستم من حدود ۱۰ ثانیه طول میکشه. ولی بازم از حالت ایده آل فاصله داریم.
حالا فرض کنیم که برای همه تست های پروژه این فیلد و انوتیشن ها رو اضافه کردیم. اگه کل تست های پروژه رو به یکباره اجرا کنیم، اتفاقی که میوفته، اینبار برای هر کلاس یک کانتینر ایجاد میشه و بعد تموم شدن تست های کلاس، کانتینر حذف میشه و برای کلاس تست بعدی دوباره این عمل انجام میشه. بازم اینکار ایده آل نیست و سرعت اجرای همه تست ها پایین میاد برای من بین ۲۵ تا ۴۰ ثانیه طول کشید. کاری که میکنیم به این صورت هست که تموم کد هایی که برای کانتینر نوشتیم به همراه رو پاک میکنیم و یک کلاس در پکیج تست ها به این صورت ایجاد میکنیم و در همه کلاس های تست از این کلاس ارث بری میکنیم:
public class DatabaseContainer { private static final PostgreSQLContainer container = (PostgreSQLContainer) new PostgreSQLContainer("postgres:13.1-alpine") .withReuse(true); // this script should be run for the first time // echo testcontainers.reuse.enable=true > ~/.testcontainers.properties static { container.start(); } @DynamicPropertySource public static void overrideProps(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", container::getJdbcUrl); registry.add("spring.datasource.username", container::getUsername); registry.add("spring.datasource.password", container::getPassword); registry.add("spring.datasource.driver-class-name", container::getDriverClassName); registry.add("spring.jpa.properties.hibernate.dialect", () -> "org.hibernate.dialect.PostgreSQLDialect"); } }
همونطور که توی یک کامنت میبینید اول این دستور رو اجرا کنید تا تست کانتینر بدونه که قراره از کانتینر دوباره استفاده کنه:
echo testcontainers.reuse.enable=true > ~/.testcontainers.properties
برای ویندوز فایلی به این نام .testcontainers.properties که با نقطه شروع میشود در پوشه home یا همان c:/Users/username ایجاد کنید و مقدار زیر رو داخلش کپی کنید. بدیهی است که username در این مسیر نام کاربری خود شماست:
testcontainers.reuse.enable=true
در فیلدی که تعریف کرده ایم متد withReuse رو فراخونی کردیم که این اجازه رو به تست کانتینر میده تا هربار نیاد برای هر کلاس تست، کانتینر رو ایجاد و حذف کنه بلکه کانتینر رو نگه میداره و برای کلاس بعدی نیاز نیست که دوباره ایجاد بشه.
در متدی که با انوتیشن DynamicPropertySource مشخص کرده ایم به اسپرینگ گفته ایم که در این متد پراپرتی های دیتابیس بصورت دایانامیک یا پویا اضافه میشن. چون تست کانتینر این پراپرتی ها مثل پورت رو رندوم جنریت میکنه تا تداخلی با پورت های دیگه سیستم نداشته باشه نیازی نیست که ما خودمون بصورت دستی این پراپرتی ها رو ست بکنیم.
در متد بعدی هم گفتیم که قبل از همه کلاس ها این کانتینر رو استارت کنه. دقت کنید که مشخص نکردیم بعد تموم شدن تست های پروژه این کانتینر حذف بشه. پس برای دفعه بعدی اجرای کل تست ها، چون کانتینر هنوز در حالت اجراس، سرعت اجرا شدن تست ها بیشتر از قبل میشه.
این روش ارث بری زیاد جالب نیس چون ممکنه بازم همچین کلاس هایی در تست ها داشته باشیم که نیاز باشه ارثبری کنیم. پس از اکستنشن های junit برای اینکار استفاده میکنیم. ابتدا کلاس DatabaseContainer به اینصورت بازنویسی میکنیم:
public class DatabaseContainer implements BeforeAllCallback { private static final PostgreSQLContainer container = (PostgreSQLContainer) new PostgreSQLContainer("postgres:13.1-alpine") .withReuse(true); // this script should be run for the first time // echo testcontainers.reuse.enable=true > ~/.testcontainers.properties @DynamicPropertySource public static void overrideProps(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", container::getJdbcUrl); registry.add("spring.datasource.username", container::getUsername); registry.add("spring.datasource.password", container::getPassword); registry.add("spring.datasource.driver-class-name", container::getDriverClassName); registry.add("spring.jpa.properties.hibernate.dialect", () -> "org.hibernate.dialect.PostgreSQLDialect"); } @Override public void beforeAll(ExtensionContext context) { container.start(); } }
بعد برای هر کلاس تست این انوتیشن رو اضافه میکنیم و ارثبری رو حذف میکنیم:
@ExtendWith(DatabaseContainer.class)
تا اینجای کار ما قدم به قدم تست کانتینر رو ایجاد و تنظیم کردیم تا از اجرای چند دقیقه ای همه تست ها به زیر ده ثانیه رسیدیم.
تست کانیتر قابلیت های زیادی داره، میشه حتی در حالت پروداکشن هم استفاده کرد یا ایمیج هایی مثل rabbitMQ رو هم در کلاس های تست داشته باشیم
لینک پروژه: LINK
انشاالله اگه فرصتی باقی باشه در پست های بعدی درمورد تست کردن بصورت Reactive هم صحبت خواهیم کرد. هرجا سوال، پیشنهاد، اصلاحی داشتید ممنون میشم در کامنتا من و بقیه رو مطلع کنید ?