Yayına almak
Test
Gerçek uygulamayı app.Handler() ve net/http/httptest üzerinden test edin — sayfalar, sahtecilik token'larıyla formlar, document'lar ve statik dışa aktarma.
Bir collage uygulaması bir http.Handler'dır. app.Handler() onu döndürür; dinleyen
bir sunucu, seçilecek bir port yoktur. Böylece net/http/httptest, sitenin tamamını
sıradan bir Go testinden sürer: route'lar, data handler'lar, şablonlar, önbellek,
formlar, document'lar ve middleware, tam olarak üretim ortamında çalıştıkları gibi.
collage new'in iskeletini kurduğu proje, bu şekilde yazılmış bir test dosyasıyla
gelir. Bu sayfa o dosyanın kalıbını adım adım anlatır.
main'in kurduğu uygulamayı test edin
İskeletteki main.go, uygulamayı kendine ait bir fonksiyonda kurar:
func newApp(devMode bool, port int) (*collage.App, error)
main onu sunmak için, testler de test etmek için çağırır. Test dosyasındaki en
önemli karar budur: testlerin sınadığı şey, gerçek yapılandırması, route'ları ve
mount'larıyla gerçekten çalışan sitedir; ondan yavaş yavaş uzaklaşan ikinci bir
kurulum değil.
func handler(t *testing.T) http.Handler {
t.Helper()
cacheDir = t.TempDir()
app, err := newApp(false, 0)
if err != nil {
t.Fatalf("newApp() = %v, want nil", err)
}
return app.Handler()
}
// get returns the response to a GET of target, and its body.
func get(t *testing.T, h http.Handler, target string) (*httptest.ResponseRecorder, string) {
t.Helper()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil))
return rec, rec.Body.String()
}
newApp(false, 0) üretim yapılandırmasını kurar: geliştirme modu kapalı ve port
0; collage bunu kendi varsayılanıyla değiştirir — zaten hiçbir şey dinlemez.
Bundan sonra bir test, bir istek ve yanıta bir bakıştan ibarettir:
func TestPagesRender(t *testing.T) {
h := handler(t)
for target, want := range map[string]string{
"/": "Explore the features",
"/features": "Four live demos",
"/hello": "Hello, stranger!",
} {
rec, body := get(t, h, target)
if rec.Code != http.StatusOK {
t.Errorf("GET %s = %d, want 200", target, rec.Code)
continue
}
if !strings.Contains(body, want) {
t.Errorf("GET %s does not contain %q", target, want)
}
// Counted, not merely found: a layout writing one and a page hoisting
// another is two titles, which is a page that looks fine and is not.
if n := strings.Count(body, "<title>"); n != 1 {
t.Errorf("GET %s has %d titles, want exactly 1", target, n)
}
}
}
handler(t)'nin yaptığı gibi her test için yeni bir handler kurun. app.Handler()'a
yapılan ilk çağrı uygulamayı başlatır ve kaydı kapatır; böylece her test boş bir
önbellekle başlar.
Başlatma başarısız olursa — bir plugin'in Init'i hata verirse, bir sayfa kayıtlı
olmayan bir hata sayfasını adlandırırsa, bir mount bir route'u gölgelerse —
app.Handler() her isteğe 503 ile yanıt verir ve nedenini loglar. Bu başarısızlığı
testte bir hata olarak almak istediğinizde önce app.Start()'ı çağırın:
if err := app.Start(); err != nil {
t.Fatalf("start: %v", err)
}
Disk önbelleğini yalıtın
İskeletin önbellek dizini bir paket değişkenidir ve handler, uygulamayı kurmadan
önce onu yeni bir dizine yönlendirir:
// cacheDir is where rendered pages are kept between restarts. A variable so the
// tests can point it at a directory of their own.
var cacheDir = ".cache"
Geliştirme modu kapalıyken uygulama disk önbelleğini kullanır ve bir disk önbelleği,
aynı dizini ve aynı build'i kullanan her şey tarafından paylaşılır. cacheDir = t.TempDir() olmadan bir teste, daha önceki bir testin — ya da önbellek süreçten
sağ çıktığı için daha önceki bir çalıştırmanın — render ettiği bir sayfa sunulabilir
ve test, kodla hiçbir ilgisi olmayan nedenlerle geçebilir ya da kalabilir. Ayrıca
paketinizde bir .cache dizini bırakırdı.
cacheDir tüm paket tarafından paylaşıldığı için bu testler t.Parallel()
çağırmaz. Paralel testler istiyorsanız dizini bunun yerine newApp'e bir parametre
olarak verin.
Önbellekle ilgili bir test bundan yararlanabilir: aynı handler'a iki istek yapın ve
ikincisinin önbellekten sunulduğunu doğrulayın ya da aralarında
app.InvalidateTags'i çağırın ve sunulmadığını doğrulayın.
Formlar ve sahtecilik token'ı
Güvenli olmayan bir metodun arkasındaki her action — bir form gönderimi, bir
fetch() — bir istek sahteciliği token'ı denetler ve bir testin de bunu bir
tarayıcının yaptığı gibi göndermesi gerekir:
- Formun bulunduğu sayfaya
GETyapın. Yanıtcollage_csrfçerezini ayarlar ve sayfadaki{{csrfToken}}, aynı token'ı taşıyan gizli bir_csrfalanı render eder. - Token'ı sayfadan okuyun.
- Formu, token
_csrfalanında — ya da birfetch()'in yaptığı gibiX-CSRF-Tokenheader'ında — ve 1. adımdaki çerezle birliktePOSTedin.
İskeletin yardımcıları:
// token reads the forgery token out of a rendered page, the way a browser does.
func token(t *testing.T, body string) string {
t.Helper()
m := regexp.MustCompile(`name="_csrf" value="([^"]+)"`).FindStringSubmatch(body)
if m == nil {
t.Fatalf("no forgery token in the page:\n%s", body)
}
return m[1]
}
// post sends a POST to target, carrying the cookies from page.
func post(t *testing.T, h http.Handler, target string, form url.Values, header http.Header, page *httptest.ResponseRecorder) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, target, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
for name, values := range header {
req.Header[name] = values
}
if page != nil {
for _, c := range (&http.Response{Header: page.Header()}).Cookies() {
req.AddCookie(c)
}
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
(&http.Response{Header: page.Header()}).Cookies(), recorder'ın Set-Cookie
header'larını standart kütüphanenin kendi çerez ayrıştırıcısıyla ayrıştırır; böylece
test tam olarak bir tarayıcının geri göndereceği şeyi gönderir.
Token'ı alanda taşıyan bir form gönderimi:
func TestHelloGreetsTheSubmittedName(t *testing.T) {
h := handler(t)
page, body := get(t, h, "/features")
rec := post(t, h, "/hello", url.Values{"_csrf": {token(t, body)}, "name": {"Ada"}}, nil, page)
if rec.Code != http.StatusOK {
t.Fatalf("POST /hello = %d, want 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), "Hello, Ada!") {
t.Errorf("the response does not greet the name:\n%s", rec.Body.String())
}
}
Token'ı header'da taşıyan bir JSON uç noktası:
func TestCountAnswersWithTheNewCount(t *testing.T) {
h := handler(t)
page, body := get(t, h, "/features")
rec := post(t, h, "/api/count", nil, http.Header{"X-Csrf-Token": {token(t, body)}}, page)
if rec.Code != http.StatusOK {
t.Fatalf("POST /api/count = %d, want 200", rec.Code)
}
var answer struct{ Count int64 }
if err := json.Unmarshal(rec.Body.Bytes(), &answer); err != nil {
t.Fatalf("body = %q, want JSON: %v", rec.Body.String(), err)
}
if answer.Count < 1 {
t.Errorf("count = %d, want at least 1 after a click", answer.Count)
}
}
Ve korumayı açık tutan test:
// Without a token a submission never reaches its handler. This is the test that
// fails if the protection is ever turned off by accident.
func TestASubmissionWithNoTokenIsRefused(t *testing.T) {
h := handler(t)
for _, target := range []string{"/api/count", "/hello"} {
if rec := post(t, h, target, url.Values{"name": {"Ada"}}, nil, nil); rec.Code != http.StatusForbidden {
t.Errorf("POST %s with no token = %d, want 403", target, rec.Code)
}
}
}
Token Security.CSRFKey ile, anahtar ayarlanmamışsa uygulama başına üretilen bir
anahtarla imzalanır. Her iki durumda da bir testteki sayfa ve gönderim aynı
uygulamaya gider; bu yüzden testlerin kendilerine ait bir anahtara ihtiyacı yoktur.
_csrf ve collage_csrf varsayılan adlardır; bunları Security.CSRFFieldName ya da
CSRFCookieName ile yeniden adlandıran bir uygulama, testlerinde de yeniden
adlandırır.
Document'lar, yönlendirmeler ve durum kodları
Bir document, bir sayfa gibi test edilir. Gövdenin yanında içerik tipini de denetleyin; çünkü içerik tipi, bir document'ın ne olduğunun yarısıdır:
func TestHealthCheck(t *testing.T) {
rec, body := get(t, handler(t), "/healthz")
if rec.Code != http.StatusOK {
t.Fatalf("GET /healthz = %d, want 200", rec.Code)
}
var health struct{ Status string }
if err := json.Unmarshal([]byte(body), &health); err != nil || health.Status != "ok" {
t.Errorf("body = %q, want a status of ok", body)
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
t.Errorf("Content-Type = %q, want application/json", ct)
}
}
Bulunamadı sayfası, Allow header'ıyla bir 405, bir yönlendirmenin Location'ı,
bir Cache-Control header'ı — her biri recorder üzerindeki bir alandır:
func TestNotFoundPage(t *testing.T) {
rec, body := get(t, handler(t), "/there-is-nothing-here")
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404", rec.Code)
}
if !strings.Contains(body, "There is nothing at this address") {
t.Errorf("body = %q, want this site's own not-found page", body)
}
}
app.Use ile kaydedilen middleware da handler'ın parçası olduğu için bu testlerde
çalışır. Bir önizlemeyi ya da bir collage.Vary boyutunu test etmek için
ServeHTTP'den önce istekteki çerezi ya da header'ı ayarlayın.
Her sayfa render edilir
Her sayfayı ziyaret eden bir test, yalnızca bunlardan birinde başarısız olan şablonu
yakalar. collage-docs içeriğini yükler ve her sayfayı ister — burada yukarıdaki
handler ve get yardımcılarıyla, tüm tur için tek bir uygulama:
// Every page of the documentation renders, with its own title.
func TestEveryDocRenders(t *testing.T) {
loaded, err := site.Load(content.FS)
if err != nil {
t.Fatalf("site.Load: %v", err)
}
h := handler(t)
for _, page := range loaded.Pages() {
rec, body := get(t, h, page.URL())
if rec.Code != http.StatusOK {
t.Errorf("GET %s = %d", page.URL(), rec.Code)
continue
}
if !strings.Contains(body, "<title>"+page.Title+" — collage</title>") {
t.Errorf("GET %s has no title %q", page.URL(), page.Title)
}
}
}
Sayfaları bir CMS'ten gelen bir site için aynı test, path provider'ınızın döndürdüğü listeyi dolaşabilir.
Dışa aktarmayı test etmek
Statik dışa aktarma da koddur ve atlanan bir sayfanın üretim ortamında sessiz kaldığı
tek yerdir. Onu t.TempDir() içine çalıştırın ve beklediğiniz dosyaların orada
olduğunu denetleyin:
// The export writes the home page, every doc and the 404 page.
func TestExport(t *testing.T) {
cacheDir = t.TempDir()
app, err := newApp(false, 0)
if err != nil {
t.Fatal(err)
}
out := t.TempDir()
if err := staticBuild(app, out, false); err != nil {
t.Fatalf("staticBuild: %v", err)
}
loaded, _ := site.Load(content.FS)
want := []string{"index.html", "404.html"}
for _, page := range loaded.Pages() {
want = append(want, filepath.Join("docs", page.Slug, "index.html"))
}
for _, file := range want {
if _, err := os.Stat(filepath.Join(out, file)); err != nil {
t.Errorf("%s was not written: %v", file, err)
}
}
}
collage export'un çalıştırdığı staticBuild'in aynısını çağırır; böylece test,
sayfaların yanında path provider'ı ve builder seçeneklerini de kapsar.
staticBuild, build'in hatasını döndürür; bu da bozulmuş (degraded) bir sayfa, boş
bir render ya da bir panic için testi başarısız kılar.
Atlamalar ve uyarılar üzerinde de doğrulama yapmak için builder'ı doğrudan çağırın ve raporu okuyun:
builder, err := collage.NewBuilder(app, collage.BuildOptions{OutDir: t.TempDir()})
if err != nil {
t.Fatal(err)
}
report, err := builder.Build(context.Background())
if err != nil {
t.Fatalf("build: %v", err)
}
for _, skip := range report.Skipped {
if skip.Page == "home" {
t.Errorf("home was skipped: %s", skip.Reason)
}
if errors.Is(skip.Err, collage.ErrDynamicPathUnresolved) {
t.Errorf("%s has a {param} the path provider does not cover", skip.Page)
}
}
if len(report.Warnings) > 0 {
t.Errorf("warnings: %v", report.Warnings)
}
v0.10.0'dan itibaren skip.Err, her atlamanın arkasındaki sentinel hatadır; bkz.
Hatalar.
HTTP olmadan render etmek
app.RenderPath, tek bir sayfayı dışa aktarmanın yaptığı gibi render eder — istek
yok, sayfa önbelleği yok, middleware yok — ve HTML'i ve render'ın ne yaptığını
döndürür. Plugin'lerinizin render hook'ları yine çalışır, veri önbelleği de öyle:
collage.Cached'ın bir render ya da
sunulan bir istek sırasında sakladığı değer, aynı uygulamadaki bir sonraki
RenderPath'e verilen değerdir. Bir test yeni veriye ihtiyaç duyduğunda yeni bir
uygulama kurun:
result, err := app.RenderPath(context.Background(), "/blog/hello-world", "", nil)
if err != nil {
t.Fatalf("render: %v", err)
}
if result.Degraded() {
t.Errorf("a fragment failed: %+v", result.Metadata)
}
app.RenderDocumentPath aynısını bir document için yapar. Çoğu test için
app.Handler() daha uygundur, çünkü bir okuyucunun ne aldığını test eder; bunlar
tek bir render'a ayrıntılı bakmak içindir.
Testleri çalıştırmak
go test ./...
Bunu CI'da, build almadan ya da dışa aktarmadan önce çalıştırın. Gerçek newApp'i
kullanan bir test, sitenin başlamasını engelleyecek her şeyde orada başarısız olur;
bu, sunucuda öğrenmekten daha ucuzdur.