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-811. 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.Tiga hal yang perlu disiapkan berurutan:
- MySQL hidup di
localhost:3306dengan databasegolang_clean_architectureconfig.json:12-232. - Migration dijalankan sehingga tabel
users,contacts, danaddressessudah ada. config.jsonberada di direktori kerja atau satu tingkat di atasnya.config.NewVipermencari berkasconfigbertipejsondi./../lalu di./, dan panic dengan pesanFatal error config file: %wkalau tidak menemukannya internal/config/viper.go:11-243.
Migrasi Database
README menaruh seluruh migration di folder db/migrations dan memberi dua perintah: membuat berkas migration baru dan menjalankan migration README.md:47-614. Berkas yang ada berpasangan up dan down, dan tiap berkas up membuat satu tabel.
Berkas up | Tabel | Kolom wajib | Foreign key |
|---|---|---|---|
20231030144428_create_table_users.up.sql | users | id, name, password, created_at, updated_at; token boleh null | — |
20231030144435_create_table_contacts.up.sql | contacts | id, first_name, user_id, created_at, updated_at | fk_contacts_user_id references users (id) |
20231030144441_create_table_addresses.up.sql | addresses | id, contact_id, created_at, updated_at | fk_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-105 db/migrations/20231030144435_create_table_contacts.up.sql:1-136 db/migrations/20231030144441_create_table_addresses.up.sql:1-147. 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-558. 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
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:2410. Kalau database.host, database.port, atau database.name diubah, perintah migrate harus diubah terpisah karena tidak membaca config.json config.json:12-232.
⚠️ Berkas
downbersifat destruktif. Ketiganya hanya berisi satu pernyataandrop tabletanpaIF EXISTS:drop table users;,drop table contacts;, dandrop table addresses;db/migrations/20231030144428_create_table_users.down.sql:111 db/migrations/20231030144435_create_table_contacts.down.sql:112 db/migrations/20231030144441_create_table_addresses.down.sql:113. Karenaaddressesmereferensikancontactsdancontactsmereferensikanusers, urutan rollback harus dibalik:addresses, lalucontacts, laluusers.
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-2314. Setelah route terdaftar, server listen pada web.port yang bernilai 3000 di config.json config.json:5-815. Kegagalan listen ditutup dengan log.Fatalf("Failed to start server: %v", err) cmd/web/main.go:25-3016.
config.NewDatabase gagal keras kalau MySQL tidak bisa dihubungi: pesan failed to connect database: %v dikirim lewat log.Fatalf internal/config/gorm.go:35-4217. Ukuran pool diambil dari database.pool.idle (10), database.pool.max (100), dan database.pool.lifetime (300) config.json:12-232, lalu diterapkan dengan SetMaxIdleConns, SetMaxOpenConns, dan SetConnMaxLifetime internal/config/gorm.go:44-4618.
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-3619. config.Bootstrap lalu melewati pembuatan UserProducer, ContactProducer, dan AddressProducer internal/config/app.go:39-4320, 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-8121. 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-2522. 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-6223.
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-4124. Setelah ctx dibatalkan, ConsumeTopic menutup consumer group lewat consumerGroup.Close() internal/delivery/messaging/consumer.go:71-7525.
⚠️ Kalau broker tidak bisa dihubungi,
NewKafkaConsumerGroupmemanggillog.Fatalfdengan pesanFailed to create consumer group: %vinternal/config/kafka.go:25-2826. Satu consumer yang gagal membuat broker tidak terjangkau akan menghentikan seluruh proses worker, bukan hanya consumer itu. Broker dan group diambil dariconfig.jsonmelaluikafka.bootstrap.serversdankafka.group.idconfig.json:24-3027.
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-6928. 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-3829. Karena NewViper mencari ./../ lebih dulu, test binary menemukan config.json di akar repository walaupun direktori kerjanya test/ internal/config/viper.go:16-1730.
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-4631. Karena producer pada init() juga nil selama kafka.producer.enabled masih false internal/config/kafka.go:32-3619, 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.Helper untuk menyiapkan dan membersihkan data tinggal di satu berkas.
| Fungsi | Kegunaan |
|---|---|
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-3732. Fungsi pembuat data ada di test/helper_test.go:39-5433 dan test/helper_test.go:56-7034, sedangkan pengambil data pertama ada di test/helper_test.go:72-9035.
⚠️ Test bergantung pada state database dan saling memanggil.
TestRegisterDuplicatemenjalankanClearAll()lalu memanggilTestRegister(t)untuk menyiapkan user sebelum mengujihttp.StatusConflicttest/user_test.go:78-8036. KarenaClearAllmenghapus seluruh baris di tiga tabel test/helper_test.go:12-3732, dua prosesgo testyang berjalan bersamaan pada database yang sama akan saling menghapus data.