Testing pada Go(lang)

Endrawan Andika Wicaksana
PPL A-4 YUK RECYCLE
3 min readMar 20, 2019

Halo, perkenalkan nama saya Endrawan Andika Wicaksana, bisa dipanggil Endrawan atau Endra. Pada mata kuliah Proyek Perangkat Lunak (PPL) Fasilkom UI, saya berada pada proyek Yuk — Recycle dan berperan sebagai Hacker.

Pada blog kali ini saya akan memberikan contoh testing pada Go pada sprint 2 di PPL Fasilkom UI.

Implementasi Go pada proyek kami yaitu sebagai backend yang akan berkomunikasi dengan mobile app.

Pada kelompok kami, terdapat beberapa folder utama yang digunakan seperti controllers, models, repositories, dan services. Salah satu contoh alur ketika terdapat request yaitu dimulai dari main.go (routing) menuju controller lalu service lalu repository lalu mendapatkan model.

Kelompok kami menggunakan Dependency Injection untuk mempermudah melakukan testing. Implementasi kami yaitu di main.go akan membuat satu object untuk sebuah class dimulai dari yang tidak memiliki dependency. Ketika suatu class memiliki dependency (misalkan service membutuhkan repository untuk query dan controller membutuhkan service untuk business logic), maka object dependency tersebut akan dimasukkan sebagai property untuk object tersebut.

Misalkan terdapat controller Settings:

type Settings struct {    SettingsService services.ISettingsService}

dan terdapat service SettingsService (yang mengimplement ISettingsService):

type SettingsService struct {}type ISettingsService interface {    GetSettingsData(mitraId uint) (*Schedules, []GarbageSetting, uint, error)}

Pada code di atas, Settings memiliki dependency ISettingsService (yang nanti akan dimasukkan object SettingsService). Implementasi Dependency Injectionnya yaitu:

settingsService := SettingsService{}mitraSettingsController := Settings{SettingsService: settingServices}

Karena sudah menggunakan design pattern di atas, maka ketika ingin melakukan testing pada Settings, kita cukup masukkan mock object sebagai property untuk Settings.

Untuk pembuatan mock object kami menggunakan tools mockery yang akan otomatis melakukan code generation dan akan dimasukkan ke folder mocks. Command untuk menggunakan mockery (dijalankan pada folder utama backend) yaitu:

mockery -all -keeptree

Setelah menyiapkan fungsi yang ingin ditest dan mock, maka kita dapat memulai membuat test. Contoh test yang dibuat yaitu:

func TestSettings_GetSettings(t *testing.T) {    tests := []struct {        description                     string        inputMitraID                    uint        returnSchedulesMock             *services.Schedules        returnGarbageSettingsMock       []services.GarbageSetting        returnMaximumPickUpDistanceMock uint        returnErrorMock                 error        expectedHTTPCode                int        expectedBody                    string    }{
{
description: "Get data success", inputMitraID: uint(1), returnSchedulesMock: &services.Schedules{}, returnGarbageSettingsMock: []services.GarbageSetting{}, returnMaximumPickUpDistanceMock: 10, returnErrorMock: nil, expectedHTTPCode: http.StatusOK,
},
} assert := assert.New(t) for _, tc := range tests { req, err := http.NewRequest("GET", "/api/v1/mitra/settings", strings.NewReader("")) req = req.WithContext(context.WithValue(req.Context(), "UserID", tc.inputMitraID)) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() mockedSettingsService := new(mockServices.ISettingsService) mockedSettingsService.On("GetSettingsData", tc.inputMitraID).Return( tc.returnSchedulesMock, tc.returnGarbageSettingsMock, tc.returnMaximumPickUpDistanceMock, tc.returnErrorMock, ) settingsController := Settings{SettingsService: mockedSettingsService} handler := http.HandlerFunc(settingsController.GetSettings) handler.ServeHTTP(rr, req) returnedBody := rr.Body.String() var result SettingsRequestOrResponse json.Unmarshal([]byte(returnedBody), &result) assert.Equal(tc.expectedHTTPCode, rr.Code, tc.description) assert.Equal(tc.returnSchedulesMock, result.Schedules, tc.description) assert.Equal(tc.returnGarbageSettingsMock, result.GarbageSettings, tc.description) assert.Equal(tc.returnMaximumPickUpDistanceMock, result.MaximumPickUpDistance, tc.description) }}

