İçeriğe geç
academia.sh

Ders 03 / 16

HTTP Testleri

Aynı işleyici, gerçek bir bağlantı noktası hiç açılmadan üç istekle sınanıyor: var olan bir kalem 200, olmayan bir kalem 404, parametresiz bir istek 400 döndürüyor — üçü de yalnız isleyici.ServeHTTP çağrılarak öğrenilen bir koşum bilgisi. Sınırlayıcı ölçüm, isteğin adresinin RFC 5737'nin belge amaçlı ayırdığı sabit bir adres olduğunu okuyarak testin ağı değil işleyiciyi sınadığını gösteriyor.

Yayın Güncelleme

Bu derste

  • Bir HTTP işleyicisini gerçek bir bağlantı noktası açmadan, sunucu ve istemci tarafı test yardımcılarıyla doğrudan çağırarak sınar
  • Aynı işleyicinin üç isteğe verdiği kararı (200, 404, 400) koşum bilgisi olarak sınıflar ve kaynaktaki durum kodu sayısıyla karşılaştırır
  • Sınanan isteğin gerçek bir bağlantıdan gelmediğini, isteğin adresini okuyarak sınırlar
İçindekiler

Önceki ders bir tablonun vaka sayısıyla koşan alt test sayısının nasıl eşleştiğini ve bu eşleşmenin kaynaktan mı koşumdan mı geldiğini ölçtü. Bu ders aynı soruyu ağ katmanına taşıyor: bir sunucunun bir isteğe verdiği yanıt da yalnız koşturularak öğrenilebilen bir bilgi — ama “koşturmak” burada gerçek bir bağlantı noktası açmak, bir soket dinlemek, bir istemcinin o soketi çağırması anlamına gelmiyor. Test paketinin net/http/httptest alt paketi bu ikisini ayırıyor: bir işleyicinin davranışını sınamak, o işleyiciyi ağın üzerinden değil, doğrudan bir işlev çağrısıyla sınamak anlamına gelebiliyor.

Üretim ortamında çalışan bir HTTP sunucusunun davranışı, doğrudan sunucuya bağlanıp isteği elle göndermeden sınanabiliyorsa, bu sınama sunucunun çalıştığı makinenin ağ ayarlarına, boş bir bağlantı noktası bulunmasına ya da bir zaman aşımına bağlı kalmıyor — yani sonucu ortamdan bağımsız oluyor. Bu ders bu iddiayı iki adımda ölçüyor: önce bir işleyicinin gerçekten bir bağlantı olmadan nasıl çağrıldığı, sonra bu çağrının ürettiği isteğin gerçek bir ağdan gelmediğinin kendisinin nasıl doğrulanabildiği.

İşleyici Doğrudan Çağrılıyor, Bağlantı Noktası Hiç Açılmıyor

envanter paketi bu derste HTTP katmanına taşınıyor: kalemIsleyici, bir depo (ad’dan Kalem’e bir sözlük) alıp bir http.HandlerFunc döndürüyor. İşleyici üç adımda karar veriyor: önce isteğin sorgu dizgisinden ad parametresini okuyor, sonra bu adın depoda olup olmadığına bakıyor, son olarak varsa kalemin fiyatını düz metin olarak yazıyor. İstek bir ad sorgu parametresi taşımıyorsa 400, taşıdığı ad depoda yoksa 404, varsa kalemin fiyatını gövdede döndürüp 200 veriyor. Bu işleyicinin sıradan bir Go HTTP sunucusuna bağlanabilmesi için tek gereken, onu bir http.ServeMuxe kaydetmek — ama bu ders onu hiç sunucuya bağlamıyor.

İşleyicinin imzası da bu ayrımı kolaylaştıran bir seçim: kalemIsleyici depoyu bir parametre olarak alıyor ve bir http.HandlerFunc döndürüyor, deponun kendisi paket düzeyinde bir değişken olarak sabitlenmiyor. Bu, aynı işleyicinin farklı bir depoyla — örneğin bu derste kullanılan tek kalemlik sabit depoyla, ya da başka bir ölçümde boş bir depoyla — yeniden kurulabilmesini sağlıyor; işleyicinin kendisi hangi depoyla çalıştığını hiç bilmiyor, yalnız kendisine verilen map[string]Kalem değerine bakıyor.

