Lompat ke konten
dokumentasi.id
Commit 43aad26
v1
Star1,1 rb

4

Menjalankan Aplikasi, Migrasi, dan Test

Repository ini tidak memakai Makefile atau skrip pembungkus. Seluruh langkah operasional ditulis sebagai perintah shell di README: migrasi schema, dua entry point aplikasi, dan satu perintah test README.md:63-81. Semua perintah itu membaca berkas config.json yang sama, sehingga urutan start dan isi konfigurasi menentukan apakah satu proses bisa hidup. Rincian tiap field konfigurasi ada di Konfigurasi dan Bootstrap.

Prasyarat dan Urutan Start

Diagram berikut memetakan apa yang harus hidup sebelum tiap proses bisa dijalankan.

graph TD
  cfg["config.json"]
  mysql[("MySQL localhost:3306"))]
  kafka["Kafka localhost:9092"]
  migrate["migrate ... up"]
  tables["tabel users, contacts, addresses"]
  web["go run cmd/web/main.go"]
  worker["go run cmd/worker/main.go"]
  test["go test -v ./test/"]

  cfg --> migrate
  mysql --> migrate
  migrate --> tables
  tables --> web
  tables --> test
  cfg --> web
  cfg --> worker
  kafka --> worker
  kafka -. "hanya jika producer.enabled=true" .-> web

  style mysql fill:#336791,color:#fff
  style tables fill:#336791,color:#fff
  style kafka fill:#336791,color:#fff
  style cfg fill:#f59e0b,color:#000
  style migrate fill:#f59e0b,color:#000
  style web fill:#2d7d46,color:#fff
  style worker fill:#2d7d46,color:#fff
  style test fill:#8b5cf6,color:#fff
%% fig: Prasyarat dan urutan start tiap proses
%% desc: Migration menyiapkan tabel di MySQL sebelum web, worker, dan test dijalankan; Kafka hanya dibutuhkan worker dan producer HTTP yang diaktifkan.
Fig. 1: Prasyarat dan urutan start tiap prosesMigration menyiapkan tabel di MySQL sebelum web, worker, dan test dijalankan; Kafka hanya dibutuhkan worker dan producer HTTP yang diaktifkan.

Tiga hal yang perlu disiapkan berurutan:

  1. MySQL hidup di localhost:3306 dengan database golang_clean_architecture config.json:12-23.
  2. Migration dijalankan sehingga tabel users, contacts, dan addresses sudah ada.
  3. config.json berada di direktori kerja atau satu tingkat di atasnya. config.NewViper mencari berkas config bertipe json di ./../ lalu di ./, dan panic dengan pesan Fatal error config file: %w kalau tidak menemukannya internal/config/viper.go:11-24.

Migrasi Database

README menaruh seluruh migration di folder db/migrations dan memberi dua perintah: membuat berkas migration baru dan menjalankan migration README.md:47-61. Berkas yang ada berpasangan up dan down, dan tiap berkas up membuat satu tabel.

Berkas upTabelKolom wajibForeign key
20231030144428_create_table_users.up.sqlusersid, name, password, created_at, updated_at; token boleh null—
20231030144435_create_table_contacts.up.sqlcontactsid, first_name, user_id, created_at, updated_atfk_contacts_user_id references users (id)
20231030144441_create_table_addresses.up.sqladdressesid, contact_id, created_at, updated_atfk_addresses_contact_id references contacts (id)

Semua id bertipe varchar(100) dan kedua timestamp disimpan sebagai bigint, bukan datetime db/migrations/20231030144428_create_table_users.up.sql:1-10 db/migrations/20231030144435_create_table_contacts.up.sql:1-13 db/migrations/20231030144441_create_table_addresses.up.sql:1-14. Tipe kolom lengkap per entity dibahas di Entity dan Schema Database.

Membuat berkas migration baru:

migrate create -ext sql -dir db/migrations create_table_xxx

Perintah itu membuat sepasang berkas up dan down dengan prefiks timestamp README.md:53-55. Menjalankan seluruh migration yang belum diterapkan:

migrate -database "mysql://root:@tcp(localhost:3306)/golang_clean_architecture?charset=utf8mb4&parseTime=True&loc=Local" -path db/migrations up

README.md:59-61

Ada dua format DSN berbeda untuk database yang sama. Perintah migrate memakai skema URL mysql://..., sedangkan aplikasi membangun DSN Go MySQL driver %s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local dari nilai config.json internal/config/gorm.go:24. Kalau database.host, database.port, atau database.name diubah, perintah migrate harus diubah terpisah karena tidak membaca config.json config.json:12-23.

⚠️ Berkas down bersifat destruktif. Ketiganya hanya berisi satu pernyataan drop table tanpa IF EXISTS: drop table users;, drop table contacts;, dan drop table addresses; db/migrations/20231030144428_create_table_users.down.sql:1 db/migrations/20231030144435_create_table_contacts.down.sql:1 db/migrations/20231030144441_create_table_addresses.down.sql:1. Karena addresses mereferensikan contacts dan contacts mereferensikan users, urutan rollback harus dibalik: addresses, lalu contacts, lalu users.

Menjalankan Web Server

cmd/web/main.go menyusun komponen dengan urutan tetap: config.NewViper, config.NewLogger, config.NewDatabase, config.NewValidator, config.NewFiber, config.NewKafkaProducer, lalu config.Bootstrap dengan semua komponen itu sebagai BootstrapConfig cmd/web/main.go:8-23. Setelah route terdaftar, server listen pada web.port yang bernilai 3000 di config.json config.json:5-8. Kegagalan listen ditutup dengan log.Fatalf("Failed to start server: %v", err) cmd/web/main.go:25-30.

config.NewDatabase gagal keras kalau MySQL tidak bisa dihubungi: pesan failed to connect database: %v dikirim lewat log.Fatalf internal/config/gorm.go:35-42. Ukuran pool diambil dari database.pool.idle (10), database.pool.max (100), dan database.pool.lifetime (300) config.json:12-23, lalu diterapkan dengan SetMaxIdleConns, SetMaxOpenConns, dan SetConnMaxLifetime internal/config/gorm.go:44-46.

Kafka tidak wajib untuk web server. Selama kafka.producer.enabled bernilai false seperti pada konfigurasi bawaan, NewKafkaProducer menulis log Kafka producer is disabled dan mengembalikan nil internal/config/kafka.go:32-36. config.Bootstrap lalu melewati pembuatan UserProducer, ContactProducer, dan AddressProducer internal/config/app.go:39-43, sehingga HTTP server tetap bisa melayani request tanpa broker.

Daftar route yang didaftarkan config.Bootstrap ada di Delivery HTTP dan Middleware, dan bentuk tiap endpoint ada di API Spec dan Daftar Endpoint.

Menjalankan Worker

Worker adalah proses terpisah tanpa HTTP server: go run cmd/worker/main.go README.md:77-81. Program membuat context.WithCancel, lalu menjalankan tiga consumer sebagai goroutine: RunUserConsumer untuk topic users, RunContactConsumer untuk topic contacts, dan RunAddressConsumer untuk topic addresses cmd/worker/main.go:21-25. Tiap fungsi membangun consumer group sendiri lewat config.NewKafkaConsumerGroup, membuat handler messaging.New*Consumer, lalu memanggil messaging.ConsumeTopic dengan nama topic-nya cmd/worker/main.go:43-62.

Proses ini tidak pernah selesai sendiri. Program menunggu SIGINT, SIGKILL, atau SIGTERM, mencatat Got one of stop signals, shutting down worker gracefully, SIGNAL NAME :, memanggil cancel(), lalu tidur 5 * time.Second supaya consumer selesai memproses cmd/worker/main.go:27-41. Setelah ctx dibatalkan, ConsumeTopic menutup consumer group lewat consumerGroup.Close() internal/delivery/messaging/consumer.go:71-75.