Variabel tests menjelaskan test case apa saja yang ingin dicoba (contohnya pada code di atas hanya satu test case yaitu get data success).

Bagian di bawah ini akan membuat object mock dan kita tetapkan return value apabila suatu fungsi pada object tersebut dipanggil.

mockedSettingsService := new(mockServices.ISettingsService)mockedSettingsService.On("GetSettingsData", tc.inputMitraID).Return(    tc.returnSchedulesMock,    tc.returnGarbageSettingsMock,    tc.returnMaximumPickUpDistanceMock,    tc.returnErrorMock,)

Kemudian object mock dimasukkan ke dalam instance object class yang ingin ditest. Setelah dijalan operasi perlu dilakukan pengecekan apakah nilainya sesuai dengan yang diinginkan yaitu dengan cara:

assert.Equal(tc.expectedHTTPCode, rr.Code, tc.description)

Selain kasus mock di atas, ada juga kasus di mana kita tidak dapat melakukan mock yaitu test repository. Test repository menggunakan stub yaitu dengan melakukan simulasi pada environment yang berbeda (bukan database yang asli).

Misalkan terdapat repository GarbageType:

type GarbageTypeRepository struct {}type IGarbageTypeRepository interface {    GetAll() ([]models.GarbageType, error)}func (g GarbageTypeRepository) GetAll() ([]models.GarbageType, error) {    var garbageTypes []models.GarbageType    err := models.GetDB().Find(&garbageTypes).Error    return garbageTypes, err}

Method GetAll di atas melakukan query untuk mendapatkan semua data. Kita dapat melakukan simulasi query database dengan test berikut:

func TestGarbageTypeRepository_GetAll(t *testing.T) {    var fieldNames = []string{"id", "title", "description", "quantity_type", "image_base64"}    rows := sqlmock.NewRows(fieldNames)    imageBase64 := "SGFsbwo="    rows.AddRow("1", "Sampah1", "Ini sampah 1", "Kg", imageBase64)    rows.AddRow("2", "Sampah2", "Ini sampah 2", "Kg", imageBase64)    db, mock, _ := sqlmock.New()    gormDb, _ := gorm.Open("postgres", db)    mock.ExpectQuery("^SELECT (.+) FROM \"garbage_types\".+$").WillReturnRows(rows)    models.SetDB(gormDb)    repository := new(GarbageTypeRepository)    garbageTypes, _ := repository.GetAll()    assert := assert.New(t)    assert.Equal(2, len(garbageTypes))    assert.Equal(uint(1), garbageTypes[0].ID)    assert.Equal("Sampah1", garbageTypes[0].Title)    assert.Equal(uint(2), garbageTypes[1].ID)    assert.Equal("Sampah2", garbageTypes[1].Title)}

Code berikut melakukan pengecekan dengan regex untuk pemanggilan query database yang akan dilakukan. Apabila regexnya benar maka akan dibuat untuk mengembalikan data pada variabel rows.

mock.ExpectQuery("^SELECT (.+) FROM \"garbage_types\".+$").WillReturnRows(rows)

Setelah dijalan operasi perlu dilakukan pengecekan apakah nilainya sesuai dengan yang diinginkan yaitu dengan cara:

assert.Equal(2, len(garbageTypes))

Kita sudah mencoba testing menggunakan mock dan stub. Mock digunakan ketika terdapat suatu class yang memiliki dependent pada class lain yang membutuhkan hasilnya. Stub digunakan ketika ingin melakukan test pada behaviour suatu operasi misal operasi query database.

Sekian blog dari saya. Terima kasih sudah membaca.

--

--