Golang Todolist CLI #2 โ€“ Membuat Model dan Repository Task

Ajitama DevAjitama Dev
3 min read

Selamat datang kembali di seri Golang Todolist CLI bersama saya, Ajitama! ๐ŸŽ‰
Pada bagian sebelumnya, kita telah berhasil menginisialisasi proyek. Kali ini, kita akan mulai membuat struktur data dan lapisan penyimpanan data: model Task dan repository-nya.


๐Ÿ“ Struktur Folder

Sebelum kita mulai, mari kita perbaiki struktur proyek agar lebih idiomatik sesuai best practice Golang:

golang-todolist-cli/
โ”œโ”€โ”€ go.mod
โ”œโ”€โ”€ internal/
โ”‚   โ”œโ”€โ”€ model/
โ”‚   โ”‚   โ””โ”€โ”€ task.go
โ”‚   โ””โ”€โ”€ repository/
โ”‚       โ”œโ”€โ”€ task_repository.go
โ”‚       โ””โ”€โ”€ task_repository_test.go

Kita akan menyimpan kode domain aplikasi (seperti model dan repository) di dalam folder internal/, karena ini merupakan praktik umum untuk membatasi akses antar package.


1๏ธโƒฃ Membuat Model Task

Pertama, kita buat struktur data Task di file internal/model/task.go:

package model

import "time"

type Task struct {
    Id          int
    Description string
    CreatedAt   time.Time
    CompletedAt *time.Time
}

Struktur Task ini menyimpan informasi deskripsi tugas, waktu dibuat, dan waktu selesai (jika sudah diselesaikan).


2๏ธโƒฃ Membuat Task Repository

Selanjutnya, kita buat repository untuk menyimpan dan mengelola data Task. Di sini, kita gunakan penyimpanan in-memory agar mudah dan ringan untuk tahap awal.

File: internal/repository/task_repository.go

package repository

import (
    "fmt"
    "time"

    "github.com/fardannozami/golang-todolist-cli/internal/model"
)

type TaskRepository interface {
    AddTask(task model.Task) error
    GetAllTasks() ([]model.Task, error)
    DeleteTaskById(id int) error
    MarkTaskAsCompleted(id int) error
}

type InMemoryTaskRepository struct {
    tasks []model.Task
}

func NewInMemoryTaskRepository() *InMemoryTaskRepository {
    return &InMemoryTaskRepository{
        tasks: make([]model.Task, 0),
    }
}

func (r *InMemoryTaskRepository) AddTask(task model.Task) error {
    r.tasks = append(r.tasks, task)
    return nil
}

func (r *InMemoryTaskRepository) GetAllTasks() ([]model.Task, error) {
    taskCopy := make([]model.Task, len(r.tasks))
    copy(taskCopy, r.tasks)
    return taskCopy, nil
}

func (r *InMemoryTaskRepository) DeleteTaskById(id int) error {
    for i, task := range r.tasks {
        if task.Id == id {
            r.tasks = append(r.tasks[:i], r.tasks[i+1:]...)
            return nil
        }
    }
    return fmt.Errorf("task with id %d not found", id)
}

func (r *InMemoryTaskRepository) MarkTaskAsCompleted(id int) error {
    for i, task := range r.tasks {
        if task.Id == id {
            completedAt := time.Now()
            r.tasks[i].CompletedAt = &completedAt
            return nil
        }
    }
    return fmt.Errorf("task with id %d not found", id)
}

3๏ธโƒฃ Menambahkan Unit Test untuk Repository

File: internal/repository/task_repository_test.go

Untuk memastikan repository kita bekerja dengan benar, kita buat serangkaian unit test menggunakan library testify/assert.

package repository

import (
    "testing"
    "time"

    "github.com/fardannozami/golang-todolist-cli/internal/model"
    "github.com/stretchr/testify/assert"
)

var tasks = []model.Task{
    {
        Id:          1,
        Description: "Learn Golang",
        CreatedAt:   time.Now(),
    },
    {
        Id:          2,
        Description: "Write Unit Tests",
        CreatedAt:   time.Now(),
    },
}

func TestNewInMemoryTaskRepository(t *testing.T) {
    repo := NewInMemoryTaskRepository()
    assert.NotNil(t, repo)
    assert.Empty(t, repo.tasks)
}

func TestInMemoryTaskRepository_AddTask(t *testing.T) {
    repo := NewInMemoryTaskRepository()
    for _, task := range tasks {
        err := repo.AddTask(task)
        assert.NoError(t, err)
    }
    assert.Len(t, repo.tasks, len(tasks))
    assert.Equal(t, tasks, repo.tasks)
}

func TestInMemoryTaskRepository_GetAll(t *testing.T) {
    repo := NewInMemoryTaskRepository()
    for _, task := range tasks {
        _ = repo.AddTask(task)
    }
    result, err := repo.GetAllTasks()
    assert.NoError(t, err)
    assert.Len(t, result, len(tasks))
    assert.Equal(t, tasks, result)

    // Pastikan hasil GetAll merupakan salinan (bukan referensi langsung)
    result[0].Description = "Modified"
    assert.NotEqual(t, result[0].Description, repo.tasks[0].Description)
}

func TestInMemoryTaskRepository_DeleteTaskById(t *testing.T) {
    repo := NewInMemoryTaskRepository()
    for _, task := range tasks {
        _ = repo.AddTask(task)
    }

    err := repo.DeleteTaskById(1)
    assert.NoError(t, err)
    assert.Len(t, repo.tasks, 1)
    assert.Equal(t, 2, repo.tasks[0].Id)

    err = repo.DeleteTaskById(999)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "task with id 999 not found")
}

func TestInMemoryTaskRepository_MarkTaskAsCompleted(t *testing.T) {
    repo := NewInMemoryTaskRepository()
    for _, task := range tasks {
        _ = repo.AddTask(task)
    }

    err := repo.MarkTaskAsCompleted(1)
    assert.NoError(t, err)
    assert.NotNil(t, repo.tasks[0].CompletedAt)
    assert.Nil(t, repo.tasks[1].CompletedAt)

    err = repo.MarkTaskAsCompleted(999)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "task with id 999 not found")
}

Untuk menjalankan test, cukup gunakan perintah:

go test ./internal/repository

โœ… Kesimpulan

Di seri ini kita telah:

  • Membuat model Task

  • Mengimplementasikan InMemoryTaskRepository

  • Menambahkan unit test lengkap untuk setiap metode repository

Ini adalah pondasi penting sebelum kita masuk ke bagian interaktif (command line interface) di seri berikutnya.


Sampai jumpa di bagian ketiga! ๐Ÿš€

0
Subscribe to my newsletter

Read articles from Ajitama Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ajitama Dev
Ajitama Dev