Skip to content

Testing Conventions

Panduan menulis dan menjalankan test di codebase RetailOS.

Go Tests

Menjalankan Tests

bash
# Semua tests
go test ./...

# Package tertentu
go test ./internal/store/pos/...
go test ./internal/cloud/router/...

# Dengan verbose output
go test -v -count=1 ./internal/store/router/...

# Dengan race detector
go test -race ./...

# E2E tests
go test -v -tags=e2e ./test/e2e/...

Table-Driven Tests

RetailOS menggunakan table-driven tests sebagai pattern utama:

go
func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name     string
        subtotal float64
        promoID  string
        memberTier string
        wantDisc float64
        wantErr  bool
    }{
        {
            name:     "no promo no member",
            subtotal: 100000,
            wantDisc: 0,
        },
        {
            name:       "gold member 10% off",
            subtotal:   100000,
            memberTier: "gold",
            wantDisc:   10000,
        },
        {
            name:     "promo buy 2 get 1",
            subtotal: 50000,
            promoID:  "PROMO-B2G1",
            wantDisc: 16667,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            disc, err := calculateDiscount(tt.subtotal, tt.promoID, tt.memberTier)
            if (err != nil) != tt.wantErr {
                t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
            }
            if disc != tt.wantDisc {
                t.Errorf("discount = %v, want %v", disc, tt.wantDisc)
            }
        })
    }
}

Mock Database Pattern

Untuk unit test yang membutuhkan database, RetailOS menggunakan in-memory SQLite:

go
func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() { db.Close() })

    // Run migrations
    for _, migration := range storeMigrations {
        if _, err := db.Exec(migration); err != nil {
            t.Fatalf("migration failed: %v", err)
        }
    }

    return db
}

func TestCreateTransaction(t *testing.T) {
    db := setupTestDB(t)

    // Seed test data
    seedTestProducts(t, db)
    seedTestSession(t, db)

    mgr := pos.NewTransactionManager(db, "STORE-TEST")

    // Test
    txn, err := mgr.Create(ctx, pos.CreateRequest{
        SessionID: "sess-01",
        Items: []pos.Item{
            {SKU: "SKU-001", Qty: 2, Price: 5000},
        },
    })

    if err != nil {
        t.Fatal(err)
    }
    if txn.Total != 10000 {
        t.Errorf("total = %v, want 10000", txn.Total)
    }
}

Full Test Helpers

Router integration tests menggunakan helper yang setup full Dependencies:

go
// internal/store/router/full_test_helpers_test.go

func setupFullRouter(t *testing.T) (http.Handler, *sql.DB) {
    db := setupTestDB(t)

    deps := Dependencies{
        DB:             db,
        StoreID:        "STORE-TEST",
        EventLog:       eventlog.New(db),
        POSSession:     pos.NewSessionManager(db, "STORE-TEST"),
        POSTransaction: pos.NewTransactionManager(db, "STORE-TEST"),
        POSPayment:     pos.NewPaymentManager(db),
        // ... semua dependencies
    }

    router := New(deps)
    return router, db
}

HTTP Test Pattern

go
func TestPOSTransactionFlow(t *testing.T) {
    router, db := setupFullRouter(t)

    // Seed data
    seedTestProducts(t, db)

    // Open shift
    req := httptest.NewRequest("POST", "/api/pos/sessions/open", strings.NewReader(`{
        "pos_id": "POS-01",
        "cashier_id": "EMP-01",
        "cashier_name": "Test Cashier",
        "starting_cash": 200000
    }`))
    req.Header.Set("X-API-Key", "test-key")
    w := httptest.NewRecorder()
    router.ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Fatalf("open shift: status %d, body: %s", w.Code, w.Body.String())
    }
}

Test File Organization

internal/store/router/
├── router.go                       ← Production code
├── pos_handlers.go
├── pos_handlers_test.go            ← Unit tests per handler
├── cashops_handlers.go
├── cashops_handlers_test.go
├── full_test_helpers_test.go        ← Shared test setup
└── router_test.go                   ← Router integration tests

internal/cloud/router/
├── router.go
├── router_test.go
└── ...

E2E Tests

End-to-end tests berada di test/e2e/ dan menguji alur lengkap:

FileCakupan
pos_e2e_test.goFull POS flow: shift, transaction, payment, close
stock_store_e2e_test.goStock operations: receiving, transfer, opname
edge_cloud_finance_e2e_test.goStore-to-cloud sync, settlement
identity_auth_e2e_test.goLogin, JWT, role-based access
dc_operations_e2e_test.goDC picking, packing, shipping
masterdata_commercial_procurement_e2e_test.goMaster data CRUD, PO flow

Frontend Tests

POS Electron

bash
cd pos-electron
npm test

Menggunakan Vitest + React Testing Library:

typescript
// __tests__/components/cart.test.tsx
import { render, screen } from '@testing-library/react'
import { Cart } from '../renderer/components/pos/cart'

test('displays empty cart message', () => {
    render(<Cart items={[]} />)
    expect(screen.getByText('Keranjang Kosong')).toBeInTheDocument()
})

Portal Tests

bash
cd ho-finance
npm test

Portal tests menggunakan Vitest dengan konfigurasi di vitest.config.ts.

CI/CD

CI dijalankan via GitHub Actions (.github/workflows/ci.yml):

  1. Go tests -- go test ./... with race detector
  2. Go vet -- Static analysis
  3. Frontend lint -- ESLint pada portal TypeScript
  4. Build check -- Pastikan semua binary dan portal bisa di-build

RetailOS - Sistem ERP Retail Modern untuk Indonesia