09 Ağu 2025·5 dk okuma

Go REST handler'larını test etme: httptest ve tablo-tabanlı kontroller

httptest ve tablo-tabanlı vakalarla Go REST handler'larını test etmek, yayın öncesi kimlik doğrulama, doğrulama, durum kodları ve köşe vakalarını tekrar edilebilir şekilde kontrol etmenizi sağlar.

Go REST handler'larını test etme: httptest ve tablo-tabanlı kontroller

Sürüm öncesi hangi konularda emin olmalısınız?

Bir REST handler derlenebilir, hızlıca el ile kontrol edilebilir ve yine de üretimde başarısız olabilir. Çoğu hata sözdizimi problemi değildir. Bunlar sözleşme problemleri: handler reddetmesi gerekeni kabul eder, yanlış durum kodu döndürür veya hata mesajında detay sızdırır.

Manuel test yardımcı olur, ama köşe vakalarını ve regresyonları kaçırmak kolaydır. Mutlu yolu (happy path) denersiniz, belki bir açık hatayı kontrol edersiniz ve devam edersiniz. Sonra doğrulama veya middleware'de küçük bir değişiklik, kararlı olduğunu düşündüğünüz davranışı sessizce bozabilir.

Handler testlerinin amacı basittir: handler'ın verdiği sözleri tekrar edilebilir kılmak. Bu, kimlik doğrulama kurallarını, giriş doğrulamayı, öngörülebilir durum kodlarını ve istemcilerin güvenle dayanabileceği hata gövdelerini içerir.

Go'nun httptest paketi mükemmeldir çünkü gerçek bir sunucu başlatmadan handler'ı doğrudan çalıştırabilirsiniz. Bir HTTP isteği oluşturur, handler'a verirsiniz ve yanıt gövdesini, başlıkları ve durum kodunu incelersiniz. Testler hızlı, izole ve her commit'te çalıştırılmaya uygundur.

Sürüm öncesinde bilmeniz gerekenler (ummayın, bilin):

  • Kimlik doğrulama davranışı eksik token, geçersiz token ve yanlış roller için tutarlı olmalı.
  • Girdiler doğrulanmalı: gerekli alanlar, tipler, aralıklar ve (zorunluysa) bilinmeyen alanlar.
  • Durum kodları sözleşmeye uymalı (örneğin 401 vs 403, 400 vs 422).
  • Hata yanıtları güvenli ve tutarlı olmalı (stack trace yok, her seferinde aynı şekil).
  • Mutlu olmayan yollar ele alınmış olmalı: zaman aşımı, bağımlılık hataları ve boş sonuçlar.

"Bilet oluştur" endpoint'i, mükemmel JSON ile admin olarak gönderildiğinde çalışıyor olabilir. Testler, denemeyi unuttuğunuz durumları yakalar: süresi geçmiş token, istemcinin yanlışlıkla gönderdiği ekstra alan, negatif öncelik ya da bir bağımlılık başarısız olduğunda "bulunamadı" ile "iç hata" arasındaki fark.

Her endpoint için sözleşmeyi tanımlayın

Test yazmadan önce handler'ın ne yapmayı vaat ettiğini yazın. Net bir sözleşme, testleri odaklı tutar ve onları kodun "ne demek istediğine" dair tahminlere dönüşmekten alıkoyar. Ayrıca yeniden düzenlemeleri (refactor) güvenli kılar; iç mantığı değiştirseniz bile davranışı değiştirmemiş olursunuz.

Girdilerle başlayın. Her değerin nereden geldiği ve neyin zorunlu olduğu konusunda spesifik olun. Bir endpoint path'ten id, sorgudan limit, Authorization başlığı ve bir JSON gövdesi alabilir. Önemli kuralları not edin: izin verilen formatlar, min/max değerler, gerekli alanlar ve bir şey eksik olduğunda ne olacağı.

Sonra çıktıları tanımlayın. Sadece "JSON döndürüyor" demekle yetinmeyin. Başarının nasıl göründüğüne, hangi başlıkların önemli olduğuna ve hataların nasıl olacağına karar verin. Eğer istemciler kararlı hata kodlarına ve tahmin edilebilir JSON şekline güveniyorsa, bunu sözleşmenin parçası olarak ele alın.