Bu yapı bir kapanış (closure) — kalemIsleyici’nin döndürdüğü işlev, kendisini oluşturan çağrıda verilen depo değişkenini gövdesinde tutmaya devam ediyor, tıpkı bu konudan önceki dersin orneklerVer yardımcısının kendi çağıranına bağlı kalması gibi. Test yardımcıları bu kapanışın içine hiç girmiyor; yalnız dışarıdan görünen http.HandlerFunc imzasını çağırıyor ve kapanışın içinde hangi deponun tutulduğu ölçümün kendisi için görünmez kalıyor.

İsteği kuran çağrı http.MethodGet sabitini kullanıyor, "GET" dizgisini elle yazmak yerine — standart kitaplığın kendi tanımladığı bu sabitler, bir yöntem adının yanlış yazılmasını derleme zamanında değil ama en azından tek bir kaynaktan yakalıyor ve bu ders boyunca kullanılan bütün istekler aynı sabiti paylaşıyor.

httptest paketinin iki yordamı bu bağlantıyı gereksiz kılıyor: httptest.NewRequest, bir istemcinin ağdan göndereceği bir isteğin Go karşılığını üretiyor — istemci tarafı bir yardımcı, çünkü normalde bu isteği bir istemci bir bağlantı üzerinden yollar. httptest.NewRecorder ise bir yanıtı toplayan, bir http.ResponseWriter gerçekleştiren bir yapı — sunucu tarafı bir yardımcı, çünkü normalde bir yanıtı bir bağlantının karşı ucuna yazan şey budur. İkisi birlikte kullanıldığında, isleyici.ServeHTTP(kayit, istek) çağrısı işleyiciyi gerçek bir ağ yığıtına hiç dokunmadan, aynı süreç içinde, bir işlev çağrısı gibi çalıştırıyor.

httptest paketinin bir de NewServer yordamı var ve o gerçekten bir bağlantı noktası açıyor — verilen işleyiciyi yerel makinede geçici, boşta bulunan bir bağlantı noktasına bağlıyor, bir adres döndürüyor ve o adrese gerçek bir istemci isteği gönderilmesini bekliyor. Bu yordam gerçek bir ağ yolunu (bağlantı kurma, veri gönderme, bağlantıyı kapatma) sınamak istendiğinde gerekli, ama bu derste kullanılmıyor: kalemIsleyici’nin mantığını sınamak için ağ yoluna hiç ihtiyaç yok ve gerçek bir bağlantı noktası açmak hem ölçümü yavaşlatır hem de hangi bağlantı noktasının boş olduğuna bağlı, koşumdan koşuma değişebilecek bir bilgi ekler — tam olarak bu konunun yazılmayan yasağı. Recorder ve NewRequest ikilisi bu maliyeti hiç doğurmadan aynı mantığı sınıyor.

Üç İsteğin Kararı

TS7. İşleyicinin kaynağında kaç ayrı durum kodu yazılı olduğu kaynaktan sayılabiliyor: http.StatusBadRequest ve http.StatusNotFound açıkça yazılı, üçüncü durum (http.StatusOK, hiçbir durum kodu yazılmadığında net/http’nin kendi varsayımı) hiçbir sabitle anılmadan örtük duruyor — toplam üç ayrı sonuç kaynaktan çıkarılabiliyor. Hangi isteğin hangi sonucu tetikleyeceği ise kaynaktan değil, işleyiciyi gerçekten çağırarak öğreniliyor.

durumKoduSayisiKaynakta bu sayımı gerçekten kod bloğunu bir araç ya da bir çalıştırma kullanmadan yapıyor: işleyicinin gövdesini bir dizgi olarak alıp içinde StatusBadRequest ve StatusNotFound sabitlerinin geçip geçmediğine bakıyor, StatusOK’in kendisi hiç yazılmadığı için onu örtük bir varsayım olarak sayıyor. Bu, önceki iki derste kurulan kaynak sayımı yordamının (func Test geçişlerini saymak, {" ile başlayan satırları saymak) bir başka biçimi — burada sayılan şey bir işlev adı ya da bir vaka satırı değil, bir sabitin adı, ama yöntem aynı: kaynağı bir dizgi olarak okuyup içinde aranan bir kalıbın kaç kez geçtiğini saymak.

// olcum.go — ayni isleyici uc istekle sinaniyor, gercek baglanti noktasi acilmiyor
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

type Kalem struct {
	Ad    string
	Adet  int
	Fiyat int
}

func kalemIsleyici(depo map[string]Kalem) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ad := r.URL.Query().Get("ad")
		if ad == "" {
			http.Error(w, "ad gerekli", http.StatusBadRequest)
			return
		}
		k, bulundu := depo[ad]
		if !bulundu {
			http.Error(w, "bulunamadi", http.StatusNotFound)
			return
		}
		fmt.Fprintf(w, "%d", k.Fiyat)
	}
}

