Git Dostu API Client'ı: Bruno ile Collection'ı Repo'da Tutmak, OpenCollection YAML ve CLI ile CI'da Koşmak
Bruno collection'ı OpenCollection YAML dosyaları olarak repo'da tutup GitHub Actions'ta Bruno CLI ile koşmak, istek değişikliklerini code review'da okunur bir diff'e çeviriyor. Bookstore için auth miras alan bir collection, OpenAPI ve Postman import'u (pm.variables.get'in bru.getVar'a yanlış çevrilmesi), .env ile secret'lar, assertion ve test script'leri, JUnit/HTML raporu, rapor klasörü olmayınca 2 dönen CLI ve YAML collection'ındaki .bru dosyasını sessizce atlayan run gerçek koşularla.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; collection Bruno CLI 4.3 (@usebruno/cli) ile çalıştırılmış, workflow actionlint 1.7 ile kontrol edilmiştir. Bruno desktop uygulamasının arayüzü ve fiyatlandırması resmi doküman ve sürüm notlarından aktarılmıştır. GitHub Actions workflow'unu GitHub'da değil, adımlarını yerelde aynı sırayla çalıştırarak denedim.
Önce Tahmin Et
Bookstore'un Bruno collection'ı OpenCollection YAML (girintiyle yapı kuran, okunabilir veri formatı) formatında: her istek bir .yml dosyası. Ekipten biri eski bir projeden kopyaladığı bir isteği .bru formatında aynı klasöre koyuyor:
meta {
name: Me
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/auth/me
body: none
auth: inherit
}
assert {
res.status: eq 200
}
CI'da bru run çalışıyor. Ne olur: CLI format hatası verip durur mu, iki formatı da okuyup isteği çalıştırır mı, yoksa başka bir şey mi? Run yeşil mi kırmızı mı? Cevap "Sık Düşülen Tuzaklar" bölümünde; ipucu: en kötü seçenek en sessiz olanıdır.
Bruno'nun Modeli ve OpenCollection YAML
Bruno hesap ve bulut olmadan çalışan, collection'ları doğrudan dosya sisteminde tutan bir API istemcisi. Collection bir klasör, istek bir dosya, environment bir dosya. Uygulama bu dosyaları okuyup yazar ve senkronizasyonu Git'e bırakır. Çekirdeği ve CLI'ı MIT (kullanmaya, değiştirmeye ve dağıtmaya izin veren açık kaynak lisansı) lisanslı. Uygulamanın Pro, Ultimate ve Enterprise sürümleri var. Fiyat sayfasına göre uygulama içindeki Git arayüzünün bazı işlemleri ve bazı entegrasyonlar ücretli sürümlerde; dosyaları kendi git komutlarınla commit'lemek her sürümde mümkün.
Format tarafında 2026'da önemli bir değişiklik oldu. Bruno yıllarca kendi .bru dilini kullandı. 3.0 ile YAML tabanlı OpenCollection formatı geldi, Bruno'nun 17 Şubat 2026 tarihli blog yazısına göre 3.1'den beri yeni ve import edilen collection'lar varsayılan olarak YAML. 4.0'ın değişiklik listesinde import'ların da varsayılan formatının YAML yapıldığı yazıyor; bu yazıda kullandığım CLI 4.3.0. .bru desteği sürüyor. OpenCollection'ın şartnamesi spec.opencollection.com'da açık olarak yayınlanıyor. Bookstore'un collection'ı repo'da bruno/ klasöründe:
bruno/
├── opencollection.yml
├── .env.example
├── environments/
│ └── dev.yml
├── Identity/
│ ├── folder.yml
│ └── login.yml
├── Catalog/
│ ├── folder.yml
│ └── find-the-hobbit.yml
└── Ordering/
├── folder.yml
├── create-cart.yml
├── add-two-copies.yml
├── place-order.yml
└── read-order.yml
Kök dosya collection'ın adını ve bütün isteklerin miras aldığı varsayılan auth'u taşıyor:
opencollection: 1.0.0
info:
name: Bookstore API
request:
auth:
type: bearer
token: "{{accessToken}}"
extensions:
bruno:
ignore:
- node_modules
- .git
Bir istek dosyası dört bölümden oluşuyor: info (ad, sıra), http (method, URL, body, auth), runtime (assertion ve script'ler) ve settings. "Place order":
info:
name: Place order
type: http
seq: 3
http:
method: POST
url: "{{baseUrl}}/api/orders"
body:
type: json
data: |-
{
"cartId": "{{cartId}}",
"customerName": "Ada Lovelace",
"customerEmail": "{{email}}"
}
auth: inherit
runtime:
assertions:
- expression: res.status
operator: eq
value: "201"
scripts:
- type: after-response
code: |-
bru.setVar("orderUrl", res.headers["location"]);
settings:
encodeUrl: true
auth: inherit token'ı kök dosyadan alır. Login ve katalog isteklerinde auth anahtarı hiç yok; ilk denememde oraya type: none yazmıştım, CLI her koşuda "toBrunoAuth failed: Unsupported auth type" uyarısı bastı. Bruno'nun dönüştürücü kaynağına bakınca auth'suz istek için anahtarı tamamen kaldırmak gerektiğini gördüm.
Collection'ı Kurmak: OpenAPI'den ve Postman'den Import
Boş bir collection'a başlamak yerine bru import ile Bookstore.Api'nin OpenAPI dokümanından iskelet üretilebiliyor. Varsayılan format OpenCollection, istekler OpenAPI tag'lerine göre klasörleniyor:
$ npx @usebruno/cli import openapi -s openapi.json -o imported -n "Bookstore API"
Reading OpenAPI specification from .../openapi.json...
Converting OpenAPI specification to Bruno format...
Bruno collection created at .../imported
$ find imported -type f | sort | head -8
imported/Catalog/folder.yml
imported/Catalog/get -api-books--isbn-cover.yml
imported/Catalog/get -api-books--isbn.yml
imported/Catalog/get -api-books.yml
imported/Catalog/get -api-categories.yml
imported/Catalog/put -api-books--isbn-cover.yml
imported/Identity/folder.yml
imported/Identity/get -api-auth-me.yml
Postman'deki dönüşümde gördüğümüz durum burada dosya adı olarak çıkıyor: summary'si olmayan endpoint'ler get -api-books--isbn.yml gibi adlar alıyor. Bearer security scheme'i yine işe yaradı; korumalı istekler auth: type: bearer ile geldi, body'ler boş string'li iskeletler. Bookstore'da import'u yalnız referans olarak kullanıp istekleri kısa adlarla elle yazdım.
Postman collection'ını import etmeyi Bruno uygulaması yapıyor; CLI'ın import komutu yalnız OpenAPI ve WSDL (SOAP servislerini tanımlayan XML formatı) kabul ediyor. Uygulamanın kullandığı dönüştürücü @usebruno/converters paketi olarak CLI ile birlikte geliyor. Dördüncü yazıdaki Postman collection'ını onun postmanToBruno fonksiyonundan geçirdim. Collection'ın pre-request script'i şöyle çevrildi (ilk satırlar):
if (req.getName() === "Login") {
return;
}
const token = bru.getCollectionVar("accessToken");
const expiresAt = Number(bru.getCollectionVar("accessTokenExpiresAt") || 0);
if (token && Date.now() < expiresAt - 30000) {
return;
}
await bru.sendRequest({
url: bru.interpolate("{{baseUrl}}/api/auth/login"),
method: "POST",
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ email: bru.getVar("email"), password: bru.getVar("password") })
}, async (error, response) => {
Çeviri düzgün görünüyor ve dönüştürücü hiçbir "issue" raporlamadı, ama bir anlam farkı var. Postman'de pm.variables.get bütün scope'lara bakar. Bruno'da bru.getVar yalnız runtime variable'larını okur, environment için bru.getEnvVar var. Bruno CLI'da küçük bir script'le denedim:
getVar(email) = null
getEnvVar(email) = ada@example.com
interpolate = ada@example.com
Yani çevrilen script login'e null e-posta ve şifre gönderirdi. Import edilen her collection'ın script'lerini satır satır gözden geçir; en çok değişken okuma ve pm.sendRequest callback'leri kayıyor.
Environment'lar ve Secret'lar
Environment'lar environments/ altında, birer YAML dosyası. Şifre dosyada değil; {{process.env.BOOKSTORE_PASSWORD}}, Bruno'nun collection kökündeki .env dosyasından ya da process'in environment variable'larından okunuyor:
name: dev
variables:
- name: baseUrl
value: http://localhost:5080
- name: email
value: ada@example.com
- name: password
value: "{{process.env.BOOKSTORE_PASSWORD}}"
Repo'ya yalnız .env.example giriyor. Üçüncü yazıda gördüğümüz .gitignore satırı burada da korumayı sağlıyor:
$ git check-ignore -v bruno/.env bruno/reports/bruno.html
.gitignore:7:.env bruno/.env
.gitignore:494:reports/ bruno/reports/bruno.html
Bruno 4.0 ile secret tarafında bir değişiklik var. Sürüm notlarına göre AWS (Amazon'un bulut platformu), Azure ve Vault gibi dış secret manager ayarları environment dosyalarına taşındı, CLI da bunları okuyabiliyor. Eski {{$secrets.*}} syntax'ı deprecated ve üç ay sonra kaldırılacak. Bir de script'ten bru.setEnvVar() ile yazılan değerler artık diske kaydediliyor. Sürüm notu hassas değerler için bru.setVar()'ı öneriyor; token'ı bu yüzden runtime variable'ında tutuyorum.
Assertion, Script ve bru run
Bruno'da kontrolün iki yolu var. runtime.assertions kod yazmadan ifade, operatör ve değer listesi; tests tipindeki script ise Chai'nin expect'iyle serbest JavaScript. Login'deki assertion'lar ve token'ı saklayan script:
runtime:
assertions:
- expression: res.status
operator: eq
value: "200"
- expression: res.body.tokenType
operator: eq
value: Bearer
scripts:
- type: after-response
code: |-
bru.setVar("accessToken", res.body.accessToken);
"Add two copies"de hesaplama gerektiği için test script'i kullandım:
runtime:
scripts:
- type: tests
code: |-
test("cart total is two copies", function () {
expect(res.status).to.equal(200);
expect(res.body.items).to.have.lengthOf(1);
expect(res.body.total).to.equal(bru.getVar("unitPrice") * 2);
});
Bruno CLI collection kökünde çalıştırılır; klasör ve isteklerin sırası seq alanlarından gelir. 3.0'dan beri script'ler varsayılan olarak "safe mode" sandbox'ında çalışıyor; dış npm paketi ya da dosya sistemi gerekiyorsa --sandbox=developer vermek gerekiyor. Bookstore'un script'leri ikisini de kullanmıyor:
$ cd bruno
$ mkdir -p reports
$ npx @usebruno/cli run --env dev --reporter-junit reports/bruno-junit.xml --reporter-html reports/bruno.html
Identity/login (200 OK) - 10 ms
Assertions
✓ res.status: eq 200
✓ res.body.tokenType: eq Bearer
Catalog/find-the-hobbit (200 OK) - 6 ms
Assertions
✓ res.status: eq 200
✓ res.body.total: eq 1
Ordering/create-cart (201 Created) - 7 ms
Assertions
✓ res.status: eq 201
Ordering/add-two-copies (200 OK) - 13 ms
Tests
✓ cart total is two copies
Ordering/place-order (201 Created) - 15 ms
Assertions
✓ res.status: eq 201
Ordering/read-order (200 OK) - 4 ms
Assertions
✓ res.status: eq 200
✓ res.body.status: eq Placed
📊 Execution Summary
┌───────────────┬──────────────┐
│ Metric │ Result │
├───────────────┼──────────────┤
│ Status │ ✓ PASS │
├───────────────┼──────────────┤
│ Requests │ 6 (6 Passed) │
├───────────────┼──────────────┤
│ Tests │ 1/1 │
├───────────────┼──────────────┤
│ Assertions │ 8/8 │
├───────────────┼──────────────┤
│ Duration (ms) │ 323 │
└───────────────┴──────────────┘
Wrote html results to reports/bruno.html
Wrote junit results to reports/bruno-junit.xml
$ grep -c '<testcase' reports/bruno-junit.xml
9
mkdir -p reports satırı süs değil. İlk denememde klasör yoktu; bütün istekler geçti, özet tablosu başarılı dedi, ama CLI "Output directory ../reports does not exist" yazıp 2 koduyla çıktı. CI'da bu, testleri geçen ama raporu yazamadığı için kırmızı olan bir job demek. Hatayı ilk okuyuşta test hatası sanmak kolay.
Yanlış şifreyle ve --bail ile koşu ilk hatada duruyor:
$ npx @usebruno/cli run --env dev --env-var password=wrong --bail
Identity/login (401 Unauthorized) - 14 ms
Assertions
✕ res.status: eq 200
expected 401 to equal 200
✕ res.body.tokenType: eq Bearer
expected undefined to equal 'Bearer'
Bail: Stopping run, assertion failure in "Login". Remaining 5 request(s) skipped.
...
│ Requests │ 6 (1 Failed, 5 Skipped (Bail)) │
$ echo $?
1
Zincirli bir akışta --bail mantıklı: token alınamadıysa sonraki beş isteğin 401'leri yeni bilgi taşımaz, yalnız logu kalabalıklaştırır.
GitHub Actions'ta Koşmak
CI'da akış şu: SQL Server'ı service container olarak başlat, API'yi çalıştır, ayağa kalkmasını bekle, collection'ı koş, raporları artifact olarak sakla. .github/workflows/api-tests.yml:
name: API tests
on:
push:
branches: [main]
pull_request:
jobs:
bruno:
runs-on: ubuntu-24.04
services:
sql:
image: mcr.microsoft.com/mssql/server:2025-latest
env:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: ${{ secrets.CI_SQL_PASSWORD }}
ports:
- 1433:1433
options: >-
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P \"$MSSQL_SA_PASSWORD\" -Q 'SELECT 1'"
--health-interval 10s
--health-retries 12
env:
ConnectionStrings__BookstoreDb: Server=localhost,1433;Database=BookstoreDb;User Id=sa;Password=${{ secrets.CI_SQL_PASSWORD }};TrustServerCertificate=True
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- uses: actions/setup-node@v7
with:
node-version: 24
- name: Start Bookstore.Api
run: |
dotnet run --project src/Bookstore.Api > api.log 2>&1 &
curl --silent --show-error --fail --retry 30 --retry-connrefused --retry-delay 2 \
http://localhost:5080/api/categories > /dev/null
- name: Run the Bruno collection
working-directory: bruno
env:
BOOKSTORE_PASSWORD: ${{ secrets.BOOKSTORE_PASSWORD }}
run: |
mkdir -p reports
npx --yes @usebruno/cli@4.3.0 run --env dev --env-var baseUrl=http://localhost:5080 \
--reporter-junit reports/bruno-junit.xml --reporter-html reports/bruno.html
- name: Upload reports
if: always()
uses: actions/upload-artifact@v7
with:
name: bruno-reports
path: |
bruno/reports/
api.log
Kararlar: Service container'daki health check, GitHub'ın SQL Server hazır olmadan adımlara geçmemesini sağlıyor; API açılışta migration uyguladığı için bu şart. API, launch profile'ı üzerinden Development ortamında çalışıyor; demo kullanıcılar ve rastgele imza anahtarı oradan geliyor. Bekleme adımı ikinci yazıdaki --retry-connrefused satırı. CLI sürümü @4.3.0 ile sabit; bir sabah yeni bir major sürümün davranış değiştirmesini istemezsin, nitekim 4.0'da JUnit raporunun classname alanı değişti. Adres --env-var baseUrl=... ile dışarıdan veriliyor; environment dosyası yerel geliştirmenin adresini taşımaya devam ediyor. Secret'lar repository secret'ı: SQL Server'ın sa şifresi ve demo şifresi. Demo şifresi zaten appsettings.Development.json'da açık, ama gerçek bir ortamda aynı yer secret olacağı için kalıbı baştan doğru kurdum.
Workflow'u GitHub'da çalıştıramadım, bu yüzden iki şey yaptım. Birincisi actionlint ile syntax'ını kontrol ettim; hatasız çıktı. İkincisi adımları yerelde aynı sırayla koştum: veritabanını sıfırlayıp API'yi başlattım, bekleme komutunu çalıştırdım, .env dosyasını kaldırıp şifreyi yalnız environment variable olarak verdim:
$ curl --silent --show-error --fail --retry 30 --retry-connrefused --retry-delay 2 http://localhost:5080/api/categories > /dev/null
curl: (7) Failed to connect to localhost port 5080 after 0 ms: Couldn't connect to server
curl: (7) Failed to connect to localhost port 5080 after 0 ms: Couldn't connect to server
$ cd bruno && mkdir -p reports
$ BOOKSTORE_PASSWORD=*** npx --yes @usebruno/cli@4.3.0 run --env dev --env-var baseUrl=http://localhost:5080 \
--reporter-junit reports/bruno-junit.xml --reporter-html reports/bruno.html
...
│ Status │ ✓ PASS │
│ Requests │ 6 (6 Passed) │
│ Assertions │ 8/8 │
Wrote html results to reports/bruno.html
Wrote junit results to reports/bruno-junit.xml
API açılırken curl iki kez bağlantı reddi aldı, üçüncü denemede geçti. .env olmadan da process.env çalıştı. API'yi container olarak bir compose dosyasıyla ayağa kaldırmak da bir seçenek; o kurgu Docker Compose serisinde anlatılıyor.
Code Review'da Diff ve .http ile Karşılaştırma
Dosya tabanlı olmanın asıl kazancı review'da görünüyor. "Add two copies" isteğini üç kopyaya çevirdim ve aynı değişikliği Postman collection'ında da yaptım. Bruno'nun diff'i:
@@ -10,7 +10,7 @@ http:
type: json
data: |-
{
- "quantity": 2
+ "quantity": 3
}
auth: inherit
@@ -18,10 +18,10 @@ runtime:
scripts:
- type: tests
code: |-
- test("cart total is two copies", function () {
+ test("cart total is three copies", function () {
expect(res.status).to.equal(200);
expect(res.body.items).to.have.lengthOf(1);
- expect(res.body.total).to.equal(bru.getVar("unitPrice") * 2);
+ expect(res.body.total).to.equal(bru.getVar("unitPrice") * 3);
});
Postman JSON'unun diff'i aynı satır sayısında ama okuması farklı:
@@ -277,7 +277,7 @@
},
"body": {
"mode": "raw",
- "raw": "{\n \"quantity\": 2\n}",
+ "raw": "{\n \"quantity\": 3\n}",
"options": {
"raw": {
"language": "json"
@@ -294,8 +294,8 @@
"const cart = pm.response.json();",
"pm.test(\"cart has one line with two copies\", () => {",
" pm.expect(cart.items).to.have.lengthOf(1);",
- " pm.expect(cart.items[0].quantity).to.eql(2);",
- " pm.expect(cart.total).to.eql(cart.items[0].unitPrice * 2);",
+ " pm.expect(cart.items[0].quantity).to.eql(3);",
+ " pm.expect(cart.total).to.eql(cart.items[0].unitPrice * 3);",
"});"
Body bir string'in içinde kaçışlı satır sonlarıyla, script tırnaklı satırlardan oluşan bir array. İki satırlık bir değişiklikte sorun değil; on satırlık bir script'i ya da merge conflict'i bu halde çözmek zahmetli. Postman v12'nin Native Git'teki YAML formatı bu farkı kapatmayı amaçlıyor.
.http dosyalarıyla karşılaştırınca: ikisi de repo'da yaşıyor ve diff'i okunuyor. .http dosyası API projesinin içinde, IDE'nin kendisinde çalışıyor ve ek uygulama istemiyor; backend geliştiricinin günlük elle denemesi için yeterli. Bruno ise IDE'den bağımsız, frontend geliştiricisi ve QA (kalite güvence, yani test ekibi) için de aynı arayüzü veriyor, assertion ve script'i, CLI'ı ve raporları tek bir dialect'te topluyor. Ekipte yalnız backend geliştiriciler varsa ve CI'da koşan bir akış gerekmiyorsa .http yeter. Ekip karışıksa ya da collection CI'da test olarak koşacaksa Bruno daha az sorun çıkarıyor. Bookstore'da ikisi birlikte duruyor: .http dosyaları geliştirme sırasında, Bruno collection'ı paylaşım ve CI için.
Üretimde Dikkat
CI'daki collection gerçek bir veritabanına sipariş yazıyor. Bookstore'da bu, her job'da sıfırdan kurulan bir service container; paylaşılan bir staging veritabanına karşı koşacaksan her run test siparişleri bırakır. Ya ayrı bir test müşterisi kullan ve onu temizleyen bir adım ekle, ya da collection'ın yazma isteklerini staging'de çalıştırma (bru run'a yalnız Catalog klasörünü vermek gibi).
HTML raporu istek ve cevap gövdelerini içerebilir; artifact olarak saklandığında repo'ya erişimi olan herkes indirebilir. Token ve şifre içeren header'ları raporda istemiyorsan CLI'ın --reporter-skip-all-headers ve --reporter-skip-body seçenekleri var. JUnit raporu da makinenin hostname'ini yazıyor; CI'da bu runner'ın adı, yerelde senin bilgisayarının adı.
Bruno'nun sürüm geçişlerine dikkat et. 4.0 sürüm notları, 4.x ile kaydedilen collection'ların eski sürümleri kullanan ekip arkadaşlarında açılmayabileceğini söylüyor. Uygulama ve CLI sürümünü ekipçe aynı anda yükseltin ve CI'daki sabit sürümü de aynı commit'te güncelleyin.
Sık Düşülen Tuzaklar
YAML ve .bru'yu aynı collection'da karıştırmak. Tahmin sorusundaki dosyayı Bookstore collection'ının yalnız Identity klasörünü bıraktığım bir kopyasına koyup CLI 4.3.0 ile koştum:
$ npx @usebruno/cli run --env dev
Identity/login (200 OK) - 11 ms
Assertions
✓ res.status: eq 200
✓ res.body.tokenType: eq Bearer
...
│ Status │ ✓ PASS │
│ Requests │ 1 (1 Passed) │
CLI .bru dosyasını hata vermeden atladı ve run yeşil oldu. Kimse bir şeyin eksik çalıştığını fark etmez. Collection'ın formatı neyse bütün dosyalar o formatta olmalı; Bruno'nun blog yazısı da bir collection'ın tek format kullanmasını söylüyor. Geçiş sırasında bir CI adımıyla .bru dosyası kalmadığını kontrol edebilirsin.
CI'da adresi hard-code etmek. Environment dosyasındaki http://localhost:5080'i staging adresiyle değiştirip commit'lemek, yerelde çalışan herkesin isteklerini staging'e yollar. Adresi CI'da --env-var ile ya da ayrı bir environment dosyasıyla ver.
Rapor klasörünü oluşturmamak. Testler geçse de CLI 2 koduyla çıkar; job'un kırmızı olduğunu görünce önce testlere bakarsın.
Import edilen script'lere güvenmek. Postman'den gelen pm.variables.get, bru.getVar olarak çevrilir ve environment variable'ını görmez; login sessizce null gönderir.
Token'ı bru.setEnvVar ile saklamak. 4.0'dan beri bu değer environment dosyasına, yani diske yazılıyor; bir sonraki commit'te repo'ya girebilir. bru.setVar kullan.
Küçük Bir Deneme
★☆☆ bruno/ klasöründe npx @usebruno/cli run Catalog --env dev ile yalnız katalog isteklerini koş. Ardından find-the-hobbit.yml'deki total assertion'ını 2 yap ve çıkan hata mesajını oku.
★★☆ Collection'a Identity/me.yml ekle (YAML olarak): token'ın sahibinin e-postasının environment'taki email ile aynı olduğunu bir test script'iyle doğrula. bru.getEnvVar ve bru.getVar farkını kendin gör.
★★★ Workflow'a bir adım ekle: bruno/ altında .bru uzantılı dosya varsa job'u durdursun. Sonra workflow'u kendi GitHub fork'unda çalıştır, JUnit raporunu artifact'tan indir ve 4.0'da değişen classname alanının neye benzediğine bak.
Bu yazıyla serinin sonuna geldik: aynı Bookstore akışını curl, iki dialect'te .http dosyası, Postman ve Bruno ile koştuk ve her birinin nerede durduğunu, secret'ı nerede sakladığını ve CI'da nasıl davrandığını gördük. Bu collection'lar akışın ayakta olduğunu söyler. API'nin davranışını codebase'te test etmek ilerideki Integration ve E2E (uçtan uca) Test serisinin, aynı akışı yük altında ölçmek de Performance ve Load Testing serisinin konusu.