M
Mr Sugiarto
Developer
30 Aug 2026 6 min read

Sampai part sebelumnya, kita selalu testing endpoint secara manual pakai curl atau Postman. Cara ini cukup untuk development sehari-hari, tapi tidak scalable — begitu API bertambah banyak, testing manual jadi lama dan gampang kelewat satu-dua skenario. Di part ini kita akan menulis automated feature test menggunakan tooling testing bawaan Goravel.

Struktur Testing di Goravel

Kalau kita lihat folder tests/, Goravel sudah menyiapkan scaffold-nya sejak awal:

tests/
├── test_case.go
└── feature/
    └── example_test.go

test_case.go berisi base struct yang meng-boot aplikasi Goravel (bootstrap.Boot()) supaya semua facade (facades.Orm(), facades.Route(), dst) bisa dipakai di dalam test:

package tests

import (
	"github.com/goravel/framework/testing"

	"myblog/bootstrap"
)

func init() {
	bootstrap.Boot()
}

type TestCase struct {
	testing.TestCase
}

testing.TestCase inilah yang menyediakan method Http(t) — sebuah HTTP test client yang bisa langsung memanggil route API kita tanpa perlu server sungguhan berjalan.

Menulis Feature Test untuk Auth

Buat file tests/feature/auth_test.go:

package feature

import (
	"bytes"
	"encoding/json"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/suite"

	"myblog/tests"
)

type AuthTestSuite struct {
	suite.Suite
	tests.TestCase
}

func TestAuthTestSuite(t *testing.T) {
	suite.Run(t, new(AuthTestSuite))
}

func (s *AuthTestSuite) TestRegisterAndLogin() {
	email := fmt.Sprintf("feature-test-%d@example.com", time.Now().UnixNano())

	registerBody, _ := json.Marshal(map[string]string{
		"name":     "Feature Test",
		"email":    email,
		"password": "secret123",
	})

	registerResponse, err := s.Http(s.T()).Post("/register", bytes.NewReader(registerBody))
	s.Require().NoError(err)
	registerResponse.AssertStatus(200)

	data, err := registerResponse.Json()
	s.Require().NoError(err)
	payload := data["data"].(map[string]any)
	s.NotEmpty(payload["token"])
	s.NotEmpty(payload["refresh_token"])

	loginBody, _ := json.Marshal(map[string]string{
		"email":    email,
		"password": "secret123",
	})

	loginResponse, err := s.Http(s.T()).Post("/login", bytes.NewReader(loginBody))
	s.Require().NoError(err)
	loginResponse.AssertStatus(200)
}

func (s *AuthTestSuite) TestLoginWithWrongPasswordFails() {
	email := fmt.Sprintf("feature-wrongpass-%d@example.com", time.Now().UnixNano())

	registerBody, _ := json.Marshal(map[string]string{
		"name":     "Wrong Pass Test",
		"email":    email,
		"password": "secret123",
	})
	_, err := s.Http(s.T()).Post("/register", bytes.NewReader(registerBody))
	s.Require().NoError(err)

	loginBody, _ := json.Marshal(map[string]string{
		"email":    email,
		"password": "wrong-password",
	})

	loginResponse, err := s.Http(s.T()).Post("/login", bytes.NewReader(loginBody))
	s.Require().NoError(err)
	loginResponse.AssertStatus(401)
}

func (s *AuthTestSuite) TestRegisterDuplicateEmailFails() {
	email := fmt.Sprintf("feature-dup-%d@example.com", time.Now().UnixNano())

	body, _ := json.Marshal(map[string]string{
		"name":     "Dup Test",
		"email":    email,
		"password": "secret123",
	})

	_, err := s.Http(s.T()).Post("/register", bytes.NewReader(body))
	s.Require().NoError(err)

	response, err := s.Http(s.T()).Post("/register", bytes.NewReader(body))
	s.Require().NoError(err)
	response.AssertStatus(409)
}

Beberapa catatan penting:

  • Email dibuat unik pakai time.Now().UnixNano() supaya tiap kali test dijalankan tidak bentrok dengan data lama (mengingat kita tidak mereset database di antara test run).
  • s.Http(s.T()).Post(...) otomatis mengirim Content-Type: application/json.
  • AssertStatus(...) adalah salah satu dari banyak method assertion siap pakai — ada juga AssertOk(), AssertUnauthorized(), AssertJson(...), dan sebagainya.

Menulis Feature Test untuk Post

Buat file tests/feature/post_test.go untuk menguji endpoint yang butuh autentikasi:

package feature

import (
	"bytes"
	"encoding/json"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/suite"

	"myblog/tests"
)

