mit einem Klick
test
// Write tests for OpenMeter services following project conventions. Use when creating unit tests, integration tests, or service tests.
// Write tests for OpenMeter services following project conventions. Use when creating unit tests, integration tests, or service tests.
Work with the subscription sync bridge in `openmeter/billing/worker/subscriptionsync/...`. Use when modifying how subscription target state is reconciled into billing artifacts such as invoice lines, split-line groups, or charges; when changing persisted-state loading, reconciler patch routing, or subscription sync tests; and when reasoning about the bridge between subscription views and billing state.
Use when writing or refactoring Go code in OpenMeter that can use github.com/samber/lo helpers, especially trivial slice-to-map, map keys/values, mapping, filtering, grouping, uniqueness, set-like conversions, and map entry transformations.
Work with OpenMeter billing charges, including the root charges facade, charge meta queries, charge creation and advancement, usage-based lifecycle state machines, realization runs, and charges test setup. Use when modifying `openmeter/billing/charges/...` or charge-related tests.
Work with the OpenMeter ledger package. Use when modifying ledger code, writing ledger tests, or debugging ledger issues.
Add or modify API endpoints using TypeSpec. Use when adding new API routes, modifying request/response types, or changing the OpenAPI spec.
Work with the OpenMeter billing package. Use this skill whenever touching invoice lifecycle, billing profiles, customer overrides, invoice line items, gathering invoices, standard invoices, the invoice state machine, billing validation issues, billing-subscription sync, the billing worker, invoice calculation, rating/pricing engine, or tax config on billing objects. Also use when writing or debugging billing integration tests (BaseSuite, SubscriptionMixin), billing adapter (Ent queries), billing HTTP handlers, or the subscription→billing sync algorithm. Trigger this skill for any file under `openmeter/billing/`, `openmeter/billing/worker/`, `openmeter/billing/service/`, `openmeter/billing/adapter/`, `openmeter/billing/rating/`, `test/billing/`, or `cmd/billing-worker/`.
| name | test |
| description | Write tests for OpenMeter services following project conventions. Use when creating unit tests, integration tests, or service tests. |
| user-invocable | true |
| argument-hint | [description of what to test] |
| allowed-tools | Read, Edit, Write, Bash, Grep, Glob, Agent |
You are helping the user write tests for OpenMeter following established conventions.
| Type | Purpose | Location | DB Required |
|---|---|---|---|
| Unit tests | Validation, pure functions | openmeter/<domain>/<domain>_test.go | No |
| Integration tests | Adapter against real Postgres | openmeter/<domain>/adapter/*_test.go | Yes |
| Service tests | Full stack via TestEnv | openmeter/<domain>/service/*_test.go | Yes |
make test # All tests (sets POSTGRES_HOST=127.0.0.1)
make test-nocache # Tests bypassing cache
make etoe # End-to-end tests (requires docker compose)
Before running: docker compose up -d postgres
Build tag: all Go test commands use -tags=dynamic.
Prefer direct command execution. Do not wrap test commands in sh -lc, bash -lc, or similar helper shells when a direct invocation works. For environment variables, prefer env POSTGRES_HOST=127.0.0.1 go test ... or POSTGRES_HOST=127.0.0.1 go test ....
For running a specific test directly:
POSTGRES_HOST=127.0.0.1 go test -tags=dynamic ./openmeter/<domain>/...
When a Postgres-backed test fails, the suite output often includes a testdbconf: URL for the per-test database. Use that URL with psql to inspect the failed test state before rerunning, because the database is still useful for RCA. Quote the connection string if it contains query parameters, for example:
psql 'postgres://pgtdbuser:pgtdbpass@127.0.0.1:5432/testdb_tpl_...?sslmode=disable'
From openmeter/testutils/:
| Utility | Usage |
|---|---|
testutils.InitPostgresDB(t) | Provisions fresh Postgres DB per test; skips if POSTGRES_HOST not set |
testutils.NewDiscardLogger(t) | Silent logger for tests |
testutils.NewLogger(t) | Default slog logger for tests |
From openmeter/watermill/eventbus/:
| Utility | Usage |
|---|---|
eventbus.NewMock(t) | Mock event publisher for tests |
For service/integration tests, create a testutils/ package with a TestEnv that wires up the full stack.
Reference: openmeter/customer/testutils/env.go
package testutils
import (
"sync"
"testing"
"github.com/stretchr/testify/require"
"<domain>"
<domain>adapter "<domain>/adapter"
<domain>service "<domain>/service"
entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
"github.com/openmeterio/openmeter/openmeter/testutils"
"github.com/openmeterio/openmeter/openmeter/watermill/eventbus"
)
type TestEnv struct {
Logger *slog.Logger
Service <domain>.Service
Client *entdb.Client
db *testutils.TestDB
close sync.Once
}
func (e *TestEnv) DBSchemaMigrate(t *testing.T) {
t.Helper()
require.NotNilf(t, e.db, "database must be initialized")
err := e.db.EntDriver.Client().Schema.Create(t.Context())
require.NoErrorf(t, err, "schema migration must not fail")
}
func (e *TestEnv) Close(t *testing.T) {
t.Helper()
e.close.Do(func() {
if e.db != nil {
if err := e.db.EntDriver.Close(); err != nil {
t.Errorf("failed to close ent driver: %v", err)
}
if err := e.db.PGDriver.Close(); err != nil {
t.Errorf("failed to close postgres driver: %v", err)
}
}
if e.Client != nil {
if err := e.Client.Close(); err != nil {
t.Errorf("failed to close ent client: %v", err)
}
}
})
}
func NewTestEnv(t *testing.T) *TestEnv {
t.Helper()
logger := testutils.NewDiscardLogger(t)
// Init database
db := testutils.InitPostgresDB(t)
client := db.EntDriver.Client()
// Init event publisher
publisher := eventbus.NewMock(t)
// Init adapter
adapter, err := <domain>adapter.New(<domain>adapter.Config{
Client: client,
Logger: logger,
})
require.NoErrorf(t, err, "initializing adapter must not fail")
// Init service
service, err := <domain>service.New(<domain>service.Config{
Adapter: adapter,
Publisher: publisher,
})
require.NoErrorf(t, err, "initializing service must not fail")
return &TestEnv{
Logger: logger,
Service: service,
Client: client,
db: db,
close: sync.Once{},
}
}
func TestCreate<Resource>(t *testing.T) {
env := testutils.NewTestEnv(t)
t.Cleanup(func() { env.Close(t) })
env.DBSchemaMigrate(t)
ns := testutils.NewTestNamespace(t) // generates a random ULID namespace
t.Run("success", func(t *testing.T) {
result, err := env.Service.Create<Resource>(t.Context(), <domain>.Create<Resource>Input{
Namespace: ns,
Name: "test",
})
require.NoError(t, err)
assert.Equal(t, "test", result.Name)
})
t.Run("validation error", func(t *testing.T) {
_, err := env.Service.Create<Resource>(t.Context(), <domain>.Create<Resource>Input{})
require.Error(t, err)
assert.True(t, models.IsGenericValidationError(err))
})
}
For testing validation, pure functions, and domain logic without DB:
func TestCreate<Resource>Input_Validate(t *testing.T) {
tests := []struct {
name string
input <domain>.Create<Resource>Input
wantErr bool
}{
{
name: "valid",
input: <domain>.Create<Resource>Input{
Namespace: "test-ns",
Name: "test",
},
wantErr: false,
},
{
name: "missing namespace",
input: <domain>.Create<Resource>Input{Name: "test"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.input.Validate()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
Use clock.SetTime or clock.FreezeTime for time-deterministic tests. Always defer a reset:
func TestTimeSensitive(t *testing.T) {
now := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
clock.SetTime(now)
defer clock.ResetTime()
// Test code that depends on current time...
}
clock.SetTime(t) — sets the clock to a specific timeclock.FreezeTime(t) — freezes time so it doesn't advancedefer clock.ResetTime() to avoid leaking into other testsWithinDuration(...) for clock-sensitive assertions. It is fragile under varying CI worker load and scheduler timing.clock.Now(), prefer clock.FreezeTime(...) and assert exact equality instead of tolerances.For complex test scenarios with shared setup/teardown, use testify's suite.Suite:
type <Domain>Suite struct {
suite.Suite
*require.Assertions
env *testutils.TestEnv
}
func (s *<Domain>Suite) SetupSuite() {
s.env = testutils.NewTestEnv(s.T())
s.env.DBSchemaMigrate(s.T())
}
func (s *<Domain>Suite) TearDownSuite() {
s.env.Close(s.T())
}
func (s *<Domain>Suite) TestCreate() {
// Use s.Require() or s.Assert() for assertions
result, err := s.env.Service.Create(s.T().Context(), input)
s.NoError(err)
s.Equal("expected", result.Name)
}
func TestSuite(t *testing.T) {
suite.Run(t, new(<Domain>Suite))
}
Key conventions:
*require.Assertions for convenient assertion methodsSetupSuite/TearDownSuite for one-time setup (DB, env)SetupTest/TearDownTest for per-test setup if neededrequire for fatal assertions (test cannot continue), assert for soft assertionst.Helper() in all helper functionst.Context() instead of context.Background()t.Run() for multiple casestestutils.NewTestULID(t) or testutils.NewTestNamespace(t) for random test identifiersPOSTGRES_HOST is not set (via testutils.InitPostgresDB)