⚠️ Kalau broker tidak bisa dihubungi, NewKafkaConsumerGroup memanggil log.Fatalf dengan pesan Failed to create consumer group: %v internal/config/kafka.go:25-28. Satu consumer yang gagal membuat broker tidak terjangkau akan menghentikan seluruh proses worker, bukan hanya consumer itu. Broker dan group diambil dari config.json melalui kafka.bootstrap.servers dan kafka.group.id config.json:24-30.

Detail producer, consumer, dan handler per topic ada di Messaging Kafka: Producer dan Consumer.

Menjalankan Test

Perintah test di README adalah go test -v ./test/ README.md:65-69. Package test tidak memakai TestMain; seluruh aplikasi disiapkan sekali oleh init(): viper, logger, validator, Fiber app, database, dan Kafka producer, lalu config.Bootstrap test/init.go:23-38. Karena NewViper mencari ./../ lebih dulu, test binary menemukan config.json di akar repository walaupun direktori kerjanya test/ internal/config/viper.go:16-17.

Test tidak membuka port. Setiap test membangun httptest.NewRequest, memanggil app.Test(request) pada instance Fiber dari init(), lalu memeriksa response.StatusCode dan isi model.WebResponse test/user_test.go:17-46. Karena producer pada init() juga nil selama kafka.producer.enabled masih false internal/config/kafka.go:32-36, test hanya memerlukan MySQL yang sudah dimigrasi.

Alurnya sebagai berikut.

graph TD
  run["go test -v ./test/"]
  init["init() di test/init.go"]
  boot["config.Bootstrap"]
  fixtures["ClearAll()"]
  db[("MySQL")]
  req["httptest.NewRequest"]
  app["app.Test(request)"]
  resp["*http.Response + assert"]

  run --> init
  init --> boot
  boot --> app
  run --> fixtures
  fixtures -->|"delete users, contacts, addresses"| db
  run --> req
  req --> app
  app --> db
  app --> resp

  style db fill:#336791,color:#fff
  style init fill:#f59e0b,color:#000
  style boot fill:#f59e0b,color:#000
  style fixtures fill:#dc2626,color:#fff
  style run fill:#8b5cf6,color:#fff
  style req fill:#8b5cf6,color:#fff
  style app fill:#2d7d46,color:#fff
  style resp fill:#2d7d46,color:#fff
%% fig: Alur satu kali go test
%% desc: init() menyiapkan app dan database, helper menghapus data lama, lalu tiap test mengirim request lewat app.Test tanpa membuka port.
Fig. 2: Alur satu kali go testinit() menyiapkan app dan database, helper menghapus data lama, lalu tiap test mengirim request lewat app.Test tanpa membuka port.

Helper untuk menyiapkan dan membersihkan data tinggal di satu berkas.

FungsiKegunaan
ClearAll()memanggil ClearAddresses(), ClearContact(), lalu ClearUsers()
ClearUsers()db.Where("id is not null").Delete(&entity.User{}), gagal berarti log.Fatalf
ClearContact()db.Where("id is not null").Delete(&entity.Contact{})
ClearAddresses()db.Where("id is not null").Delete(&entity.Address{})
CreateContacts(user, total)membuat total contact milik satu user dengan ID dari uuid.NewString()
CreateAddresses(t, contact, total)membuat total address milik satu contact dengan alamat contoh
GetFirstUser(t)db.First(user)
GetFirstContact(t, user)db.Where("user_id = ?", user.ID).First(contact)
GetFirstAddress(t, contact)db.Where("contact_id = ?", contact.ID).First(address)

Urutan ClearAll mengikuti arah foreign key: addresses lebih dulu, lalu contacts, lalu users test/helper_test.go:12-37. Fungsi pembuat data ada di test/helper_test.go:39-54 dan test/helper_test.go:56-70, sedangkan pengambil data pertama ada di test/helper_test.go:72-90.

⚠️ Test bergantung pada state database dan saling memanggil. TestRegisterDuplicate menjalankan ClearAll() lalu memanggil TestRegister(t) untuk menyiapkan user sebelum menguji http.StatusConflict test/user_test.go:78-80. Karena ClearAll menghapus seluruh baris di tiga tabel test/helper_test.go:12-37, dua proses go test yang berjalan bersamaan pada database yang sama akan saling menghapus data.