type PostTestSuite struct {
	suite.Suite
	tests.TestCase
}

func TestPostTestSuite(t *testing.T) {
	suite.Run(t, new(PostTestSuite))
}

func (s *PostTestSuite) registerAndGetToken() string {
	email := fmt.Sprintf("post-feature-%d@example.com", time.Now().UnixNano())

	body, _ := json.Marshal(map[string]string{
		"name":     "Post Feature Test",
		"email":    email,
		"password": "secret123",
	})

	response, err := s.Http(s.T()).Post("/register", bytes.NewReader(body))
	s.Require().NoError(err)

	data, err := response.Json()
	s.Require().NoError(err)

	return data["data"].(map[string]any)["token"].(string)
}

func (s *PostTestSuite) TestCreatePostWithoutTokenFails() {
	body, _ := json.Marshal(map[string]string{
		"title": "Unauthorized Post",
		"body":  "Should not be created",
	})

	response, err := s.Http(s.T()).Post("/posts", bytes.NewReader(body))
	s.Require().NoError(err)
	response.AssertStatus(401)
}

func (s *PostTestSuite) TestCreatePostWithTokenSucceeds() {
	token := s.registerAndGetToken()

	body, _ := json.Marshal(map[string]string{
		"title": "Post dari Feature Test",
		"body":  "Isi post dari automated test",
	})

	response, err := s.Http(s.T()).WithToken(token).Post("/posts", bytes.NewReader(body))
	s.Require().NoError(err)
	response.AssertStatus(201)

	data, err := response.Json()
	s.Require().NoError(err)
	post := data["data"].(map[string]any)
	s.Equal("Post dari Feature Test", post["title"])
	s.Equal("DRAFT", post["status"])
}

func (s *PostTestSuite) TestCreatePostWithoutTitleFailsValidation() {
	token := s.registerAndGetToken()

	body, _ := json.Marshal(map[string]string{
		"body": "Post tanpa judul",
	})

	response, err := s.Http(s.T()).WithToken(token).Post("/posts", bytes.NewReader(body))
	s.Require().NoError(err)
	response.AssertStatus(422)
}

func (s *PostTestSuite) TestListPostsIsPublic() {
	response, err := s.Http(s.T()).Get("/posts")
	s.Require().NoError(err)
	response.AssertStatus(200)

	data, err := response.Json()
	s.Require().NoError(err)
	responseData := data["data"].(map[string]any)
	s.Contains(responseData, "posts")
	s.Contains(responseData, "pagination")
}

WithToken(token) menambahkan header Authorization: Bearer {token} secara otomatis — cocok untuk menguji endpoint yang dibungkus JwtMiddleware.

Menjalankan Test

go test ./tests/... -v

Kalau semua endpoint bekerja sesuai harapan, seluruh test case akan PASS:

--- PASS: TestAuthTestSuite (1.21s)
    --- PASS: TestAuthTestSuite/TestLoginWithWrongPasswordFails (0.49s)
    --- PASS: TestAuthTestSuite/TestRegisterAndLogin (0.48s)
    --- PASS: TestAuthTestSuite/TestRegisterDuplicateEmailFails (0.24s)
--- PASS: TestPostTestSuite (0.48s)
    --- PASS: TestPostTestSuite/TestCreatePostWithTokenSucceeds (0.24s)
    --- PASS: TestPostTestSuite/TestCreatePostWithoutTitleFailsValidation (0.24s)
    --- PASS: TestPostTestSuite/TestCreatePostWithoutTokenFails (0.00s)
    --- PASS: TestPostTestSuite/TestListPostsIsPublic (0.00s)

Penutup

Test yang kita tulis di sini masih level feature test (menguji endpoint API secara end-to-end lewat HTTP), belum unit test murni untuk masing-masing fungsi helper. Untuk aplikasi yang terus berkembang, akan sangat membantu kalau setiap endpoint penting (terutama yang berkaitan dengan autentikasi, validasi, dan proteksi kepemilikan seperti yang sudah kita buat di part-part sebelumnya) punya test sendiri — supaya perubahan kode di kemudian hari tidak diam-diam merusak fitur yang sudah ada. Di part berikutnya kita akan membahas caching untuk mempercepat response endpoint yang sering diakses.

M
Mr Sugiarto

Developer

Bagian dari Series: Seri Tutorial Belajar Framework Goravel Rest API untuk Pemula

Overview Seri ini membahas cara membangun REST API menggunakan Goravel, framework web berbasis Golang yang terinspirasi dari Laravel. Materi mencakup...

Lihat Series Lengkap