Pendahuluan

Go Fiber dengan GORM menawarkan fondasi kuat untuk membangun API RESTful. Namun, saat menulis unit test, kita membutuhkan pendekatan yang lebih deterministik untuk repository yang berinteraksi langsung dengan database. Artikel ini mengulas pola testing repository GORM di Go Fiber, bermula dari setup mock DB/in-memory SQLite, menyiapkan fixture data, hingga menguji handler dan memastikan query, relasi, serta transaksi bekerja sesuai harapan.

Mengapa Harus Mock DB atau SQLite In-Memory

Direct hit ke database nyata dalam unit test mengundang flakiness karena ketergantungan state eksternal. Dua pendekatan populer:

  • In-memory SQLite: Menjalankan GORM terhadap SQLite yang hanya hidup selama test. Sangat membantu untuk memastikan migrasi, relasi, dan constraint berjalan. Tidak membutuhkan mock tambahan, tapi lebih lambat dibanding mock.
  • Mock DB (SQLMock atau interface GORM): Mengizinkan verifikasi query tanpa eksekusi. Lebih cepat dan memberikan kontrol ketat atas error path. Namun, perlu lebih banyak boilerplate karena harus menyimulasikan behavior database.

Pilih SQLite in-memory jika Anda ingin memvalidasi query SQL atau constraint berlapis, pilih mock DB bila fokus pada logika repository dan ingin menjalankan banyak kombinasi kasus error dengan cepat.

Menyiapkan Repository dan Interface

Menyaring dependency untuk repository membuat testing lebih gampang. Definisikan interface repository agar handler dan service hanya tahu kontrak, bukan implementasi GORM langsung.

type ProductRepository interface {
    GetByID(ctx context.Context, id uint) (*models.Product, error)
    Create(ctx context.Context, product *models.Product) error
    Update(ctx context.Context, product *models.Product) error
}

Implementasi GORM menyimpan *gorm.DB. Pastikan method menerima context.Context dan melakukan WithContext untuk mendukung tracing/test timeout.

Mock Database dengan SQLMock

SQLMock cocok untuk mem-verifikasi SQL yang dihasilkan GORM. Berikut langkah umum:

  1. Pasang github.com/DATA-DOG/go-sqlmock.
  2. Buat *sql.DB mock lalu bungkus dengan GORM.
  3. Buat expectation query dan hasilnya.
  4. Panggil repository dan verifikasi ekspektasi terpenuhi.

Contoh unit test sederhana untuk method GetByID:

func TestProductRepository_GetByID(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatal(err)
    }
    gormDB, _ := gorm.Open(mysql.New(mysql.Config{Conn: db}), &gorm.Config{})
    repo := repository.NewProductRepository(gormDB)

    rows := sqlmock.NewRows([]string{"id","name"}).AddRow(1, "Kopi Luwak")
    mock.ExpectQuery("SELECT .* FROM `products` WHERE").WithArgs(1).WillReturnRows(rows)

    prod, err := repo.GetByID(context.Background(), 1)
    assert.NoError(t, err)
    assert.Equal(t, "Kopi Luwak", prod.Name)
    assert.NoError(t, mock.ExpectationsWereMet())
}

Tips:

  • Gunakan regex sederhana untuk expectation query agar tidak mudah gagal ketika GORM menambahkan alias.
  • Warnakan transaksional behavior dengan ExpectBegin, ExpectCommit, dan ExpectRollback.
  • Pastikan memanggil ExpectationsWereMet() di akhir untuk memastikan semua expectation terpenuhi.

Mock DB paling berguna untuk memverifikasi bahwa repository mengonversi error SQL menjadi error domain yang sesuai.

Testing dengan SQLite In-Memory

SQLite in-memory cocok untuk memastikan relasi, constraint, dan transaksi bekerja. Langkah utamanya:

  1. Gunakan sqlite.Open("file::memory:?cache=shared") agar GORM menggunakan database sementara.
  2. Jalankan migrasi untuk schema yang relevan.
  3. Set up fixture data untuk relasi (misalnya product dan category).
  4. Cleanup setelah test dengan db.Migrator().DropTable(...) jika perlu.

Contoh setup di test_main.go:

func newTestDB(t *testing.T) *gorm.DB {
    db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
    require.NoError(t, err)
    err = db.AutoMigrate(&models.Product{}, &models.Category{})
    require.NoError(t, err)
    return db
}

Fixture data bisa ditempatkan dalam helper:

func seedProductFixture(db *gorm.DB) *models.Product {
    cat := models.Category{Name: "Minuman"}
    prod := models.Product{Name: "Es Teh", Price: 8000, Category: &cat}
    db.Create(&prod)
    return &prod
}

Dengan setup ini, kita dapat menguji relasi eager loading:

func TestProductRepository_LoadCategory(t *testing.T) {
    db := newTestDB(t)
    repo := repository.NewProductRepository(db)
    prod := seedProductFixture(db)

    got, err := repo.GetByIDWithCategory(context.Background(), prod.ID)
    assert.NoError(t, err)
    assert.Equal(t, "Minuman", got.Category.Name)
}

Keuntungan SQLite in-memory adalah cocok untuk integrasi ringan. Trade-off: kecepatan lebih rendah dibanding mock dan lebih sulit mensimulasikan error spesifik kecuali menulis custom trigger.

Menulis Unit Test Handler di Go Fiber

Handler seharusnya bergantung pada interface repository. Ketika mengetes handler:

  1. Gunakan mock repository (misalnya dengan testify/mock) untuk mengontrol respons.
  2. Gunakan httptest.NewRequest dan fiber.New().Test untuk memanggil handler.
  3. Periksa status code, header, dan payload JSON sesuai ekspektasi.

Contoh handler test untuk endpoint buat produk:

func TestCreateProductHandler(t *testing.T) {
    app := fiber.New()
    mockRepo := new(mocks.ProductRepository)
    handler := handler.NewProductHandler(mockRepo)

    mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*models.Product")).Return(nil)

    body := `{"name":"Kopi","price":15000}`
    req := httptest.NewRequest(fiber.MethodPost, "/products", strings.NewReader(body))
    req.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON)

    app.Post("/products", handler.Create)
    resp, _ := app.Test(req)

    assert.Equal(t, fiber.StatusCreated, resp.StatusCode)
    mockRepo.AssertCalled(t, "Create", mock.Anything, mock.Anything)
}

Catat strategi assertions:

  • Query level: dengan SQLMock, pastikan SELECT/INSERT/UPDATE sesuai nama tabel dan argumen.
  • Relasi: gunakan SQLite in-memory untuk cek eager load, gunakan assertion JSON untuk memastikan nested object muncul.
  • Transaksi: mock Begin/Commit/Rollback dengan SQLMock untuk memverifikasi branch success/error.

Selain itu, jangan lupa menguji error path: misalnya, jika repository mengembalikan gorm.ErrRecordNotFound, handler harus menerjemahkan menjadi 404 dan tidak memanggil operasi tambahan.

Strategi Assertion dan Debugging

Beberapa tips praktis:

  • Gunakan fixture builder pattern untuk membuat objek domain konsisten antar test.
  • Log query dan errors pada setup test; GORM menyediakan logger dengan Logger.LogMode(logger.Info) berguna saat debugging test yang gagal.
  • Pastikan isolation: setiap test memulai DB fresh. Untuk SQLite, gunakan transaction rollback atau drop table agar tidak saling mempengaruhi.
  • Simulasikan race condition dengan menjalankan test paralel (gunakan t.Parallel()) tapi hati-hati jika sharing DB.

Debugging failure SQLMock: tidak terpenuhinya expectation biasanya disebabkan GORM menambahkan clause alias. Tinjau query aktual di log GORM dan perbarui regex expectation.

Kesimpulan

Testing repository GORM di Go Fiber efektif bila memadukan mock DB dan SQLite in-memory sesuai kebutuhan. Mock DB cocok untuk verifikasi query & error path, sementara SQLite in-memory memberikan confidence terhadap relasi dan migrasi schema. Handler testing tetap bergantung pada interface repository dan menegakkan assertion status respons. Dengan fixture yang konsisten, testing menjadi lebih maintainable. Selalu bandingkan trade-off antara kecepatan test dan cakupan kebenaran data.