Golang Fiber ile Integration Test Geliştirmek

·Golang·
#Golang#Gofiber#Integration Test#Test

Golang Fiber ile Integration Test Geliştirmek

Bu makalemizde Golang üzerindeki Gofiber frameworkü üzerinde nasıl integration test geliştirebiliriz bu konu üzerine konuşacağız. Integration test nedir buna değinerek başlayabiliriz.

Integration test nedir?

Integration test unitlerin birleştiğinde istenilen davranışı sergilemesini kontrol ettiğimiz testler bütünüdür. Örnek olarak bahsedersek uygulamamızda A, B ve C işlemleri için unit testler geliştirdik ve üç işlem de testlere uygun çıktılar sağladı. Ardından bu üç işlemi kullanarak farklı işlemler gerçekleştiren yeni bir fonksiyon oluşturduk. Bu fonksiyonun test edilmesine integration test diyebiliriz. 
Aynı zamanda HTTP testleri de integration testlerine girmektedir. Bugün vereceğimiz örneklerimizde de Fiber Handler yani controllerlarımız üzerinde test gerçekleştireceğiz.

Integration test önemi nedir?

Integration testleri uygulamamızda yüksek önem taşımaktadır. Testlerin yazıldığı fonksiyonlar üzerinde değişiklikler gerçekleştiğinde uygulama halen belirtilen çıktıları bize sağlıyor mu, düzgün şekilde işleyişi mevcut mu bunları göz ile görmektense otomatik şekilde tespitini sağlamaktadır. Anlaşılması için bir örnek sunalım.

Sisteminizde login olmanızı sağlayan ve JWT token üreten bir kontrolcü mevcut olduğunu varsayalım. Bu kontrolcünün doğru şekilde çalıştığından emin olmamız için önce bir dummy user oluşturmamız gerekir. İlk aşamadaki testimiz bizim kullanıcımızın oluşup oluşmadığını kontrol etmektedir. Eğer ilk testte belirtilen çıktı gerçekleştiyse login işlemini gerçekleştirebiliriz. İkinci test koşulu olarak da oluşturulmuş kullanıcının gerçekten giriş yaptığında JWT token elde edebildiğini kontrol etmemiz gerekir. Bu iki koşul sağlandığında sisteminizdeki kullanıcılar gerçek ortamda da sorunsuz şekilde giriş yapabildiğini varsayabiliriz. Önemi ise bu login sistemi üzerinde iyileştirmeler ve yeni eklemeler yaptığınızda hata çıkma olasılığı her an mevcuttur ancak yazdığımız bu testler sayesinde tek tuş ile kullanıcılar halen başarılı şekilde akışa uygun işlem gerçekleştirebiliyor mu test edebiliriz.

Test koşullarını nasıl hazırlarım?

Buradaki önemli kısım platform bağımlı düşünmek değil, bu uygulamanın otomatize testlerinde ne gibi koşullarla karşılaşılmasının beklendiğidir. Örneğin bir modelinizde CRUD işlemleri gerçekleştiren handlerlarınız olduğunu düşünün. Create işleminde test edeceğimiz şey validasyonların gerçekten çalıştığının kontrolü (boş alanlar, unique alan ihlali…), doğru girdiler verildiğinde gerçekten istenilen çıktı elde edilir mi bunların kontrolü yapılır. Delete işleminde hiç varolmayan bir girdi verildiğinde HTTP response kodu ne döner, hata mesajı gerçekten handle edilmiş midir gibi koşulları kontrol edersiniz. Bu noktada oluşturacağınız testler tamamen hayal gücünüze kalmış ancak yukarıdakiler temel olarak bulundurabileceğiniz örneklerdir.

Gofiber ile Integration Test nasıl geliştirilir?

Gofiber üzerinde integration testleri geliştirmek için klasik Golang test şablonundan dışarı çıkmayacağız. Ben dış bir kütüphane eklemedim (testify gibi) ancak bunun tercihi de size kalmış. Önemli olan temel mantığı kavramak.

Ben örnek testimizi daha önce paylaştığım golang-rest-api-boilerplate üzerinden anlatacağım. O örnekte tanımladığımız Post modeli üzerindeki Index ve Create handlerlarına test geliştirdim. Burada bir Create handlerine nasıl test yazmışım bunu inceleyelim.

İlk önce create_test.go dosyamı create.go dosyasının bulunduğu dizinde oluşturuyorum. Ardından dosyanın içeriğini aşağıdaki şekilde tanımlıyorum. Tanımlamanın ardından gerekli kod blokları üzerine konuşacağım.

package post
 
import (
    "bytes"
    "encoding/json"
    "io"
    "net/http/httptest"
    "testing"
 
    "github.com/gofiber/fiber/v2"
)
 
