ワンクリックで
test
Write tests for OpenMeter services following project conventions. Use when creating unit tests, integration tests, or service tests.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Write tests for OpenMeter services following project conventions. Use when creating unit tests, integration tests, or service tests.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Work on OpenMeter currency primitives in pkg/currencyx for fiat and custom currency codes, shared currency interfaces, rounding modes, calculators, allocation precision, fiat/custom boundaries, and callers in billing, charges, ledger, product catalog, subscriptions, API, or currency registry code.
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.
Write end-to-end tests for OpenMeter against a live server. Use when adding tests under e2e/ that exercise API endpoints over HTTP (v1 generated SDK or v3 raw HTTP).
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/`.
Use when writing or refactoring Go collection/pointer helper code in OpenMeter, especially when choosing between standard library slices/maps helpers and github.com/samber/lo for cloning, copying, equality, sorting, pointer literals, slice-to-map transforms, map keys/values, mapping, filtering, grouping, uniqueness, set-like conversions, and map entry transformations.
Add or modify API endpoints using TypeSpec. Use when adding new API routes, modifying request/response types, or changing the OpenAPI spec.
| 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)