const isleyiciKaynagi = `
		ad := r.URL.Query().Get("ad")
		if ad == "" {
			http.Error(w, "ad gerekli", http.StatusBadRequest)
			return
		}
		k, bulundu := depo[ad]
		if !bulundu {
			http.Error(w, "bulunamadi", http.StatusNotFound)
			return
		}
		fmt.Fprintf(w, "%d", k.Fiyat)
`

func durumKoduSayisiKaynakta(kod string) int {
	n := 1 // yazili sabiti olmayan varsayilan basarili donus
	for _, s := range []string{"StatusBadRequest", "StatusNotFound"} {
		if strings.Contains(kod, s) {
			n++
		}
	}
	return n
}

func main() {
	fmt.Println("-- kaynak: kac ayri durum kodu yazili --")
	fmt.Println("isleyicide yazili durum kodu sayisi:", durumKoduSayisiKaynakta(isleyiciKaynagi))

	depo := map[string]Kalem{"kalem": {"kalem", 3, 250}}
	isleyici := kalemIsleyici(depo)

	fmt.Println()
	fmt.Println("-- kosum: uc istegin karari --")
	istek1 := httptest.NewRequest(http.MethodGet, "/kalem?ad=kalem", nil)
	kayit1 := httptest.NewRecorder()
	isleyici.ServeHTTP(kayit1, istek1)
	fmt.Println("var olan kalem, durum kodu:", kayit1.Code, "govde:", kayit1.Body.String())

	istek2 := httptest.NewRequest(http.MethodGet, "/kalem?ad=olmayan", nil)
	kayit2 := httptest.NewRecorder()
	isleyici.ServeHTTP(kayit2, istek2)
	fmt.Println("olmayan kalem, durum kodu:", kayit2.Code)

	istek3 := httptest.NewRequest(http.MethodGet, "/kalem", nil)
	kayit3 := httptest.NewRecorder()
	isleyici.ServeHTTP(kayit3, istek3)
	fmt.Println("parametresiz istek, durum kodu:", kayit3.Code)
}
-- kaynak: kac ayri durum kodu yazili --
isleyicide yazili durum kodu sayisi: 3

-- kosum: uc istegin karari --
var olan kalem, durum kodu: 200 govde: 250
olmayan kalem, durum kodu: 404
parametresiz istek, durum kodu: 400

TS8. Kaynakta üç ayrı durum kodu yazılı olduğu bilinse de, hangi isteğin hangi kodu tetikleyeceği ancak ServeHTTP çağrıldıktan sonra okunuyor — üç httptest.NewRequest çağrısı birer istemci isteği kuruyor; üç httptest.NewRecorder bu isteklerin yanıtlarını ayrı ayrı topluyor ve kayit.Code her seferinde işleyicinin fiilen yazdığı durum kodunu döndürüyor. Bu, önceki iki derste kurulan aynı ayrımın ağ katmanındaki karşılığı: kaç sonuç olabileceği kaynaktan sayılabiliyor, hangi sonucun gerçekleştiği yalnız koşumdan okunabiliyor. Test Paketi Temelleri’nde bu ayrım bir işlevin geçip geçmediğiydi, Tablo Güdümlü Testler’de bir vakanın kaç alt teste dönüştüğüydü; burada aynı ayrım bir isteğin hangi durum koduna dönüştüğü sorusuna uygulanıyor — üç ders de aynı iskeleti farklı bir yüzeyde koşturuyor.

Üç isteğin de aynı isleyici değişkeni üzerinden geçmesi önemli — işleyici bir kez kuruluyor, sonra üç ayrı girdiyle üç kez çağrılıyor, tıpkı bir sunucunun gerçek çalışmasında aynı işleyicinin ardışık isteklere yanıt vermesi gibi. Aradaki fark, burada bu ardışık çağrıların bir dinleyici soketten değil, doğrudan bir döngüden geldiği.