Pratik bir kontrol listesi:

  • Girdiler: path/query değerleri, gerekli başlıklar, JSON alanları ve doğrulama kuralları
  • Çıktılar: durum kodu, yanıt başlıkları, başarı ve hata için JSON şekli
  • Yan etkiler: hangi verinin değiştiği ve neyin oluşturulduğu
  • Bağımlılıklar: veritabanı çağrıları, dış servisler, mevcut zaman, üretilen ID'ler

Ayrıca handler testlerinin nerede duracağını kararlaştırın. Handler testleri HTTP sınırında en güçlüdür: kimlik doğrulama, parsing, doğrulama, durum kodları ve hata gövdeleri. Daha derin endişeleri entegrasyon testlerine bırakın: gerçek veritabanı sorguları, ağ çağrıları ve tam yönlendirme (routing).

Eğer backend'iniz üretiliyorsa (örneğin AppMaster Go handler'ları ve iş mantığını üretiyorsa), sözleşme-öncelikli yaklaşım daha da faydalıdır. Kod yeniden üretildiğinde bile her endpoint'in aynı kamu davranışını koruduğunu doğrulayabilirsiniz.

Minimal bir httptest düzeni kurun

İyi bir handler testi, gerçek bir isteği göndermiş gibi hissettirmeli ama sunucu başlatmamalıdır. Go'da bu genelde şu adımları içerir: httptest.NewRequest ile bir istek oluşturun, yanıtı httptest.NewRecorder ile yakalayın ve handler'ı çağırın.

Handler'ı doğrudan çağırmak hızlı, odaklı testler sağlar. Bu, handler içindeki davranışı doğrulamak istediğinizde idealdir: auth kontrolleri, doğrulama kuralları, durum kodları ve hata gövdeleri. Sözleşme path parametrelerine, route eşlemesine veya middleware sırasına bağlıysa testlerde bir router kullanmak faydalıdır. Önce doğrudan çağrılarla başlayın, gerekirse router ekleyin.

Başlıklar çoğunun düşündüğünden daha önemlidir. Eksik bir Content-Type handler'ın gövdeyi okuma şeklini değiştirebilir. Her durumda beklediğiniz başlıkları ayarlayın ki hatalar test kurulumundan ziyade mantığa işaret etsin.

Tekrar kullanılabilir minimal bir desen:

req := httptest.NewRequest(http.MethodPost, "/v1/widgets", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)
res := rec.Result()
defer res.Body.Close()

Doğrulamaları tutarlı tutmak için küçük bir yardımcı fonksiyonla yanıt gövdesini okumak ve decode etmek yardımcı olur. Çoğu testte önce durum kodunu kontrol edin (böylece başarısızlıklar kolayca taranır), sonra vaat ettiğiniz ana başlıkları (sıkça Content-Type) ve ardından gövdeyi.

Eğer backend'iniz üretilmişse (AppMaster tarafından üretilmiş bir Go backend gibi), bu düzen hâlâ geçerlidir. Burada test ettiğiniz, kullanıcıların dayandığı HTTP sözleşmesidir, arkasındaki kod stilini değil.

Okunabilir kalan tablo-tabanlı test vakaları tasarlayın

Tablo-tabanlı testler en iyi şekilde her vaka küçük bir hikaye gibi okunduğunda çalışır: gönderdiğiniz istek ve beklediğiniz yanıt. Tabloyu taradığınızda dosyada gezinmeden kapsamı anlamalısınız.

Sağlam bir vaka genelde şuna sahiptir: net bir ad, istek (method, path, başlıklar, gövde), beklenen durum kodu ve yanıt için bir kontrol. JSON gövdeleri için, tüm JSON dizgesini eşlemek yerine birkaç kararlı alanı (örneğin bir hata kodu) doğrulamayı tercih edin; sözleşmeniz sıkı çıktı gerektirmedikçe.

Yeniden kullanabileceğiniz basit bir vaka yapısı

Vaka struct'ını odaklı tutun. Tek seferlik kurulumu helper'lara taşıyın ki tablo küçük kalsın.

type tc struct {
	name       string
	method     string
	path       string
	headers    map[string]string
	body       string
	wantStatus int
	wantBody   string // alt dize veya kompakt JSON
}

Farklı girdiler için, farkı bir bakışta gösteren küçük gövde dizeleri kullanın: geçerli yük, bir alan eksik, yanlış tip ve boş dize. Tabloya çok fazla biçimlendirilmiş JSON koymaktan kaçının — çabukça gürültülü olur.

Tekrarlayan kurulumları (token oluşturma, ortak başlıklar, varsayılan gövde) newRequest(tc) veya baseHeaders() gibi yardımcı fonksiyonlara taşıyın.

Eğer bir tablo çok fazla fikri karıştırmaya başlarsa, bölün. Başarı yolları için bir tablo, hata yolları için başka bir tablo genellikle daha okunaklı ve hata ayıklaması kolaydır.

Kimlik doğrulama kontrolleri: genelde atlanan vakalar

Yaygın entegrasyonları bağlayın
Stripe ödemeleri, mesajlaşma veya OpenAI entegrasyonları ekleyin ve API davranışını tutarlı tutun.
Entegrasyonları Keşfet

Auth testleri genelde mutlu yol için iyi görünür, sonra üretimde bir "küçük" vaka denenmediği için başarısız olur. Auth'ı bir sözleşme olarak ele alın: istemci ne gönderir, sunucu ne döndürür ve ne asla ifşa edilmemelidir.

Token varlığı ve geçerliliği ile başlayın. Korunan bir endpoint, başlık yokken ve yanlışken farklı davranmalı. Kısa ömürlü token'lar kullanıyorsanız, süresinin dolmasını da test edin; bunu bir doğrulayıcı enjekte ederek "süresi doldu" döndürecek şekilde simüle edebilirsiniz.

Çoğu boşluğu şu vakalar kapar:

  • Authorization başlığı yok -> 401 ve kararlı bir hata yanıtı
  • Bozuk başlık (yanlış önek) -> 401
  • Geçersiz token (kötü imza) -> 401
  • Süresi dolmuş token -> 401 (veya seçtiğiniz kod) ile öngörülebilir mesaj
  • Geçerli token ama yanlış rol/izin -> 403

401 vs 403 ayrımı önemlidir. Çağrı sahibi kimliği doğrulanmamışsa 401 kullanın. Kimliği doğrulanmış ama yetkili değilse 403 kullanın. Bunları karıştırırsanız, istemciler gereksiz yere yeniden deneyecek veya yanlış UI gösterecektir.

Rol kontrolleri "kullanıcıya ait" endpoint'lerde (örneğin GET /orders/{id}) yeterli değildir. Sahipliği test edin: kullanıcı A, kullanıcı B'nin siparişini geçerli token ile görmemeli. Bu temiz bir 403 (veya varlığını gizliyorsanız 404) olmalı ve gövde hiçbir şey sızdırmamalıdır. Hata genel tutun. "Sipariş kullanıcı 42'ye ait" gibi ipuçları vermeyin.

Giriş kuralları: reddet, doğrula ve açıkla

Pek çok sürüm öncesi hata giriş hatasıdır: eksik alanlar, yanlış tipler, beklenmedik formatlar veya çok büyük yükler.

Handler'ın kabul ettiği her girdiyi adlandırın: JSON gövde alanları, sorgu parametreleri ve path parametreleri. Her biri için eksik, boş, bozuk veya aralık dışı olduğunda ne olacağını kararlaştırın. Sonra handler'ın kötü girdileri erken reddettiğini ve her zaman aynı tür hatayı döndürdüğünü kanıtlayan vakalar yazın.

Küçük bir doğrulama vaka seti çoğu riski kapsar:

  • Gerekli alanlar: eksik vs boş dize vs null (eğer null'a izin veriyorsanız)
  • Tipler ve formatlar: sayı vs dize, e-posta/tarih/UUID formatları, boolean ayrıştırma
  • Boyut limitleri: maksimum uzunluk, maksimum öğe sayısı, çok büyük payload
  • Bilinmeyen alanlar: yok sayma vs reddetme (katı decode uygularsanız)
  • Sorgu ve path parametreleri: eksik, ayrıştırılamaz ve varsayılan davranış

Örnek: POST /users handler'ı { "email": "...", "age": 0 } kabul ediyor. email eksik, email 123, email "not-an-email", age -1 ve age "20" gibi vakaları test edin. Katı JSON istiyorsanız { "email":"[email protected]", "extra":"x" } gibi bir vakayı da test edip başarısız olduğunu doğrulayın.

Doğrulama hatalarını tahmin edilebilir yapın. Doğrulama hataları için bir durum kodu seçin (bazı ekipler 400, bazıları 422 kullanır) ve hata gövde şeklini tutarlı tutun. Testler hem durumu hem de hangi girdinin başarısız olduğunu gösteren bir mesaj (veya details alanı) doğrulamalıdır.

Durum kodları ve hata gövdeleri: tahmin edilebilir yapın

Teknik borç olmadan yeniden üretin
Gereksinimleri güncelleyin ve eski handler'ları yamalamak yerine temiz kaynak kodu yeniden üretin.
Kod Üret

Handler testleri, API hataları sıkıcı ve tutarlı olduğunda kolaylaşır. Her hatanın net bir durum koduna eşlenmesini ve aynı JSON şeklini döndürmesini istersiniz; kim yazarsa yazsın.

Hata türlerinden HTTP durum kodlarına küçük, üzerinde anlaşılmış bir eşleme ile başlayın:

  • 400 Bad Request: bozuk JSON, eksik gerekli sorgu parametreleri
  • 404 Not Found: kaynak ID'si yok
  • 409 Conflict: benzersiz kısıtlama veya durum çakışması
  • 422 Unprocessable Entity: geçerli JSON ama iş kurallarını sağlamıyor
  • 500 Internal Server Error: beklenmeyen hatalar (db down, nil pointer, üçüncü taraf hatası)

Sonra hata gövdesini sabit tutun. Mesaj metni daha sonra değişse bile istemcilerin güvenebileceği öngörülebilir alanlar olsun:

{ "code": "user_not_found", "message": "User was not found", "details": { "id": "123" } }

Testlerde sadece durum satırını değil şekli de doğrulayın. Sık yapılan hata, hata durumlarında HTML, düz metin veya boş gövde döndürmektir; bu istemcileri kırar ve hataları gizler.

Ayrıca hata yanıtları için başlıkları ve kodlamayı da test edin:

  • Content-Type application/json (ve charset kullanıyorsanız tutarlı olsun)
  • Hata durumunda bile gövde geçerli JSON olmalı
  • code, message ve details mevcut olmalı (details boş olabilir ama rastgele olmamalı)
  • Panic'ler ve beklenmeyen hatalar, stack trace sızdırmadan güvenli bir 500 döndürmeli

Bir recover middleware eklediyseniz, bir panic zorlayıp hala temiz JSON hata yanıtı aldığınızı doğrulayan bir test ekleyin.

Köşe vakaları: hata, zaman ve mutlu olmayan yollar

Gerçek kaynak kodu dışa aktarın
Tam kontrol istediğinizde Go, Vue3 ve Kotlin veya SwiftUI kodu edinin.
Kaynak İhraç Et

Mutlu yol testleri handler'ın bir kere çalıştığını kanıtlar. Köşe vakası testleri, dünya karışık olduğunda davranışını koruduğunu gösterir.

Bağımlılıkların belirli, tekrar edilebilir hatalarını zorlayın. Handler veritabanı, cache veya dış API çağrısı yapıyorsa, bu katmanlar beklemediğiniz hatalar döndüğünde ne olduğunu görmek istersiniz.

Her endpoint için en az bir kez simüle etmeye değer:

  • Alt hizmetten zaman aşımı (context deadline exceeded)
  • Depolamadan bulunamadı dönmesi (istemci veri bekliyordu)
  • Oluşturmada benzersiz kısıtlama ihlali (çakışan e-posta, duplicate slug)
  • Ağ veya taşıma hatası (connection refused, broken pipe)
  • Beklenmeyen iç hata (genel "bir şey yanlış gitti")

Her şeyi kontrol ederek testleri kararlı tutun. Flaky bir test yok testten daha kötüdür çünkü insanlar hataları görmezden gelmeye başlar.

Zaman ve rasgeleliği öngörülebilir yapın

Handler time.Now(), ID'ler veya rastgele değerler kullanıyorsa bunları enjekte edin. Handler veya servise bir clock fonksiyonu ve ID üreteci geçirin. Testlerde sabit değerler döndürün ki JSON alanlarını ve başlıkları tam olarak doğrulayabilesiniz.

Küçük sahte nesneler (fakes) kullanın ve "yan etki yok" doğrulayın

Tam moklar yerine küçük fakes veya stub'lar tercih edin. Bir fake çağrıları kaydedip bir başarısızlıktan sonra hiçbir şey olmadığını doğrulamanıza izin verir.

Örneğin, bir "kullanıcı oluştur" handler'ında veritabanı eklemesi benzersiz kısıtlama hatası verirse, durum kodunun doğru olduğunu, hata gövdesinin sabit olduğunu ve hiçbir hoş geldin e-postası gönderilmediğini doğrulayın. Fake mailer bir sayacı (sent=0) açığa çıkarabilir, böylece hata yolu hiçbir yan etki tetiklemediğini kanıtlar.

Handler testlerini güvenilmez yapan yaygın hatalar

Handler testleri genellikle yanlış nedenle başarısız olur. Testte oluşturduğunuz istek gerçek bir istemci isteğiyle aynı biçimde olmaz. Bu, gürültülü hatalara ve yanlış güvene yol açar.

Yaygın bir sorun, handler'ın beklediği başlıklar olmadan JSON göndermektir. Kodunuz Content-Type: application/json kontrol ediyorsa, bunu unutmak handler'ın JSON decode etmesini atlamasına, farklı bir durum döndürmesine veya üretimde hiç olmayan bir dala girmesine neden olabilir. Aynı şekilde auth için: eksik Authorization başlığı, geçersiz token ile aynı şey değildir. Bunlar farklı vakalar olmalı.

Bir başka tuzak da tüm JSON yanıtını ham dizge olarak doğrulamaktır. Alan sırası, boşluklar veya yeni alanlar gibi küçük değişiklikler testleri kırar; API hâlâ doğru olsa bile. Gövdeyi bir struct veya map[string]any'e decode edin, sonra önemli olanı doğrulayın: durum, hata kodu, mesaj ve birkaç ana alan.

Testler ayrıca vaka başına paylaşılan mutable durum olduğunda güvenilmez olur. Aynı bellek içi store'u, global değişkenleri veya singleton router'ı tablo satırları arasında yeniden kullanmak vakalar arasında veri sızdırır. Her test vakası temiz başlamalı veya t.Cleanup ile durumu sıfırlamalıdır.

Genellikle kırılgan teste yol açan kalıplar:

  • Gerçek istemcilerin kullandığı aynı başlıklar ve kodlama olmadan istekler oluşturmak
  • Tam JSON dizelerini doğrulamak yerine decode edip alanları kontrol etmemek
  • Vaka başına paylaşılan veritabanı/cache/global handler durumunu yeniden kullanmak
  • Auth, doğrulama ve iş mantığı doğrulamalarını tek bir aşırı büyük teste sıkıştırmak

Her testi odaklanmış tutun. Bir vaka başarısız olduğunda, bunun auth mi, giriş kuralları mı yoksa hata formatlaması mı olduğunu saniyeler içinde bilmelisiniz.

Sürüm öncesi yeniden kullanılabilir kısa kontrol listesi

İç araçları hızlıca oluşturun
Yayınlamadan önce güvenle test edebileceğiniz API'lerle desteklenen yönetici panelleri ve portallar oluşturun.
Hemen Başla

Göndermeden önce, testler iki şeyi kanıtlamalı: endpoint sözleşmesine uyuyor ve güvenli, öngörülebilir şekilde başarısız oluyor.

Bunları tablo-tabanlı vakalar olarak çalıştırın ve her vaka hem yanıtı hem de varsa yan etkileri doğrulasın:

  • Auth: token yok, kötü token, yanlış rol, doğru rol (ve "yanlış rol" vakasının detay sızdırmadığını doğrulayın)
  • Girdiler: gerekli alanlar eksik, yanlış tipler, sınır boyutları (min/max), reddetmek istediğiniz bilinmeyen alanlar
  • Çıktılar: durum kodu, ana başlıklar (ör. Content-Type), gerekli JSON alanları, tutarlı hata şekli
  • Bağımlılıklar: bir alt sistem hatasını (DB, kuyruk, ödeme, e-posta) zorlayın, güvenli bir mesaj doğrulayın, kısmi yazma olmadığını teyit edin
  • İdempotentlik: aynı isteği tekrar edin (veya zaman aşımından sonra yeniden deneyin) ve çoğaltma oluşturulmadığını doğrulayın

Bunlardan sonra, atlanan bir sanite doğrulaması ekleyin: handler'ın dokunmaması gereken şeyi dokunmadığını doğrulayın. Örneğin, doğrulama hatası vakasında hiçbir kayıt oluşturulmadığını ve e-posta gönderilmediğini doğrulayın.

Eğer API'leri AppMaster gibi bir araçla oluşturuyorsanız, aynı kontrol listesi geçerlidir. Önemli olan aynı: istemcilerin güvendiği davranışı kilitlemek.

Örnek: bir endpoint, küçük bir tablo ve neleri yakalar

Basit bir endpoint olduğunu varsayalım: POST /login. email ve password içeren JSON kabul ediyor. Başarılıysa 200 ve token döndürüyor, geçersiz giriş için 400, yanlış kimlik bilgileri için 401 ve auth servisi kapalıysa 500 döner.

Bu kapsamlı tablo üretimde bozulan çoğu şeyi kapsar.

func TestLoginHandler(t *testing.T) {
	// Gerçek sistemlere dokunmadan 200/401/500'ü zorlayabilmemiz için fake bağımlılık.
	auth := &FakeAuth{ /* configure per test */ }
	h := NewLoginHandler(auth)

	tests := []struct {
		name       string
		body       string
		authHeader string
		setup      func()
		wantStatus int
		wantBody   string
	}{
		{"success", `{"email":"[email protected]","password":"secret"}`, "", func() { auth.Mode = "ok" }, 200, `"token"`},
		{"missing password", `{"email":"[email protected]"}`, "", func() { auth.Mode = "ok" }, 400, "password"},
		{"bad email format", `{"email":"not-an-email","password":"secret"}`, "", func() { auth.Mode = "ok" }, 400, "email"},
		{"invalid JSON", `{`, "", func() { auth.Mode = "ok" }, 400, "invalid JSON"},
		{"unauthorized", `{"email":"[email protected]","password":"wrong"}`, "", func() { auth.Mode = "unauthorized" }, 401, "unauthorized"},
		{"server error", `{"email":"[email protected]","password":"secret"}`, "", func() { auth.Mode = "error" }, 500, "internal"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setup()
			req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(tt.body))
			req.Header.Set("Content-Type", "application/json")
			if tt.authHeader != "" {
				req.Header.Set("Authorization", tt.authHeader)
			}

			rr := httptest.NewRecorder()
			h.ServeHTTP(rr, req)

			if rr.Code != tt.wantStatus {
				t.Fatalf("status = %d, want %d, body=%s", rr.Code, tt.wantStatus, rr.Body.String())
			}
			if tt.wantBody != "" && !strings.Contains(rr.Body.String(), tt.wantBody) {
				t.Fatalf("body %q does not contain %q", rr.Body.String(), tt.wantBody)
			}
		})
	}
}

"missing password" vakasını baştan sona izleyin: sadece email içeren bir gövde gönderirsiniz, Content-Type'ı ayarlarsınız, ServeHTTP ile çalıştırırsınız, sonra 400 ve password'e açıkça işaret eden bir hata doğrularsınız. Bu tek vaka decoder'ınızın, doğrulayıcınızın ve hata yanıtı formatınızın birlikte çalıştığını kanıtlar.

API sözleşmelerini, auth modüllerini ve entegrasyonları standartlaştırmanın daha hızlı bir yolunu istiyorsanız AppMaster (appmaster.io) bunun için tasarlanmıştır. Yine de bu testler değerlidir çünkü istemcilerinizin dayandığı davranışı kilitlerler.

Başlaması kolay
Harika bir şey yaratın

Ücretsiz planla AppMaster ile denemeler yapın.
Hazır olduğunuzda uygun aboneliği seçebilirsiniz.

Başlayın