Jest Entegrasyon Testlerini İzole SQL Şemaları Kullanarak Paralel Çalıştırma Rehberi
Büyük ve karmaşık uygulamalarda entegrasyon testleri, sistemin farklı bileşenlerinin birbiriyle uyumlu çalışıp çalışmadığını doğrulamak için hayati öneme sahiptir. Ancak bu testler, genellikle veritabanı etkileşimleri nedeniyle yavaş çalışır ve sürekli entegrasyon (CI/CD) süreçlerini uzatabilir. Bu makalede, Jest test çatısını kullanarak entegrasyon testlerini paralel çalıştırmanın ve testler arası veri kirliliğini önlemek için izole SQL şemaları kullanmanın pratik yollarını detaylıca inceleyeceğiz. Bu yaklaşım, test süreçlerinizi hızlandırırken, test sonuçlarınızın güvenilirliğini artıracaktır.
1. Entegrasyon Testlerinde Paralelliğin ve İzolasyonun Önemi
Entegrasyon testlerinin etkinliği, hızları ve güvenilirlikleri ile doğru orantılıdır. Büyük projelerde yüzlerce, hatta binlerce entegrasyon testi bulunabilir ve bunların seri olarak çalıştırılması saatler sürebilir. Bu durum, geliştiricilerin geri bildirim döngüsünü uzatır ve CI/CD pipeline’larını yavaşlatır.
1.1. Neden Paralel Testler?
Paralel test çalıştırma, test süresini önemli ölçüde kısaltmanın en etkili yollarından biridir. Jest gibi modern test çerçeveleri, testleri birden fazla işlem (worker) üzerinde eş zamanlı olarak çalıştırma yeteneğine sahiptir. Bu sayede, test süresi teorik olarak mevcut CPU çekirdeği sayısına bölünebilir. Örneğin, 100 dakikalık test süresi olan bir proje, 8 çekirdekli bir makinede yaklaşık 12-15 dakikaya düşebilir. Bu hızlanma, geliştiricilerin daha sık test çalıştırmasına ve hataları daha erken tespit etmesine olanak tanır.
1.2. Neden Veritabanı İzolasyonu?
Paralel test çalıştırmanın getirdiği en büyük zorluklardan biri, testler arası veri kirliliğidir. Eğer birden fazla test aynı veritabanı şemasını kullanıyorsa ve bu testler aynı anda çalışıyorsa, bir testin yaptığı değişiklikler diğer testin beklenen sonuçlarını etkileyebilir. Bu durum, testlerin başarısız olmasına veya “titrek testler” (flaky tests) olarak bilinen, bazen geçen bazen kalan testlerin ortaya çıkmasına neden olur. Veritabanı izolasyonu, her testin veya her test worker’ının kendi bağımsız veritabanı ortamına sahip olmasını sağlayarak bu sorunu ortadan kaldırır. Böylece her test, temiz ve öngörülebilir bir ortamda çalışır ve sonuçlar güvenilir olur.
1.3. Ortak Sorunlar ve Çözümleri
| Sorun | Açıklama | Çözüm |
| :———————– | :——————————————————————— | :———————————————————————————————————————————– |
| Yavaş Test Süreleri | Tüm entegrasyon testlerinin seri olarak çalıştırılması. | Jest’in paralel test çalıştırma yeteneklerini kullanma. |
| Veri Kirliliği | Testlerin paylaşılan veritabanı üzerinde birbirini etkilemesi. | Her test worker’ı için izole SQL şemaları veya veritabanları oluşturma. |
| Titrek Testler | Testlerin bazen geçip bazen kalması, güvenilir olmaması. | Veritabanı izolasyonu ile testlerin bağımsızlığını sağlama. |
| Karmaşık Setup/Teardown | Her test öncesi ve sonrası veritabanı durumunu yönetmek. | Jest’in globalSetup, globalTeardown ve özel ortam (jest-environment) özelliklerini kullanma. |
2. Jest’in Paralel Test Yetenekleri
Jest, Node.js projeleri için popüler bir test çerçevesidir ve kutudan çıktığı haliyle paralel test çalıştırma yeteneğine sahiptir. Bu özellik, test süresini optimize etmek için oldukça önemlidir.
2.1. Jest’in Çalışma Prensibi
Jest, varsayılan olarak test dosyalarını ayrı Node.js süreçlerinde (worker’larda) paralel olarak çalıştırır. Her worker, kendi bağımsız ortamında çalışır ve bu, testlerin birbirini etkilemesini engellemek için bir temel sağlar. Ancak, veritabanı gibi dış kaynaklar söz konusu olduğunda ek izolasyon mekanizmalarına ihtiyaç duyulur. Jest, testleri --runInBand bayrağı ile tek bir süreçte de çalıştırabilir; bu, hata ayıklama (debugging) için kullanışlıdır ancak paralel çalıştırmanın hız avantajından mahrum bırakır.
2.2. Jest Yapılandırması ve Paralellik
Jest’in paralel çalıştırma davranışını jest.config.js dosyasında yapılandırabilirsiniz:
// jest.config.js
module.exports = {
// ... diğer Jest yapılandırmaları ...
testEnvironment: 'node', // Varsayılan ortam
maxWorkers: '50%', // Mevcut CPU çekirdeklerinin %50'si kadar worker kullan
// Veya belirli bir sayı: maxWorkers: 4,
// Veya 'auto' Jest'in otomatik olarak belirlemesini sağlar
// testSequencer: './my-custom-sequencer.js', // Test çalıştırma sırasını özelleştirmek için
};
maxWorkers seçeneği, Jest'in kaç paralel worker süreci başlatacağını kontrol eder. 'auto' veya yüzde değeri kullanmak, farklı makinelerde esneklik sağlar.
2.3. Test Dosyası Yapısı
Paralel testlerden en iyi şekilde yararlanmak için test dosyalarınızın bağımsız olması önemlidir. Yani, bir test dosyasının diğer bir test dosyasının çalışmasına veya sonucuna bağlı olmaması gerekir. Her test dosyasının kendi beforeAll, afterAll, beforeEach, afterEach döngülerinde kendi ihtiyaç duyduğu veriyi hazırlayıp temizlemesi idealdir. Bu, veritabanı izolasyonu ile birleştiğinde maksimum güvenilirlik sağlar.
3. SQL Şema İzolasyonu Stratejileri
Jest'in paralel test yeteneklerini verimli kullanabilmek için, her paralel çalışan test worker'ının kendi izole veritabanı şemasına sahip olması kritik öneme sahiptir. İşte farklı izolasyon stratejileri:
3.1. Dinamik Şema Oluşturma
Bu strateji, her Jest worker'ı için veritabanında benzersiz bir şema oluşturmayı içerir. Özellikle PostgreSQL gibi şema kavramını güçlü bir şekilde destekleyen veritabanları için uygundur.
* Nasıl Çalışır: Her test worker'ı başladığında, benzersiz bir adla (örneğin, worker ID'si veya rastgele bir UUID kullanarak) yeni bir SQL şeması oluşturur. Testler bu şema içinde çalışır. Testler bittiğinde veya worker kapatıldığında şema silinir.
* Avantajları: Hızlı şema oluşturma/silme, tek bir veritabanı sunucusu üzerinde birden fazla izole ortam.
* Dezavantajları: Tüm veritabanı sistemleri şema kavramını aynı şekilde desteklemeyebilir (örn. MySQL'de "şema" aslında "veritabanı"dır).
3.2. Dinamik Veritabanı Oluşturma
Bu yöntem, şema oluşturmaktan daha güçlü bir izolasyon sağlar ve MySQL gibi veritabanları için daha uygundur.
* Nasıl Çalışır: Her test worker'ı için tamamen yeni bir veritabanı oluşturulur. Bağlantı dizesi, bu yeni veritabanına işaret edecek şekilde güncellenir.
* Avantajları: En üst düzeyde izolasyon, veritabanı seviyesinde güvenlik ve yapılandırma farklılıkları.
* Dezavantajları: Şema oluşturmaya göre daha yavaş olabilir, daha fazla disk alanı ve sunucu kaynağı tüketebilir.
3.3. Veritabanı Kapsayıcıları (Docker)
Testcontainers gibi kütüphanelerle birlikte Docker kapsayıcıları kullanmak, en güçlü izolasyon yöntemlerinden biridir.
* Nasıl Çalışır: Her test worker'ı veya hatta her test dosyası için ayrı bir veritabanı Docker kapsayıcısı başlatılır.
* Avantajları: Tamamen izole ortamlar, gerçek üretim ortamına yakınlık, farklı veritabanı versiyonlarını kolayca test etme.
* Dezavantajları: Kapsayıcı başlatma/durdurma süresi ek yük getirebilir, daha fazla sistem kaynağı gerektirebilir.
3.4. İşlem Bazında Geri Alma (Transaction Rollback)
Bu yöntem, her testin bir veritabanı işlemi (transaction) içinde çalıştırılmasını ve test bittikten sonra işlemin geri alınmasını (rollback) içerir.
* Nasıl Çalışır: Her testten önce bir işlem başlatılır, test çalışır, ve test bittikten sonra işlem geri alınır. Böylece testin yaptığı tüm değişiklikler veritabanından silinir.
* Avantajları: Çok hızlıdır, çünkü fiziksel şema/veritabanı oluşturma/silme yoktur.
* Dezavantajları: Tüm veritabanı işlemleri bu şekilde ele alınamayabilir (örn. DDL işlemleri bazı veritabanlarında işlem içinde geri alınamaz). Ayrıca, test edilen uygulamanın kendisinin işlemleri yönetmesi durumunda çakışmalar yaşanabilir.
Bu makalede ağırlıklı olarak "Dinamik Şema Oluşturma" veya "Dinamik Veritabanı Oluşturma" stratejisine odaklanacağız, zira Jest ortamı ile entegrasyonu nispeten daha kolay ve yaygındır.
4. Jest Ortamının Veritabanı İçin Yapılandırılması
Jest'in paralel testlerini izole SQL şemaları ile çalıştırmak için özel bir Jest ortamı oluşturmamız gerekir. Bu ortam, her worker için benzersiz bir şema adı oluşturacak ve veritabanı bağlantı dizesini buna göre ayarlayacaktır.
4.1. Özel Jest Ortamı (Custom Jest Environment)
Jest, testlerin çalıştırıldığı ortamı özelleştirmenize olanak tanır. jest-environment-node'u genişleterek kendi veritabanı ortamımızı oluşturacağız. Bu ortam, her worker başladığında (setup) bir şema oluşturacak ve bittiğinde (teardown) silecektir.
// jest-environment-db.js
const NodeEnvironment = require('jest-environment-node');
const { v4: uuidv4 } = require('uuid'); // Benzersiz ID için
const { createSchema, dropSchema, setupDatabase } = require('./db-utils'); // Veritabanı yardımcı fonksiyonları
class CustomDbEnvironment extends NodeEnvironment {
constructor(config, context) {
super(config, context);
this.schemaId = test_schema_${uuidv4().replace(/-/g, '_')}; // Benzersiz şema adı
this.databaseUrl = process.env.DATABASE_URL; // Ana veritabanı URL'si
}
async setup() {
await super.setup();
console.log([Jest Worker ${process.env.JEST_WORKER_ID}] Schema setup for: ${this.schemaId});
// 1. Yeni şemayı/veritabanını oluştur
await createSchema(this.schemaId, this.databaseUrl);
// 2. Yeni şemaya/veritabanına işaret eden URL'yi ayarla
this.global.__DATABASE_URL__ = ${this.databaseUrl}?schema=${this.schemaId}; // Örnek: PostgreSQL için
// Veya MySQL için: this.global.__DATABASE_URL__ = this.databaseUrl.replace('/mydb', /${this.schemaId});
// 3. Şema içinde gerekli tabloları/migrasyonları çalıştır
await setupDatabase(this.global.__DATABASE_URL__);
}
async teardown() {
console.log([Jest Worker ${process.env.JEST_WORKER_ID}] Schema teardown for: ${this.schemaId});
await dropSchema(this.schemaId, this.databaseUrl);
await super.teardown();
}
get
Vm(code) {
return super.getVm(code);
}
}
module.exports = CustomDbEnvironment;
4.2. Dinamik Bağlantı Dizesi Yönetimi
jest-environment-db.js dosyasında oluşturulan this.global.__DATABASE_URL__ değişkeni, testlerinizde kullanılacak dinamik veritabanı bağlantı dizesini tutar. Testlerinizdeki veritabanı bağlantı modülü, bu global değişkeni okuyarak doğru şemaya bağlanmalıdır.
4.3. Örnek Jest Yapılandırması (jest.config.js)
Jest'e özel ortamınızı kullanmasını söylemek için jest.config.js dosyasını güncelleyin:
// jest.config.js
module.exports = {
// ... diğer Jest yapılandırmaları ...
testEnvironment: './jest-environment-db.js', // Özel ortamımızı kullan
setupFilesAfterEnv: ['./jest.setup.js'], // Her test dosyası için çalışacak ek kurulumlar
globalSetup: './jest.global-setup.js', // Tüm test paketi için tek seferlik kurulum
globalTeardown: './jest.global-teardown.js', // Tüm test paketi için tek seferlik temizleme
maxWorkers: 'auto', // Paralel worker sayısını otomatik ayarla
};
5. Test Öncesi ve Sonrası İşlemleri Yönetimi
Veritabanı izolasyonunu sağlamak için Jest'in farklı setup/teardown kancalarını doğru şekilde kullanmak önemlidir.
5.1. Global Setup ve Teardown
globalSetup ve globalTeardown fonksiyonları, tüm test paketiniz çalıştırılmadan önce ve sonra yalnızca bir kez çalışır. Bunlar, ana veritabanı bağlantı havuzunu başlatmak veya kapatmak, test veritabanı sunucusunun hazır olduğunu doğrulamak gibi işlemler için idealdir.
// jest.global-setup.js
const { initializeDbConnectionPool } = require('./db-utils');
module.exports = async () => {
console.log('Global Jest setup: Initializing main DB connection pool...');
await initializeDbConnectionPool(process.env.DATABASE_URL); // Ana veritabanı bağlantı havuzunu başlat
// Gerekirse ana veritabanı üzerinde tek seferlik işlemler yapılabilir.
};
// jest.global-teardown.js
const { closeDbConnectionPool } = require('./db-utils');
module.exports = async () => {
console.log('Global Jest teardown: Closing main DB connection pool...');
await closeDbConnectionPool();
};
5.2. Ortam Bazında Setup ve Teardown
jest-environment-db.js içindeki setup() ve teardown() metotları, her Jest worker'ı için ayrı ayrı çalışır. Bu, yukarıda bahsettiğimiz gibi her worker için benzersiz şema oluşturma ve silme işlemleri için mükemmeldir. Ek olarak, her test dosyası için beforeAll ve afterAll kancalarını kullanarak test verilerini tohumlayabilir veya test dosyasına özgü temizlik yapabilirsiniz.
// jest.setup.js (setupFilesAfterEnv içinde tanımlanır)
beforeAll(async () => {
// Her test dosyasından önce çalışır
// Örneğin, bu şemaya test verisi ekleyebiliriz
// const db = getDbConnection(global.__DATABASE_URL__);
// await db.seedData();
console.log([Jest Worker ${process.env.JEST_WORKER_ID}] Test dosya setup: ${expect.getState().currentTestName});
});
afterAll(async () => {
// Her test dosyasından sonra çalışır
console.log([Jest Worker ${process.env.JEST_WORKER_ID}] Test dosya teardown: ${expect.getState().currentTestName});
});
6. Uygulama Adımları ve Kod Örnekleri
Şimdiye kadar bahsettiğimiz kavramları bir araya getiren örnek bir uygulama yapısına göz atalım.
6.1. Proje Yapısı
my-project/
├── src/
│ └── ... uygulama kodları ...
├── tests/
│ ├── integration/
│ │ └── user.integration.test.js
│ └── jest.setup.js
├── jest.config.js
├── jest-environment-db.js
├── jest.global-setup.js
├── jest.global-teardown.js
├── db-utils.js # Veritabanı şema/bağlantı yardımcı fonksiyonları
└── package.json
6.2. Veritabanı Bağlantı Modülü (db-utils.js)
Bu modül, veritabanı şemalarını yönetmek ve bağlantı havuzunu sağlamak için kullanılır.
// db-utils.js (PostgreSQL örneği)
const { Pool } = require('pg');
let mainPool; // Ana veritabanı için bağlantı havuzu
const workerPools = new Map(); // Her worker için ayrı havuz tutabiliriz
async function initializeDbConnectionPool(databaseUrl) {
if (!mainPool) {
mainPool = new Pool({ connectionString: databaseUrl });
await mainPool.query('SELECT 1'); // Bağlantıyı test et
console.log('Main DB connection pool initialized.');
}
return mainPool;
}
async function closeDbConnectionPool() {
if (mainPool) {
await mainPool.end();
mainPool = null;
console.log('Main DB connection pool closed.');
}
// Worker havuzlarını da kapat (isteğe bağlı, Jest ortamı kendi kapatabilir)
for (const pool of workerPools.values()) {
await pool.end();
}
workerPools.clear();
}
async function createSchema(schemaName, databaseUrl) {
const pool = await initializeDbConnectionPool(databaseUrl);
await pool.query(CREATE SCHEMA IF NOT EXISTS ${schemaName});
console.log(Schema ${schemaName} created.);
}
async function dropSchema(schemaName, databaseUrl) {
const pool = await initializeDbConnectionPool(databaseUrl);
await pool.query(DROP SCHEMA IF EXISTS ${schemaName} CASCADE);
console.log(Schema ${schemaName} dropped.);
}
async function setupDatabase(workerDatabaseUrl) {
// Bu fonksiyon, yeni oluşturulan şema içinde tabloları oluşturur, migrasyonları çalıştırır.
// Gerçek bir uygulamada, burada bir ORM (TypeORM, Sequelize vb.) veya migrasyon aracı (Knex, Flyway) kullanırsınız.
const pool = new Pool({ connectionString: workerDatabaseUrl });
workerPools.set(process.env.JEST_WORKER_ID, pool); // Worker havuzunu kaydet
await pool.query(CREATE TABLE IF NOT EXISTS ${workerDatabaseUrl.split('schema=')[1]}.users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
););
console.log(Tables created in schema for worker ${process.env.JEST_WORKER_ID});
return pool;
}
function getDbConnection(workerDatabaseUrl) {
// Testlerinizin kullanacağı bağlantı havuzunu döndürür
const pool = workerPools.get(process.env.JEST_WORKER_ID);
if (!pool) {
throw new Error(No DB pool found for worker ${process.env.JEST_WORKER_ID});
}
return pool;
}
module.exports = {
initializeDbConnectionPool,
closeDbConnectionPool,
createSchema,
dropSchema,
setupDatabase,
getDbConnection,
};
Not: Yukarıdaki workerDatabaseUrl.split('schema=')[1] kısmı PostgreSQL'e özgüdür. MySQL gibi veritabanlarında CREATE DATABASE ve USE komutlarını kullanmanız gerekebilir.
6.3. Örnek Entegrasyon Testi (user.integration.test.js)
// tests/integration/user.integration.test.js
const { getDbConnection } = require('../../db-utils');
describe('User Integration Tests', () => {
let db;
beforeAll(async () => {
// Her test dosyasından önce, bu worker'a ait veritabanı bağlantısını al
db = getDbConnection(global.__DATABASE_URL__);
// Test verilerini tohumla (isteğe bağlı)
await db.query(INSERT INTO ${global.__DATABASE_URL__.split('schema=')[1]}.users (name, email) VALUES ('Test User', 'test@example.com'));
});
afterAll(async () => {
// Test dosyasından sonra temizlik yap (isteğe bağlı, şema silindiği için genellikle gerekmez)
});
it('should create a new user', async () => {
const schemaName = global.__DATABASE_URL__.split('schema=')[1];
const result = await db.query(INSERT INTO ${schemaName}.users (name, email) VALUES ($1, $2) RETURNING *, ['New User', 'new@example.com']);
expect(result.rows[0]).toHaveProperty('id');
expect(result.rows[0].name).toBe('New User');
expect(result.rows[0].email).toBe('new@example.com');
});
it('should retrieve a user by email', async () => {
const schemaName = global.__DATABASE_URL__.split('schema=')[1];
const result = await db.query(SELECT * FROM ${schemaName}.users WHERE email = $1, ['test@example.com']);
expect(result.rows.length).toBe(1);
expect(result.rows[0].name).toBe('Test User');
});
// ... diğer testler ...
});
6.4. Çalıştırma ve Doğrulama
Tüm bu yapılandırmaları yaptıktan sonra, testlerinizi aşağıdaki komutla çalıştırabilirsiniz:
DATABASE_URL="postgresql://user:password@host:port/main_db" jest
JEST_WORKER_ID ortam değişkeni Jest tarafından otomatik olarak ayarlanır. Konsol çıktısında her worker'ın kendi şema adı ile çalıştığını ve şema oluşturma/silme loglarını göreceksiniz.
7. Performans ve Optimizasyon İpuçları
Paralel testleri izole şemalarla çalıştırmak performansı önemli ölçüde artırsa da, daha da optimize etmek için bazı ipuçları:
7.1. Veritabanı Havuzlama (Connection Pooling)
Her test worker'ının kendi veritabanı bağlantı havuzuna sahip olması, performans açısından kritik öneme sahiptir. db-utils.js örneğimizde bunu workerPools ile sağladık. Bu, bağlantı açma/kapama maliyetini azaltır.
7.2. Şema Oluşturma/Silme Hızlandırma
* Şablon Veritabanları (PostgreSQL): PostgreSQL'de CREATE DATABASE new_db TEMPLATE existing_db komutu, mevcut bir veritabanının kopyasını hızlıca oluşturmanıza olanak tanır. Eğer şemanızın başlangıç durumu karmaşıksa, önceden hazırlanmış bir şablon veritabanı kullanmak şema oluşturma süresini kısaltabilir.
* Minimal Migrasyonlar: Her şema oluşturulduğunda sadece entegrasyon testleri için gerçekten gerekli olan tabloları ve migrasyonları çalıştırın. Tüm uygulama migrasyonlarını çalıştırmak gereksiz zaman kaybına yol açabilir.
7.3. Test Verisi Yönetimi
* Fabrikalar (Factories): Test verilerini programatik olarak oluşturmak için fabrika desenlerini kullanın. Faker.js gibi kütüphanelerle gerçekçi ancak sahte veri üretebilirsiniz.
* Minimal Veri Tohumlama (Seeding): Her test veya test dosyası için yalnızca kesinlikle gerekli olan veriyi tohumlayın. Büyük veri kümeleri testleri yavaşlatır.
7.4. Kaynak Yönetimi
* CPU ve RAM: Paralel testler CPU ve RAM yoğun olabilir. CI/CD sunucunuzun yeterli kaynağa sahip olduğundan emin olun. maxWorkers değerini sunucunuzun kapasitesine göre ayarlayın. Çok fazla worker, bağlam değiştirme (context switching) maliyetleri nedeniyle performansı düşürebilir.
* Disk I/O: Veritabanı işlemleri disk I/O'yu yoğun kullanabilir. Hızlı depolama (SSD) kullanmak performansı artırır.
Sonuç
Jest'in paralel test yeteneklerini izole SQL şemaları ile birleştirmek, entegrasyon testlerinizin hızını ve güvenilirliğini devrim niteliğinde artırabilir. Bu rehberde ele aldığımız özel Jest ortamı, dinamik şema yönetimi ve doğru setup/teardown stratejileri ile test süreçlerinizi optimize edebilir, geliştirme döngülerinizi hızlandırabilir ve daha sağlam yazılımlar geliştirebilirsiniz.