İlk isteğin gövdesi de ayrıca okunuyor: kayit1.Body.String() yalnız durum kodunu değil, işleyicinin fmt.Fprintf ile yazdığı 250 dizgisini de döndürüyor — önceki derslerdeki envanter paketinin kuruş cinsinden fiyat kararı burada da aynen sürüyor. Diğer iki istekte gövde okunmuyor, çünkü http.Error çağrısının yazdığı metin bu ölçümün sorusuyla ilgisiz; sorulan tek şey durum kodu ve http.Error’ın ürettiği metin sürüme bağlı, kaynakta yazılı olmayan bir ayrıntı. Bu, ikinci ve üçüncü ölçümde araç çıktısının kendisini basmama kuralının doğal bir uzantısı: yalnız sorulan ölçü okunuyor, geri kalan her şey görmezden geliniyor.

Sınırlayıcı Ölçüm: Sınanan Şey Ağ Değil İşleyici

TS9. httptest.NewRequest‘in ürettiği isteğin RemoteAddr alanı hep aynı sabit değeri taşıyor: 192.0.2.1:1234. Bu adres rastgele değil — RFC 5737’nin belgeleme ve örnek amaçlı ayırdığı, hiçbir gerçek ağda yönlendirilmeyen bir blok. httptest paketi bu adresi her çağrıda aynen kullanıyor, çünkü ürettiği istek hiçbir zaman gerçek bir bağlantıdan gelmiyor; bir bağlantı hiç kurulmadığı için gerçek bir uzak adres de yok.

Gerçek bir HTTP sunucusunda RemoteAddr alanı, bağlantıyı açan istemcinin gerçek adresini taşır ve her istekte, her koşumda değişebilir — tam olarak bu konunun başından beri basılmaması söylenen türden bir bilgi. httptest.NewRequest’in bu alana sabit bir değer koyması, aracın kendi tasarımının bir sonucu: bir istek gerçek bir bağlantıdan gelmediği için, dolduracak gerçek bir adres de yok ve paket bu boşluğu rastgele ya da boş bırakmak yerine, hiçbir gerçek ağda karşılığı olmayan sabit bir değerle dolduruyor. Bu seçim, bir işleyicinin RemoteAddr alanına bakan bir mantığı (örneğin bir erişim kısıtlaması) varsa, bu mantığın httptest altında hep aynı sabit adresle karşılaşacağını da ima ediyor — bu ölçümde kalemIsleyici’nin böyle bir mantığı yok, ama bu sınır ileride benzer bir işleyici sınanırken hatırlanması gereken bir ayrıntı — bir erişim kısıtlaması gerçek bir dağıtımda doğru çalışsa bile, bu test yardımcısıyla yazılmış bir sınama o kısıtlamayı hiçbir zaman gerçekten sınamaz, çünkü sınamanın kendisi sabit bir adresle çalışır.

// sinir.go — istegin adresi belge amacli ayrilmis sabit bir blok, gercek baglanti degil
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

func main() {
	istek := httptest.NewRequest(http.MethodGet, "/kalem?ad=kalem", nil)
	belgeAmacliMi := strings.HasPrefix(istek.RemoteAddr, "192.0.2.")
	fmt.Println("istegin adresi belge amacli ayrilmis bir blokta mi:", belgeAmacliMi)
}
istegin adresi belge amacli ayrilmis bir blokta mi: true

Bu derste kullanılan test yardımcısı, dolayısıyla, işleyicinin mantığını sınıyor — girdiye göre doğru durum kodunu üretip üretmediğini — ağ katmanının kendisini değil. Gerçek bir ağ bağlantısı burada hiç kurulmadı, hiç dinlenmedi, hiç kapatılmadı; sınama tamamen aynı sürecin içinde, bir işlev çağrısı zincirinde kaldı. Bu bir eksiklik değil, bilinçli bir sınır: bir işleyicinin mantığını sınamak için gerçek bir soket açmak gereksiz bir maliyet ve gereksiz bir kırılganlık kaynağı — bir bağlantı noktasının başka bir süreç tarafından kullanılıyor olması gibi, işleyicinin kendi mantığıyla hiç ilgisi olmayan bir başarısızlık nedeni ekliyor. Ama bu sınırın bir bedeli de var: bu test, gerçek bir dinleyicinin kurulumunda, zaman aşımlarında ya da eşzamanlı bağlantı yönetiminde çıkabilecek bir kusuru hiç yakalayamıyor, çünkü o katman bu ölçümde hiç çalışmadı.

