Pada modul ini kita akan membuat fungsi test untuk semua fungsi yang melakukan akses ke database (fungsi yang terdapat dalam file pkg/repository/dbrepo/users_postgres.go).
Melanjutkan dari modul sebelumnya, buka file pkg/repository/dbrepo/users_postgres_test.go, lalu ubah seperti kod dibawah.
package dbrepo
import (
"database/sql"
"fmt"
"log"
"os"
"testing"
"time"
"webapp/pkg/data"
"webapp/pkg/repository"
_ "github.com/jackc/pgconn"
_ "github.com/jackc/pgx/v4"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
var (
host = "localhost"
user = "postgres"
password = "postgres"
dbName = "users_test"
port = "5435"
dsn = "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable timezone=UTC connect_timeout=5"
)
var resource *dockertest.Resource
var pool *dockertest.Pool
var testDB *sql.DB
var testRepo repository.DatabaseRepo
func TestMain(m *testing.M) {
// connect to docker; fail if docker not running
p, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("could not connect to docker; is it running? %s", err)
}
pool = p
// set up our docker options, specifying the image and so forth
opts := dockertest.RunOptions{
Repository: "postgres",
Tag: "14.5",
Env: []string{
"POSTGRES_USER=" + user,
"POSTGRES_PASSWORD=" + password,
"POSTGRES_DB=" + dbName,
},
ExposedPorts: []string{"5432"},
PortBindings: map[docker.Port][]docker.PortBinding{
"5432": {
{HostIP: "0.0.0.0", HostPort: port},
},
},
}
// get a resource (docker image)
resource, err = pool.RunWithOptions(&opts)
if err != nil {
_ = pool.Purge(resource)
log.Fatalf("could not start resource: %s", err)
}
// start the image and wait until it's ready
if err := pool.Retry(func() error {
var err error
testDB, err = sql.Open("pgx", fmt.Sprintf(dsn, host, port, user, password, dbName))
if err != nil {
log.Println("Error:", err)
return err
}
return testDB.Ping()
}); err != nil {
_ = pool.Purge(resource)
log.Fatalf("could not connect to database: %s", err)
}
// populate the database with empty tables
err = createTables()
if err != nil {
log.Fatalf("error creating tables: %s", err)
}
testRepo = &PostgresDBRepo{DB: testDB}
// run tests
code := m.Run()
// clean up
if err := pool.Purge(resource); err != nil {
log.Fatalf("could not purge resource: %s", err)
}
os.Exit(code)
}
func createTables() error {
tableSQL, err := os.ReadFile("./testdata/users.sql")
if err != nil {
fmt.Println(err)
return err
}
_, err = testDB.Exec(string(tableSQL))
if err != nil {
fmt.Println(err)
return err
}
return nil
}
func Test_pingDB(t *testing.T) {
err := testDB.Ping()
if err != nil {
t.Error("can't ping database")
}
}
func TestPostgresDBRepoInsertUser(t *testing.T) {
testUser := data.User{
FirstName: "Admin",
LastName: "User",
Email: "admin@example.com",
Password: "secret",
IsAdmin: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
id, err := testRepo.InsertUser(testUser)
if err != nil {
t.Errorf("insert user returned an error: %s", err)
}
if id != 1 {
t.Errorf("insert user returned wrong id; expected 1, but got %d", id)
}
}
func TestPostgresDBRepoAllUsers(t *testing.T) {
users, err := testRepo.AllUsers()
if err != nil {
t.Errorf("all users reports an error: %s", err)
}
if len(users) != 1 {
t.Errorf("all users reports wrong size; expected 1, but got %d", len(users))
}
testUser := data.User{
FirstName: "Jack",
LastName: "Smith",
Email: "jack@smith.com",
Password: "secret",
IsAdmin: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
_, _ = testRepo.InsertUser(testUser)
users, err = testRepo.AllUsers()
if err != nil {
t.Errorf("all users reports an error: %s", err)
}
if len(users) != 2 {
t.Errorf("all users reports wrong size after insert; expected 2, but got %d", len(users))
}
}
func TestPostgresDBRepoGetUser(t *testing.T) {
user, err := testRepo.GetUser(1)
if err != nil {
t.Errorf("error getting user by id: %s", err)
}
if user.Email != "admin@example.com" {
t.Errorf("wrong email returned by GetUser; expected admin@example.com but got %s", user.Email)
}
_, err = testRepo.GetUser(3)
if err == nil {
t.Error("no error reported when getting non existent user by id")
}
}
func TestPostgresDBRepoGetUserByEmail(t *testing.T) {
user, err := testRepo.GetUserByEmail("jack@smith.com")
if err != nil {
t.Errorf("error getting user by email: %s", err)
}
if user.ID != 2 {
t.Errorf("wrong id returned by GetUserByEmail; expected 2 but got %d", user.ID)
}
}
func TestPostgresDBRepoUpdateUser(t *testing.T) {
user, _ := testRepo.GetUser(2)
user.FirstName = "Jane"
user.Email = "jane@smith.com"
err := testRepo.UpdateUser(*user)
if err != nil {
t.Errorf("error updating user %d: %s", 2, err)
}
user, _ = testRepo.GetUser(2)
if user.FirstName != "Jane" || user.Email != "jane@smith.com" {
t.Errorf("expected updated record to have first name Jane and email jane@smith.com, but got %s %s", user.FirstName, user.Email)
}
}
func TestPostgresDBRepoDeleteUser(t *testing.T) {
err := testRepo.DeleteUser(2)
if err != nil {
t.Errorf("error deleting user id 2: %s", err)
}
_, err = testRepo.GetUser(2)
if err == nil {
t.Error("retrieved user id 2, who should have been deleted")
}
}
func TestPostgresDBRepoResetPassword(t *testing.T) {
err := testRepo.ResetPassword(1, "password")
if err != nil {
t.Error("error resetting user's password", err)
}
user, _ := testRepo.GetUser(1)
matches, err := user.PasswordMatches("password")
if err != nil {
t.Error(err)
}
if !matches {
t.Errorf("password should match 'password', but does not")
}
}
func TestPostgresDBRepoInsertUserImage(t *testing.T) {
var image data.UserImage
image.UserID = 1
image.FileName = "test.jpg"
image.CreatedAt = time.Now()
image.UpdatedAt = time.Now()
newID, err := testRepo.InsertUserImage(image)
if err != nil {
t.Error("inserting user image failed:", err)
}
if newID != 1 {
t.Error("got wrong id for image; should be 1, but got", newID)
}
image.UserID = 100
_, err = testRepo.InsertUserImage(image)
if err == nil {
t.Error("inserted a user image with non-existent user id")
}
}
Perhatian, ada kesalahan dalam memberikan tipe variable, jadi buka file pkg/data/user_images.go, kemudian ubah field UserID menjadi int.
Kesalahan kedua adalah pada file pkg/repository/dbrepo/users_postgres.go, pada fungsi insertUserImage, terdapat typo nama field, seharusnya file_name (sebelumnya filename).
func (m *PostgresDBRepo) InsertUserImage(i data.UserImage) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
var newID int
stmt := `insert into user_images (user_id, file_Name, created_at, updated_at)
values ($1, $2, $3, $4) returning id`
err := m.DB.QueryRowContext(ctx, stmt,
i.UserID,
i.FileName,
time.Now(),
time.Now(),
).Scan(&newID)
if err != nil {
return 0, err
}
return newID, nil
}
Sampai disini test sudah selesai dibuat. Jika kita jalankan dari direktory pkg/repository/dbrepo/
$ go test -v .
=== RUN Test_pingDB
--- PASS: Test_pingDB (0.00s)
=== RUN TestPostgresDBRepoInsertUser
--- PASS: TestPostgresDBRepoInsertUser (0.23s)
=== RUN TestPostgresDBRepoAllUsers
--- PASS: TestPostgresDBRepoAllUsers (0.24s)
=== RUN TestPostgresDBRepoGetUser
--- PASS: TestPostgresDBRepoGetUser (0.00s)
=== RUN TestPostgresDBRepoGetUserByEmail
--- PASS: TestPostgresDBRepoGetUserByEmail (0.00s)
=== RUN TestPostgresDBRepoUpdateUser
--- PASS: TestPostgresDBRepoUpdateUser (0.01s)
=== RUN TestPostgresDBRepoDeleteUser
--- PASS: TestPostgresDBRepoDeleteUser (0.01s)
=== RUN TestPostgresDBRepoResetPassword
--- PASS: TestPostgresDBRepoResetPassword (0.47s)
=== RUN TestPostgresDBRepoInsertUserImage
--- PASS: TestPostgresDBRepoInsertUserImage (0.02s)
PASS
ok webapp/pkg/repository/dbrepo 7.400s
semua fungsi database sudah berhasil ditest dengan menggunakan temporary docker container.
Pada modul berikutnya kita akan membahas bagaimana memisahkan unit test dengan integration test.