Pact ile Contract Testing - Microservislerde API Uyumluluğunu Sağlama
TypeScript microservislerde Pact ile consumer-driven contract testing: breaking API değişikliklerini deployment öncesi yakalayın, integration test yükünü azaltın.
Consumer-driven contract testing, geçen bir unit test ile çalışan bir entegrasyon arasındaki boşluğu kapatıyor. Her consumer, provider API’den neye ihtiyaç duyduğunu bildiriyor; provider bunu karşılayabildiğini doğruluyor; deployment gate ise bağımlı bir servisi bozacak release’i durduruyor. Ayrı team’lerin sahiplendiği TypeScript microservisler için varsayılan tercih, can-i-deploy gate’i olan bir Pact Broker’ın arkasındaki Pact. End-to-end suite’i yeniden büyütmeden önce bakılacak yer burası.
Kazanç dar bir alanda ve bunu baştan söylemek sonraki hayal kırıklığını önlüyor. Contract testing servis sınırlarını kontrol ediyor: request şekli, response şekli, status kodları. Business workflow’lar için hâlâ end-to-end test’ler, business logic için hâlâ unit test’ler gerekiyor. Contract testing’in ortadan kaldırdığı şey, consumer’ın mock’ları ile provider’ın gerçek response’larının kimse fark etmeden birbirinden ayrılması.
API Uyumluluğu Sorunu#
Microservislerle çalışırken, geleneksel test stratejilerinin çözmekte zorlandığı belirli teknik sorunlar çıkıyor:
Breaking change’ler sızıyor: Provider team API’deki bir field’ı required’dan optional’a çeviriyor ve buna bağımlı consumer uygulamalarını bozuyor. Unit test’ler geçiyor çünkü mock’lar güncelleniyor, ama production fail oluyor.
Integration test darboğazı: Birkaç servise yayılan bir end-to-end suite kolaylıkla onlarca dakika sürebiliyor. Bu deployment cycle’ları yavaşlatıyor ve team’ler sonuçları beklerken merge conflict’leri yaratıyor.
Mock drift: Consumer unit test’leri, gerçek provider davranışıyla eşleşmeyen mocklanan API response’ları kullanıyor. Test’ler geçiyor, ancak production’da integration fail oluyor çünkü mock hiç API değişikliklerini yansıtacak şekilde güncellenmemiş.
Cross-team koordinasyon overhead’i: Birden fazla consumer team aynı provider API’ye bağımlı, ama değişiklikleri iletmek için sistematik bir yol yok. Breaking change’ler integration sırasında veya daha kötüsü production’da keşfediliyor.
Version uyumluluk takibi: Hangi consumer version’ının hangi provider version’ıyla uyumlu olduğunu belirlemek, servisler scale oldukça çöken manuel bir spreadsheet işine dönüşüyor.
Bu problemler servis sayısı arttıkça katlanıyor. Contract testing, servisler arasında açık, test edilebilir bir contract kurarak bunları ele alıyor.
Consumer-Driven Contract Testing Nedir?#
Consumer-driven contract testing, tipik testing modelini tersine çeviriyor. Provider’ın API’nin ne yapması gerektiğini tanımlayıp buna karşı test etmesi yerine, consumer’lar API’den neye ihtiyaç duyduklarını tanımlıyor. Provider daha sonra bu gereksinimleri karşılayabileceklerini doğruluyor.
Workflow şöyle işliyor:
Consumer test, API ile beklenen etkileşimleri tanımlıyor. Bu test’ten Pact, request’i, beklenen response yapısını ve matching rule’larını açıklayan bir contract dosyası (JSON) oluşturuyor. Provider bu contract’ı fetch edip implementasyonlarının bunu karşılayabildiğini doğruluyor.
Bu yaklaşım diğer testing stratejilerinden farklı:
- E2E test’ler: Tüm sistem entegrasyonunu test eder, yavaş ve brittle
- Mock’lu unit test’ler: İzolasyonda test eder, hızlı ama mock drift’e açık
- Contract test’ler: Servis sınırlarını test eder, API uyumluluğu için hızlı ve doğru
Contract test’ler mevcut katmanların üzerine ekleniyor. API uyumluluğu kapsamını E2E suite’inden alıyorlar; böylece o suite, hatanın gerçekten önemli olduğu journey’lere daralabiliyor.
TypeScript Projelerinde Pact Kurulumu#
Pact’ı development dependency olarak yükle:
npm install --save-dev @pact-foundation/pact
npm install --save-dev @pact-foundation/pact-cli
npm install --save-dev @types/jest
Consumer ve provider test’lerini ayırmak için proje yapını düzenle:
project/
├── src/
│ ├── client/
│ │ └── auth-service-client.ts
│ └── api/
│ └── user-controller.ts
├── tests/
│ ├── pact/
│ │ ├── consumer/
│ │ │ └── auth-service.pact.spec.ts
│ │ └── provider/
│ │ └── user-api.pact.spec.ts
│ └── helpers/
│ └── pact-setup.ts
├── pacts/ # Oluşturulan contract dosyaları
└── package.json
Consumer test’lerini, provider verification’ı ve contract publish’i çalıştırmak için npm script’leri ekle:
{
"scripts": {
"test:pact:consumer": "jest --testMatch='**/pact/consumer/**/*.spec.ts'",
"test:pact:provider": "jest --testMatch='**/pact/provider/**/*.spec.ts'",
"pact:publish": "npx pact-broker publish ./pacts --consumer-app-version=$GIT_COMMIT --broker-base-url=$PACT_BROKER_URL --broker-token=$PACT_BROKER_TOKEN --branch=$GIT_BRANCH",
"pact:can-deploy": "npx pact-broker can-i-deploy --pacticipant=user-service --version=$GIT_COMMIT --to=production --broker-base-url=$PACT_BROKER_URL --broker-token=$PACT_BROKER_TOKEN"
}
}
Consumer Contract Test Yazımı#
Consumer test’leri beklenen API etkileşimlerini belirterek contract’ı tanımlıyor. İşte basit bir örnek:
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { AuthServiceClient } from '@/client/auth-service-client';
import path from 'path';
const { like, string, eachLike } = MatchersV3;
const provider = new PactV3({
consumer: 'user-service',
provider: 'auth-service',
dir: path.resolve(__dirname, '../../../pacts')
});
describe('Auth Service Contract', () => {
test('user profile getirir', async () => {
await provider
.given('ID 123 olan user var')
.uponReceiving('user profile için request')
.withRequest({
method: 'GET',
path: '/users/123',
headers: { 'Authorization': like('Bearer token123') }
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: string('123'),
email: string('[email protected]'),
name: string('John Doe'),
roles: eachLike('user')
})
})
.executeTest(async (mockserver) => {
const client = new AuthServiceClient(mockserver.url);
const user = await client.getUser('123');
expect(user.email).toBe('[email protected]');
});
});
});
Etkili consumer test’leri yazmanın temel prensipleri:
1. Gerçek consumer code’u çalıştır: Sadece axios gibi bir library ile HTTP request’leri yapma. Gerçek HTTP client implementasyonunu kullan:
// YAPMA - gerçek client test edilmiyor
.executeTest(async (mockserver) => {
const response = await axios.get(`${mockserver.url}/users/123`);
expect(response.data.email).toBe('[email protected]');
});
// YAP - gerçek client test ediliyor
.executeTest(async (mockserver) => {
const client = new AuthServiceClient(mockserver.url);
const user = await client.getUser('123');
expect(user.email).toBe('[email protected]');
});
2. Flexible matching kullan: Tam değerler değil, type’ları belirt. Bu alakasız data değiştiğinde kırılan brittle test’leri önlüyor:
// KÖTÜ: Over-specified contract
.willRespondWith({
status: 200,
body: {
id: '123',
email: '[email protected]',
createdAt: '2024-01-15T10:30:00.000Z',
updatedAt: '2024-01-15T10:30:00.000Z',
// Consumer'ın aslında kullanmadığı birçok field
}
})
// İYİ: Sadece consumer'ın kullandığını belirt
.willRespondWith({
status: 200,
body: like({
id: string('123'),
email: string('[email protected]')
})
})
3. Sadece kullandığını test et: Consumer’ın aslında ihtiyaç duymadığı API field’larını contract’a ekleme. Bu provider’ın kullanılmayan API kısımlarını senin contract’ını bozmadan geliştirmesini sağlıyor.
4. Provider bug’larını test etme: Tam hata mesajlarını veya internal provider davranışını belirtme:
// YAPMA
.willRespondWith({
status: 400,
body: { error: 'Invalid email format: must contain @' }
})
// YAP
.willRespondWith({
status: 400,
body: like({ error: string('validation error') })
})
Pact Matcher Referansı#
Matcher’lar tam değerler yerine type ve yapıyı doğrulayan esnek contract rule’ları tanımlıyor:
import { MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, atLeastLike, regex, iso8601DateTime, number, string } = MatchersV3;
const productResponse = like({
id: string('prod-123'), // Herhangi bir string değer
name: string('Laptop'), // Herhangi bir string değer
price: number(999.99), // Herhangi bir number değer
sku: regex('SKU-[0-9]+', 'SKU-12345'), // Pattern'e uymalı
createdAt: iso8601DateTime('2024-01-15T10:30:00Z'), // ISO8601 format
tags: eachLike('electronics'), // String array'i
reviews: atLeastLike(
{ rating: number(5), comment: string('Great!') },
2 // En az 2 review
)
});
Matcher kullanım guideline’ları:
like(): En yaygın - type ve yapıyı match edereachLike(): Tüm elemanların bir pattern’e match ettiği array’ler içinatLeastLike(): Minimum array uzunluğu önemli olduğundaregex(): SKU, email pattern’leri gibi formatlar için az kullan- Değer semantik olarak anlamlı olmadığı sürece (API version numaraları gibi) tam değer matching’den kaçın
POST/PUT Request Testleri#
Request body’lerini test etmek consumer’ların doğru data gönderdiğini garanti ediyor:
test('yeni user oluşturur', async () => {
const newUser = {
email: '[email protected]',
name: 'Jane Smith',
roles: ['user']
};
await provider
.given('[email protected] email\'li user yok')
.uponReceiving('user oluşturma request\'i')
.withRequest({
method: 'POST',
path: '/users',
headers: {
'Content-Type': 'application/json',
'Authorization': like('Bearer token123')
},
body: like(newUser)
})
.willRespondWith({
status: 201,
headers: { 'Content-Type': 'application/json' },
body: like({
id: string('user-456'),
...newUser,
createdAt: iso8601DateTime()
})
})
.executeTest(async (mockserver) => {
const client = new AuthServiceClient(mockserver.url);
const created = await client.createUser(newUser);
expect(created.email).toBe(newUser.email);
});
});
Contract hem consumer’ın gönderdiği request formatını hem de beklediği response yapısını doğruluyor.
State Handler’larla Provider Verification#
Provider verification, gerçek API implementasyonunun consumer contract’larını karşılayabildiğini test ediyor. Bu, her provider state için uygun test data’sı kurmayı gerektiriyor:
import { Verifier } from '@pact-foundation/pact';
import { db } from '@/database';
const opts = {
provider: 'auth-service',
providerBaseUrl: 'http://localhost:3000',
pactBrokerUrl: process.env.PACT_BROKER_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: true,
providerVersion: process.env.GIT_COMMIT,
stateHandlers: {
'ID 123 olan user var': {
setup: async () => {
await db.users.insert({
id: '123',
email: '[email protected]',
name: 'John Doe',
roles: ['user']
});
return { userId: '123' };
},
teardown: async () => {
await db.users.delete({ id: '123' });
}
},
'hiç user yok': {
setup: async () => {
await db.users.deleteAll();
}
}
}
};
describe('Provider Verification', () => {
beforeAll(async () => {
// Provider API'yi localhost:3000'de başlat
});
afterAll(async () => {
// Provider API'yi durdur
});
test('tüm consumer contract\'larını verify eder', async () => {
await new Verifier(opts).verifyProvider();
});
});
State handler’lar izole, tekrarlanabilir test’ler için kritik. Her provider state tam olarak o test senaryosu için gereken data’yı kuruyor, sonra temizliyor.
Parallel test execution için paylaşılan test data’sından kaçın:
stateHandlers: {
'user\'ın 3 order\'ı var': async () => {
const testUserId = `test-user-${Date.now()}`;
await createTestUser(testUserId);
await createOrders(testUserId, 3);
return { userId: testUserId }; // Return edilen data request'lerde kullanılabilir
}
}
Pact Broker Entegrasyonu#
Pact Broker, tam consumer-driven workflow’u sağlayan merkezi contract repository. Open-source version’ı self-host edebilir veya PactFlow (hosted SaaS) kullanabilirsin:
Docker ile self-hosted:
docker run -d -p 9292:9292 \
-e PACT_BROKER_DATABASE_URL=postgres://user:pass@host/pactdb \
pactfoundation/pact-broker
Consumer’dan contract publish etme:
npx pact-broker publish ./pacts \
--consumer-app-version=$GIT_COMMIT \
--broker-base-url=$PACT_BROKER_URL \
--broker-token=$PACT_BROKER_TOKEN \
--branch=$GIT_BRANCH
Provider’dan verification sonuçlarını publish etme:
Bu Verifier option’larında publishVerificationResult: true set edildiğinde otomatik oluyor.
can-i-deploy kullanma:
can-i-deploy feature’ı bir consumer version’ının deploy edilmiş provider version’larıyla uyumlu olup olmadığını kontrol ediyor:
import { canDeploy } from '@pact-foundation/pact-cli';
const canDeployOpts = {
pacticipant: 'user-service',
version: process.env.GIT_COMMIT,
to: 'production',
pactBroker: process.env.PACT_BROKER_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN
};
const result = await canDeploy(canDeployOpts);
if (!result.success) {
throw new Error('Deploy edilemez: uyumsuz contract\'lar var');
}
Deployment pipeline’ındaki bu gate, uyumsuz servis version’larını deploy etmeyi önlüyor.
CI/CD Entegrasyonu#
Contract testing sadece deployment pipeline’ına entegre edildiğinde değer sağlıyor. İşte bir GitHub Actions örneği:
name: Contract Tests
on:
push:
branches: [main]
pull_request:
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install dependencies
run: npm ci
- name: Consumer contract test'lerini çalıştır
run: npm run test:pact:consumer
- name: Pact'leri broker'a publish et
if: github.ref == 'refs/heads/main'
run: npm run pact:publish
env:
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
GIT_COMMIT: ${{ github.sha }}
GIT_BRANCH: ${{ github.ref_name }}
provider-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Provider API'yi başlat
run: npm start &
- name: API'yi bekle
run: npx wait-on http://localhost:3000/health
- name: Provider verification'ı çalıştır
run: npm run test:pact:provider
env:
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
GIT_COMMIT: ${{ github.sha }}
can-i-deploy:
needs: [consumer-tests, provider-tests]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deployment güvenliğini kontrol et
run: npx pact-broker can-i-deploy \
--pacticipant user-service \
--version ${{ github.sha }} \
--to production \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }}
Pipeline flow’u:
- Consumer test’leri çalışıp pact dosyaları oluşturuyor
- Pact’ler broker’a publish ediliyor (sadece main branch)
- Provider test’leri broker’dan pact’leri fetch edip verify ediyor
- Verification sonuçları publish ediliyor
- can-i-deploy deployment’ın güvenli olup olmadığını kontrol ediyor
- Deployment sadece contract’lar karşılanıyorsa devam ediyor
Breaking Change Yönetimi#
Breaking change’ler bir migration stratejisi gerektiriyor. İşte bir field type değişikliğini handle etmenin yolu:
Senaryo: Provider phone’u required’dan optional’a çevirmek istiyor.
// Eski contract (consumer bekliyor)
{
phone: string('555-1234') // Required field
}
// Provider phone'u optional yapmak istiyor
{
phone: string('555-1234') | null
}
Provider bu değişikliği direkt yaparsa, consumer contract verification fail oluyor. Migration pattern’i:
- Provider yeni optional field’ı eski required field’ın yanına ekliyor
- Consumer’lar yavaş yavaş yeni field’ı kullanmaya geçiyor
- Tüm consumer’lar migrate olduktan sonra, provider eski field’ı kaldırıyor
Doğru contract’lara karşı test etmek için version selector’ları kullanma:
const opts = {
provider: 'auth-service',
providerBaseUrl: 'http://localhost:3000',
pactBrokerUrl: process.env.PACT_BROKER_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
consumerVersionSelectors: [
{ mainBranch: true }, // Main'den en son
{ deployedOrReleased: true }, // Şu an production'da
{ matchingBranch: true } // Provider ile aynı branch
],
enablePending: true, // Yeni contract'larda fail olma
includeWipPactsSince: '2024-01-01' // Work-in-progress pact'leri dahil et
};
enablePending feature’ı yeni consumer contract’ların provider build’ini fail etmeden verify edilmesini sağlıyor. Bu ne consumer ne provider’ın önce deploy edilebileceği chicken-and-egg deployment sorunlarını önlüyor.
Yaygın Tuzaklar ve Çözümler#
Tuzak 1: Over-Specified Contract’lar#
API response’daki her field’ı test etmek contract’ları brittle yapıyor. Provider kullanılmayan field’ları consumer test’lerini bozmadan geliştiremez.
Çözüm: Sadece consumer’ın gerçekten kullandığı field’ları belirt. Kullanılmayan field’ları kaldırmak için contract’ları periyodik olarak gözden geçir.
Tuzak 2: Pact’ı Genel Stub Olarak Kullanmak#
Pact mock provider’ı integration test’lerinde verification çalıştırmadan kullanmak amacı bozuyor.
Çözüm: Pact’ı sadece contract testing için kullan. Genel stubbing için MSW veya WireMock gibi araçları kullan.
Tuzak 3: Random Test Data#
Contract’larda new Date().toISOString() veya uuid() kullanmak her test çalıştırmasında değişmelerine neden oluyor.
Çözüm: Statik test data’sı veya matcher’lar kullan:
// YAPMA
body: { createdAt: new Date().toISOString() }
// YAP
body: { createdAt: iso8601DateTime('2024-01-15T10:30:00Z') }
Tuzak 4: Gerçek Consumer Code Test Etmemek#
Gerçek HTTP client’ını çalıştırmayan Pact test’leri yazmak client bug’larını kaçırıyor.
Çözüm: Contract test’lerinde her zaman production client code’unu kullan. Test, uygulamanın çağırdığı method’ları çağırmalı.
Tuzak 5: Broker Güvenilirliği#
Pact Broker downtime’ı can-i-deploy kontrolleri fail olursa deployment’ları blokluyor.
Çözüm: Güvenilirlik için PactFlow gibi hosted bir çözüm kullan veya self-hosted broker’lar için high availability implement et. Monitoring kur ve outage’lar için fallback bir süreç oluştur.
Test Stratejisinde Contract Testing’in Yeri#
Contract test’lerin diğer test katmanlarına göre yeri şöyle:
Contract testing ne zaman kullanılmalı:
- Servisler HTTP API’ler üzerinden iletişim kuruyor
- Farklı servisler birden fazla team’e ait
- API uyumluluğunu garanti etmek gerekiyor
- E2E test’lerden daha hızlı feedback isteniyorsa
Contract testing ne zaman KULLANILMAMALI:
- Tek team tüm servislere sahipse (integration test’leri kullan)
- UI-to-backend testing (E2E test’leri kullan)
- Business workflow testing (E2E test’leri kullan)
- Performance testing (load test’leri kullan)
Dengeli testing yaklaşımı:
| Test Türü | Kapsam | Hız | Güven | Ne Zaman Kullanılmalı |
|---|---|---|---|---|
| Unit | Tek fonksiyon | Hızlı (ms) | Düşük | Business logic |
| Contract | API boundary | Hızlı (sn) | Orta | Servis uyumluluğu |
| Integration | Birden fazla servis | Orta (dk) | Yüksek | Servis etkileşimi |
| E2E | Tüm sistem | Yavaş (dk) | En yüksek | Kritik workflow’lar |
E2E suite’ini kritik user journey’ler ve gece birini uyandıracak hatalar için sakla. API varyasyonları ile edge case’lerin yeri bir alt katman, yani contract test’ler: orada dakikalar yerine saniyeler içinde çalışıyor ve kırmızı bir pipeline yerine belirli bir field’ı işaret ediyorlar.
Bu Varsayılan Nerede Geçerli#
Contract testing, ayrı team’ler ayrı HTTP servisleri sahiplendiğinde ve provider API’ler koordinasyonu sürekli bir toplantıya çevirecek sıklıkta değiştiğinde maliyetini çıkarıyor. Maliyet de az değil. Provider state’leri izole test data’sı istiyor, matcher’lar gevşek kalabilmek için disiplin istiyor ve broker, birinin ayakta tutup izlemesi gereken bir altyapı parçasına dönüşüyor.
Zincirdeki tüm servisler tek bir team’e aitse varsayılanı bir kenara bırak. Orada gerçek bağımlılıklara karşı yazılan integration test’ler, işletilecek bir broker olmadan aynı güveni veriyor. Yılda birkaç kez değişen bir API için de durum aynı: pipeline kurulumu, yakalayacağı bozulmalardan daha fazla ilgi istiyor.
Benimseyeceksen tek bir consumer/provider çiftiyle, can-i-deploy gate’i dahil olacak şekilde uçtan uca başla. Gate olmadan publish edilen contract’lar dokümantasyondan ibaret kalıyor, dokümantasyon da çalışan sistemden zamanla kopuyor. Pipeline bir kez kurulduktan sonra ikinci çifti eklemek neredeyse bedava; öğrenme birinci çiftte oluyor.
Kaynaklar#
- Pact Dokümantasyonu - Giriş (yeni sekmede açılır) - Consumer-driven contract testing kavramlarını, başlangıç adımlarını ve Pact’in nasıl çalıştığını anlatan resmi Pact dokümantasyonu
- Pact Nasıl Çalışır - Pact Docs (yeni sekmede açılır) - Consumer test’i, pact dosyası, broker ve provider verification döngüsünün adım adım açıklaması
- Contract Test - Martin Fowler (yeni sekmede açılır) - Fowler’ın contract test tanımı ve bu test’lerin test double’larla ilişkisi
- Consumer-Driven Contracts: A Service Evolution Pattern - Martin Fowler (yeni sekmede açılır) - Evrilen servis arayüzleri için consumer-driven contract tasarımı üzerine temel makale
- PactFlow Dokümantasyonu (yeni sekmede açılır) - CI/CD entegrasyonu, can-i-deploy gate’leri ve bi-directional contract testing sunan yönetilen Pact Broker servisi
İlgili yazılar
Distributed sistemlerde feature flag için production rehberi: LaunchDarkly, Unleash ve AWS AppConfig karşılaştırması, rollout ve A/B testing örnekleri.
feature-flags · devops · ci-cd +5
AWS Lambda, API Gateway, DynamoDB ve Step Functions için hızlı geri bildirim ve production güvenilirliği sağlayan kapsamlı bir test stratejisi oluşturmayı öğrenin.
lambda · testing · serverless +8
Bruno .bru dosyalarını repo'ya commit etmek, API sözleşmesini kodla aynı PR ve geçmişte tutar. Tek gerçek bedel, bilinçli bir secret sınırıdır.
testing · ci-cd · developer-experience +1
DynamoDB'de OFFSET, keyfi ORDER BY ve ucuz COUNT yok. İmzalı next/prev cursor sunun, sıralamayı sort key ile modelleyin, toplam sayıyı listeden çıkarın.
dynamodb · aws · data-storage-orm +2
Acele etmek hızlı hissettirir ama yeniden iş, hata ve yangın söndürme yaratır. Refactor, test ve CI bakımı için durmak neden hız kaybı değil, hıza yatırımdır.
technical-debt · testing · ci-cd +2