Bu ayrım, işleyicinin kendi imzasında da görünür duruyor: http.HandlerFunc, bir http.ResponseWriter ile bir *http.Request alan sıradan bir Go işlevi — ne bir soket tanıtıcısı alıyor ne de bir ağ bağlamı. İşleyicinin gördüğü tek şey bu iki değer ve test yardımcıları da tam olarak bu iki değeri, gerçek bir bağlantı kurmadan üretmenin bir yolu. Bu yüzden bir işleyicinin ağdan mı yoksa httptest’ten mi çağrıldığını, işleyicinin kendi gövdesinden anlamak mümkün değil — işleyici ikisi arasındaki farkı hiç görmüyor, yalnız kendisine verilen http.ResponseWriter ve *http.Request çiftiyle çalışıyor. Bu, işleyicinin test edilebilirliğinin kaynağı: arayüzün kendisi (bu iki tip) sabit kaldığı sürece, arkasındaki gerçekleştirim (gerçek bir bağlantı mı, bir kayıt yapısı mı) işleyicinin gözünde fark etmiyor. Sıradaki ders aynı gözlemi bir adım öteye taşıyor — işleyicinin kendisi değil, işleyicinin çağırdığı bir bağımlılık bu kez arayüzün arkasına alınıyor.

Özet

  • httptest.NewRequest istemci tarafı, httptest.NewRecorder sunucu tarafı bir yardımcı; ikisi birlikte bir işleyiciyi gerçek bir bağlantı noktası açmadan, doğrudan bir işlev çağrısıyla sınıyor.
  • İşleyicideki durum kodu sayısı (üç) kaynaktan sayılabiliyor, ama hangi isteğin hangi kodu tetikleyeceği yalnız ServeHTTP çağrılarak öğrenilebiliyor.
  • Aynı isleyici değişkeni üç farklı istekle art arda çağrıldı; üçünün de kararı bir koşum bilgisi olarak okundu, adres ve bağlantı noktası hiçbir yerde basılmadı.
  • Sınırlayıcı ölçüm: httptest.NewRequest‘in ürettiği isteğin adresi RFC 5737’nin belge amaçlı ayırdığı sabit bir blokta — bu, sınanan şeyin ağ değil işleyicinin kendisi olduğunun kanıtı.
  • Bu sınır bir eksiklik değil bilinçli bir seçim, ama bedeli de var: gerçek bir dinleyicinin davranışına bağlı kusurlar bu testte hiç ortaya çıkmıyor.
  • httptest.NewServer gerçek bir bağlantı noktası açabiliyor, ama bu ders onu kullanmadı; işleyicinin mantığını sınamak için gerçek bir ağ yoluna hiç ihtiyaç olmadığı için bu maliyet baştan dışlandı.

Sonraki Adım

Bu derste bir işleyicinin davranışının gerçek bir bağlantı hiç kurulmadan, doğrudan çağrılarak sınandığı ölçüldü: kaynakta kaç ayrı sonuç yazılı olduğu sayılabildi, hangi isteğin hangi sonucu doğurduğu yalnız koşarak öğrenildi ve isteğin kendisinin gerçek bir ağdan gelmediği isteğin adresi okunarak doğrulandı. Ama işleyicinin kendisi hâlâ, main işlevinin içinde elle kurulan sabit bir sözlük kullanıyordu — gerçek bir üretim programında bu veri bir dış bağımlılıktan, örneğin bir depolama katmanından geliyor olurdu ve o katman her sınamada gerçekten çalıştırılamayacak kadar yavaş ya da kararsız olabilirdi. Sıradaki ders bu bağımlılığı bir arayüzün arkasına alıyor ve aynı test gövdesinin gerçek ve sahte bir gerçekleştirimle nasıl koştuğunu ölçüyor — sahteyi mümkün kılan şeyin, arayüzün kaynakta yazılı olması olduğunu göstererek.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat