Go ile Test Driven Development ile API Geliştirme (Handler Test) Bölüm-1

Anıl Aydın
CodeBrew
Published in
9 min readAug 8, 2023

Herkese merhaba bu yazımda çok önemsediğim ve olmazsa olmaz olarak gördüğüm Test Driven Development tekniği ile Go dilinde HTTP API geliştireceğiz.

Başlamadan önce yazılımda testin öneminden bahsetmek istiyorum. Neden yazıyoruz ?

  • Hata Tespit Etme: Testler, yazılımın farklı bileşenlerini ve işlevlerini test ederek potansiyel hataları tespit etmeye yardımcı olur. Bu sayede, yazılımın çalışması sırasında ortaya çıkabilecek hataların önüne geçilir ve yazılımın daha güvenilir olması sağlanır.
  • Kalite ve Güvenilirlik Sağlama: İyi test edilmiş yazılım, daha yüksek kalitede ve güvenilir bir ürün olarak kullanıcılara sunulur. Testler, yazılımın beklenen işlevlerini doğru bir şekilde yerine getirdiğini ve kullanıcıların taleplerine uygun olduğunu doğrulamaya yardımcı olur.
  • Değişikliklerin Etkisini Anlama: Yazılım sürekli olarak geliştirilir ve değiştirilir. Bu değişikliklerin, mevcut kod tabanına ve diğer bileşenlere olan etkisini anlamak için testlerin yazılması gerekir. Böylece, yeni eklenen kodların veya yapılan değişikliklerin mevcut işlevselliği etkilememesi sağlanır.
  • Dokümantasyon ve Anlaşılırlık: Testler, yazılımın nasıl çalıştığını belgelemeye yardımcı olur ve diğer geliştiricilerin veya testçilerin yazılımı anlamasını kolaylaştırır. Testler, kodun amacını ve kullanımını daha anlaşılır hale getirir.
  • Refactoring Desteği: Yazılımın zamanla bakım ve geliştirme süreçlerinde, kod tabanının düzenlenmesi veya yeniden yapılandırılması gerekebilir. Testler, bu tür değişikliklerin güvenli bir şekilde gerçekleştirilmesine olanak tanır ve varolan işlevselliğin bozulup bozulmadığını kontrol eder.
  • Uzun Dönemli Tasarruf: Testler, yazılımın hatalarının erken aşamalarda tespit edilmesine ve düzeltilmesine yardımcı olur. Bu, yazılımın uzun vadede daha az bakım gerektirmesine ve maliyetlerin azaltılmasına katkı sağlar.
  • Kod Kapsamı ve Test Kapsamı Ölçümü: Testler, kodun hangi kısımlarının test edilip edilmediğini gösteren bir ölçüt olarak kullanılabilir. Bu, yazılımın ne kadarının test edildiğini ve ne kadarının test edilmediğini belirlemeye yardımcı olur.
  • Müşteri Memnuniyeti: İyi test edilmiş yazılım, kullanıcıların beklentilerine daha iyi cevap verir ve daha az hata içerir. Bu da müşteri memnuniyetini artırır ve yazılımın itibarını güçlendirir.
  • Kod Değişikliklerini İzleme: Büyük yazılım projelerinde birden fazla geliştirici çalışabilir. Testler, diğer ekipteki geliştiricilerin yaptığı kod değişikliklerinin mevcut işlevselliği etkilemediğini doğrulamaya yardımcı olur.
  • Geliştirici Güveni ve Özgüveni: İyi test edilmiş bir yazılımda, geliştiriciler kendi kodlarının güvenli olduğunu ve diğer kodlarla uyumlu çalıştığını bilerek daha özgüvenle çalışabilirler.

Yukarıda bahsi geçen maddeleri test yazarak kazanım olarak elde edebilirsiniz.

Bende size bu yazımda adım adım TDD (Test Driven Development) kullanarak Go dilinde nasıl uyguladığımı mümkün olduğunca açık bir şekilde anlatacağım.

İlk olarak oluşturacağım projeden bahsetmek istiyorum. Proje basit CRUD işlemlerini yapabildiğimiz bir öğrenci kayıt sistemi olacak. Her bir fonksiyonu uçtan uca ilk önce testini yazıp ardından kodu implemente edeceğim. Öyleyse başlayalım.

İlk olarak projem için tdd-example adında bir klasör oluşturalım. Ve içerisine girelim.

mkdir tdd-example
cd tdd-example

Ardından projemize bir go.mod dosyası ekleyelim.

go mod init github.com/anilaydinn/tdd-example

Bu komutu çalıştırdıktan sonra proje dizininiz aşağıdaki gibi olmalı.

go mod init komutundan sonraki proje dosyaları

Go projemizin bir entrypoint noktası yani main fonksiyonu olması gerekiyor. Bu sebepten hemen bir main.go dosyası oluşturuyorum.

touch main.go

Kodlamaya başlamadan önce bize gerekli olan kullanacağımız paketleri go get komutunu kullanarak projemize ekleyelim.

İlk olarak çok sevdiğim Express JS ile çok benzeyen HTTP engine olan Fiber paketini ekliyorum.

go get github.com/gofiber/fiber/v2

Ardından test edeceğim yapıların bağımlı olduğu yapıları mocklamak için mockgen ve gomock paketlerini ekliyorum.

go install github.com/golang/mock/mockgen@v1.6.0
go get github.com/golang/mock/gomock

Ardından main.go dosyama aşağıdaki satırları ekleyip go run komutu ile uygulamamı bir kez çalıştırıyorum. Bunu yapmamın sebebi Fiber paketi ile doğru bir şekilde çalıştığını görmek istemem.

package main

import "github.com/gofiber/fiber/v2"

func main() {
app := fiber.New()

app.Listen(":8080")
}

main.go dosyamın son hali yukarıdaki gibi iken bir kez uygulamamı çalıştırıyorum.

go run main.go
go run main.go komutunun çıktısı

Eğer sizde yukarıdaki gibi bir çıktı aldıysanız bu API’imizin doğru bir şekilde çalıştığının göstergesidir.

Buraya kadar geldiğimize göre API imizin handler, service ve repository katmanlarını yazmaya başlayabiliriz.

İlk olarak internal adında bir klasör oluşturalım. Daha sonra internal klasörünün içerisine handler, service ve repository adında 3 adet daha klasör oluşturalım.

mkdir internal
cd internal
mkdir handler
mkdir service
mkdir repository

Bu komutları çalıştırdıktan sonraki proje dosyalarının aşağıdaki gibi görünmesi gerekiyor.

Projemizin son dosya yapısı

Evet implementasyona ilk olarak handler tarafından başlayacağız. Handler a kullanıcının gönderdiği isteklerin ilk geldiği katman diyebiliriz. İlk olarak handler klasörünün içerisine handler_test.go adında bir dosya oluşturalım.

cd handler
touch handler_test.go

Sanki endpointimiz varmış gibi bir request model oluşturuyoruz ben bunu internal klasörünün altında model olarak oluşturdum ve içerisine request modellerimi tanımlamak için request.go adında bir dosya oluşturdum yani son dosya görüntüm aşağıdaki gibi oldu.

proje dosyalarının son hali

request.go dosyamızın içerisine CreateStudentRequest modelimizi aşağıdaki gibi tanımladık.

package model

type CreateStudentRequest struct {
Name string `json:"name"`
Surname string `json:"surname"`
Age int `json:"age"`
Gender string `json:"gender"`
}

handler_test.go dosyamızı oluşturduğumuza göre ilk olarak öğrenci ekleme endpointini yazmak için testimizi yazmaya başlayalım. İlk olarak Test_CreateStudent adında bir fonksiyon oluşturalım. Ve içerisine mock controllerimizi tanımlayalım.

package handler

import (
"testing"

"github.com/anilaydinn/tdd-example/internal/model"
"github.com/golang/mock/gomock"
)

func Test_CreateStudent(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

requestBody := model.CreateStudentRequest{
Name: "Anil",
Surname: "Aydin",
Age: 25,
Gender: "Male",
}
}

handler_test.go dosyamız yukarıdaki haldeyken. Handlerimizin bağlı olduğu diğer yapıları mocklamamız gerekiyor. Bunu mockgen kullanarak yapıyoruz ancak ilk olarak bu yapılara interface tanımlamamız gerekiyor. Handler service fonksiyonlarını kullanacağı için benim service fonksiyonlarını mocklamam gerekiyor. Bunun için ilk olarak handler_test.go dosyamın yanına handler.go adında bir dosya oluşturuyorum.

touch handler.go

handler.go dosyamı oluşturduktan sonra ilk olarak StudentActions interface’ini tanımlıyorum.

package handler

import "github.com/anilaydinn/tdd-example/internal/model"

type StudentActions interface {
CreateStudent(request model.CreateStudentRequest) (model.CreateStudentResponse, error)
}

Yukarıda StudentActions interfaceimi ve ilk yazmak istediğim fonksiyonu interface implemente ettim. Şimdi mockgen kullanarak bu fonksiyonun mock halini yaratacağız.

mockgen -source internal/handler/handler.go -package mock -destination internal/mock/service_mock.go

Yukarıdaki komutu çalıştırdıktan sonra dosyalarınızın son hali aşağıdaki gibi olmalı.

mock klasörü içerisindeki service_mock.go generate edilikten sonra dosyaların son hali

Evet service fonksiyonumuzu mockladığımıza göre. Handler testimizi yazmaya devam edebiliriz. Test edeceğimiz yapı Handler olacağı için ilk olarak onu elde etmemizi sağlayacak onu yaratacak bir fonksiyon tanımlamamız lazım. İlgili fonksiyonu ekledikten sonra handler.go dosyamın son hali aşağıdaki gibi olur.

package handler

import "github.com/anilaydinn/tdd-example/internal/model"

type StudentActions interface {
CreateStudent(request model.CreateStudentRequest) (model.CreateStudentResponse, error)
}

type Handler struct {
studentActions StudentActions
}

func NewHandler(studentActions StudentActions) *Handler {
return &Handler{
studentActions: studentActions,
}
}

func (h *Handler) RegisterRoutes(app *fiber.App) {
app.Post("/students", h.CreateStudent)
}

Yukarıdada görüldüğü gibi StudentActions interface’ini impletemente ederek service yapısı ile aradaki bağımlılığı yok etmiş oluyoruz. Bunu yaptığımızda handler’i ayrı bir yapı yani unit olarak test etmiş olacağız. RegisterRoutes fonksiyonumuz ile fiber uygulamamıza endpointlerimizi ekliyoruz. Sizde bu kodu yazdığınızda hata almanız normal çünkü henüz handler yapımızın CreateStudent adında bir fonksiyonu yok.

Tekrar handler_test.go dosyamıza dönelim ve testimizi yazmaya devam edelim.

package handler

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/anilaydinn/tdd-example/internal/mock"
"github.com/anilaydinn/tdd-example/internal/model"
"github.com/gofiber/fiber/v2"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)

func Test_CreateStudent(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

requestBody := model.CreateStudentRequest{
Name: "Anil",
Surname: "Aydin",
Age: 25,
Gender: "Male",
}

t.Run("it should return 201 when student created successfully", func(t *testing.T) {
mockStudentActions := mock.NewMockStudentActions(ctrl)

mockStudentActions.EXPECT().CreateStudent(requestBody).Return(model.CreateStudentResponse{
ID: 1,
Name: requestBody.Name,
Surname: requestBody.Surname,
Age: requestBody.Age,
Gender: requestBody.Gender,
}, nil)

body, err := json.Marshal(requestBody)
assert.Nil(t, err)

req := httptest.NewRequest(http.MethodPost, "/students", bytes.NewReader(body))
req.Header.Add("Content-Type", "application/json")

app := fiber.New()

handler := NewHandler(mockStudentActions)
handler.RegisterRoutes(app)

res, err := app.Test(req, 30000)
assert.Nil(t, err)

assert.Equal(t, http.StatusCreated, res.StatusCode)
})
}

Evet handler_test.go dosyamızın son hali yukarıdaki gibi oldu. Burada requestimizi, handlerimizi ve fiber appini yaratıp. Fiber app üzerinde test isteği gönderiyoruz. Ve bu isteğin handler tarafında yukarıdaki ilk iki satırdaki method callarını yapması gerektiğini söylüyoruz. Yani istek eğer başarılı ise bana buradan CreateStudentResponse ve error nil gelmeli diyoruz. Yukarıdaki testi koştuğumuzda bize hata vericek çünkü henüz handlerimiz CreateStudent adlı bir fonksiyona sahip değil. İlk olarak o fonksiyonu oluşturalım.

package handler

import (
"github.com/anilaydinn/tdd-example/internal/model"
"github.com/gofiber/fiber/v2"
)

type StudentActions interface {
CreateStudent(request model.CreateStudentRequest) (model.CreateStudentResponse, error)
}

type Handler struct {
studentActions StudentActions
}

func NewHandler(studentActions StudentActions) *Handler {
return &Handler{
studentActions: studentActions,
}
}

func (h *Handler) RegisterRoutes(app *fiber.App) {
app.Post("/students", h.CreateStudent)
}

func (h *Handler) CreateStudent(c *fiber.Ctx) error {
return nil
}

Yukarıda CreateStudent fonksiyonunu oluşturdum ve return nil diyerek nil döndüm. Fiber paketinde nil dönen handler fonksiyonları her zaman status 200 dönüyor ben handler_test.go içerisinde bu fonksiyonun 201 yani Created dönmesini bekliyorum dolayısıyla testi tekrardan çalıştırdığımda hata almalıyım.

go test ./...

Yukarıdaki komut ile bütün testlerimizi koşabiliriz. Yukarıdaki komutu çalıştıralım ve aşağıdaki hatayı görelim.

--- FAIL: Test_CreateStudent (0.00s)
--- FAIL: Test_CreateStudent/it_should_return_201_when_student_created_successfully (0.00s)
handler_test.go:53:
Error Trace: /home/thracian/personal/tdd-example/internal/handler/handler_test.go:53
Error: Not equal:
expected: 201
actual : 200
Test: Test_CreateStudent/it_should_return_201_when_student_created_successfully
controller.go:269: missing call(s) to *mock.MockStudentActions.CreateStudent(is equal to {Anil Aydin 25 Male} (model.CreateStudentRequest)) /home/thracian/personal/tdd-example/internal/handler/handler_test.go:31
controller.go:269: aborting test due to missing call(s)

Yukarıdaki hatada da görüldüğü gibi expected yani beklenen 201 status code olduğunu bize söylüyor. Artık handler fonksiyonumuzun yapması gereken fonksiyonaliteyi yazıp testimizi geçmeye çalışalım.

func (h *Handler) CreateStudent(c *fiber.Ctx) error {
var requestBody model.CreateStudentRequest
if err := c.BodyParser(&requestBody); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}

response, err := h.studentActions.CreateStudent(requestBody)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}

return c.Status(fiber.StatusCreated).JSON(response)
}

handler.go dosyamız içerisindeki CreateStudent fonksiyonumuzun son hali yukarıdadır. İlk olarak kullanıcıdan aldığımız request bodymizi parse ediyoruz ve burada bir problem çıkarsa kullanıcıya 400 yani Bad Request dönüyoruz. Eğer ki bir problem çıkmaz ise kodumuz aşağıdaki bizim mockladığımız service fonksiyonunu çağırıyor. Eğer burada bir problem çıkarsada kullanıcıya 500 yani Internal Server Error dönüyoruz. Eğer problem çıkmaz ise kullanıcıya kaydetmiş olduğu kullanıcıyı 201 yani Created status code’u ile dönüyoruz.

Şimdi tekrar testimizi çalıştıralım ve sonucunu görelim.

go test ./...
?       github.com/anilaydinn/tdd-example       [no test files]
? github.com/anilaydinn/tdd-example/internal/mock [no test files]
? github.com/anilaydinn/tdd-example/internal/model [no test files]
? github.com/anilaydinn/tdd-example/internal/service [no test files]
ok github.com/anilaydinn/tdd-example/internal/handler (cached)

Evet yukarıda gördüğünüz gibi handler paketi içerisindeki testlerimiz geçmiş ve bize “ok” mesajı vermiş. Gelin birde hangi satırları test ettiğimizi görelim.

CreateStudent handler fonksiyonu testinin geçtiği satırlar.

Yukarıda görüldüğü gibi başarılı durumun geçtiği satırlar sadece yeşile boyanmış. Gelin şimdi diğer durumlarada testlerimizi yazalım.

İlk olarak 400 dönen senaryodaki test caseimizi handler_test.go dosyamıza ekleyelim.

t.Run("it should return 400 when request body is invalid", func(t *testing.T) {
mockStudentActions := mock.NewMockStudentActions(ctrl)

body, err := json.Marshal("invalid")
assert.Nil(t, err)

req := httptest.NewRequest(http.MethodPost, "/students", bytes.NewReader(body))
req.Header.Add("Content-Type", "application/json")

app := fiber.New()

handler := NewHandler(mockStudentActions)
handler.RegisterRoutes(app)

res, err := app.Test(req, 30000)
assert.Nil(t, err)

assert.Equal(t, http.StatusBadRequest, res.StatusCode)
})

Yukarıdaki test gördüğünüz gibi requestin bodysinde API’in istemediği tipte bir değer almış yani requestimizin bodysi bozuk bu kötü bir istek. Dolayısıyla testimizde son satırda 400 yani Bad Request dönmesini bekledik.

Tekrar testimizi çalıştıralım.

Yukarıda gördüğünüz gibi testimiz artık Bad Request döndüğü senaryoyuda cover etti.

Şimdi bir sonraki senaryomuz için test ekleyelim.

t.Run("it should return 500 when student creation failed", func(t *testing.T) {
mockStudentActions := mock.NewMockStudentActions(ctrl)

mockStudentActions.EXPECT().CreateStudent(requestBody).Return(model.CreateStudentResponse{}, assert.AnError)

body, err := json.Marshal(requestBody)
assert.Nil(t, err)

req := httptest.NewRequest(http.MethodPost, "/students", bytes.NewReader(body))
req.Header.Add("Content-Type", "application/json")

app := fiber.New()

handler := NewHandler(mockStudentActions)
handler.RegisterRoutes(app)

res, err := app.Test(req, 30000)
assert.Nil(t, err)

assert.Equal(t, http.StatusInternalServerError, res.StatusCode)
})

Yukarıdaki test ise service yapısının bize error döndüğü senaryo için yazılmış bir test. Eğer ki service tarafından yani mockladığımız fonksiyon error döner ise. 500 yani Internal Server Error dönmesini bekledik.

Tekrar testimizi çalıştıralım.

Yukarıda gördüğünüz gibi handler tarafındaki CreateStudent fonksiyonumuzu %100 test etmiş olduk.

Handler katmanımızı test edilebilir bir şekilde yazdığımıza göre bir sonraki yazımızda Service katmanını yani bütün business logic’in yazıldığı katmanının testlerini yazarak implemente edeceğiz.

Umarım buraya kadar okumuşsunuzdur ve umarım istediklerimi aktarabilmişimdir. Elimden geldiğince açık ve basit bir dille yazdım. Mümkün olduğunca sadeleştirip güncellemeye çalışacağım. Okuduğunuz için teşekkür ederim bir sonraki yazımda görüşmek dileği ile hoşçakalın.

Github Repository URL: https://github.com/anilaydinn/tdd-example

LinkedIn: https://www.linkedin.com/in/anlaydin/

--

--