func TestCreate(t *testing.T) {
    type wanted struct {
        statusCode   int
        expectedKeys []string
    }
 
    // Create fiber app for testing purposes
    app := fiber.New()
    app.Post("/", Create)
 
    // Tests struct
    tests := []struct {
        name        string
        description string
        endpoint    string
        payload     any
        want        wanted
    }{
        {
            name:        "Create a fake post",
            description: "This test should return 200 status code and created post",
            endpoint:    "/",
            payload: map[string]any{
                "title":   "Example post",
                "content": "Lorem ipsum",
            },
            want: wanted{
                statusCode: 200,
                expectedKeys: []string{
                    "id",
                    "title",
                    "content",
                },
            },
        },
        {
            name:        "Create a post with blank content",
            description: "This test should return 400 status code and error message",
            endpoint:    "/",
            payload: map[string]any{
                "title":   "Example post2",
                "content": "",
            },
            want: wanted{
                statusCode: 400,
                expectedKeys: []string{
                    "message",
                },
            },
        },
    }
 
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            body, err := json.Marshal(tt.payload)
            if err != nil {
                t.Errorf("Cannot parse body: %v", err)
                t.Fail()
            }
 
            req := httptest.NewRequest("POST", tt.endpoint, bytes.NewReader(body))
            req.Header.Add("Content-Type", "application/json")
 
            // Create request
            res, err := app.Test(req)
            if err != nil {
                t.Errorf("Cannot test Fiber handler: %v", err)
                t.Fail()
            }
 
            // Assertions
            if res.StatusCode != tt.want.statusCode {
                t.Errorf("Expected status code %d, got %d", tt.want.statusCode, res.StatusCode)
            }
 
            answer, err := io.ReadAll(res.Body)
            if err != nil {
                t.Errorf("Cannot parse body: %v", err)
            }
 
            var message map[string]any
            err = json.Unmarshal([]byte(answer), &message)
            if err != nil {
                t.Errorf("Cannot unmarshal response: %v", err)
            }
 
            for _, s := range tt.want.expectedKeys {
                if _, ok := message[s]; !ok {
                    t.Errorf("Expected response body to contain key %s but it can not be found", s)
                }
            }
        })
    }
}

İlk aşamada TestCreate isimli fonksiyonumuzu oluşturuyor ve inbuilt test kütüphanesini parametre olarak fonksiyona ekliyoruz.

Ardından testin çıktısından tam olarak ne beklediğime dair verileri ekleyeceğim wanted isimli bir struct tanımlıyorum. Ben dönen status codeları ve bodyden gelen JSON keylerini kontrol etmek istiyorum. Bu alanları structumda oluşturuyorum.

Bir gofiber uygulamasını test etmek için fiber.New() diyerek fiber uygulamamı oluşturuyorum. Ardından test edeceğim handlerin rotasını ekliyorum. Siz isterseniz birden çok rota ekleyerek zincirleme testler de geliştirebilirsiniz. Biz bu örnekte tek bir rota üzerinden ilerleyeceğiz.

Ardından tests structumu oluşturuyorum. Burada olmazsa olmazlarımız name, description ve want fieldlarıdır. Ben daha dinamik olması adına endpoint ve payload isimli iki field daha ekliyorum. 

Sonraki aşamada bu structun içeriğini oluşturmamız gerekiyor. Bu aşamada iki adet test oluşturuyorum. Birinde başarılı şekilde gönderimin oluşturulup, 200 status code döndürmesini ve JSON’u unmarshal ettiğimde istediğim keylerin bulunup bulunmadığını test ediyorum. Normal şartlarda gönderimin oluşturulup id, title ve content döndürmesi gerektiğini biliyorum.

İkinci testimde ise uygulamam content kısmının boş olmasını kabul etmeyip 400 döndürüyor. Ben de bunun gerçekten istenilen şekilde çalışıp çalışmadığını kontrol etmek için boş contente sahip bir gönderi oluşturtuyorum. Ardından 400 status code dönmesini ve dönen içerikte de sadece message alanının olmasını bekliyorum.

Testlerimi tanımladığıma göre artık gerçek kontrolleri yapmaya başlayabiliriz. for döngüsü ile testlerimi çalıştırmaya başlıyorum. Test kontrol koşullarıt.Run() fonksiyonu içerisinde yazacağım. 
İlk aşamada test payloadımın marshal edilip edilemediğini kontrol ediyorum. Bu aşama gerçekleşmezse diğer aşamalarda ilerlenmesinin bir anlamı olmadığından t.Fail() diyerek testi anında sonlandırıyorum.

Ardından handlerımı test etmek için bir HTTP requesti göndermem lazım. Bunu da inbuilt gelen httptest kütüphanesi ile gerçekleştiriyorum. Yeni bir post requesti oluşturup endpointi ve payloadımı ekliyorum. Payloadımın application/json olduğunu belirtmem fiber için çok önemli. Aksi takdirde bodyParser içeriğimizi kullanamayacaktır.

Requesti oluşturduktan sonra app.Test(req) diyerek fiber içerisinde gelen test fonksiyonu ile çalıştırıyorum. Çalıştırdıktan sonra ilk aşamada beklediğim status code gelmiş mi diye kontrol ediyorum.
Bu aşamadan sonra da gelen içeriği parse edip gerekli keyler var mı diye kontrol ettikten sonra testimi sonlandırıyorum.

Golang Fiber ile Integration Test Geliştirmek

VSCode üzerinde Go eklentisi kurulu olduğunda testleri bu kısımdan kolayca görüntüleyip çalıştırabiliyoruz.

Yazdığım testlerin bulunduğu kodlara şu repodan ulaşabilirsiniz: go-rest-api-boilerplate/app/controllers/post at main · dogukanoksuz/go-rest-api-boilerplate (github.com)

Okuduğunuz için teşekkür ederim. Sorularınız ve geri bildirimleriniz için aşağıdaki yorum bölümünü kullanabilirsiniz. İyi kodlamalar <3