Test Fixtures (coretest)
coretest builds the fixtures a service's tests need, so you do not hand-roll the same forty lines of wiring in every repository.
import "gitlab.finema.co/finema/idin-core/coretest"
func TestCreateUser(t *testing.T) {
ctx := coretest.NewContext(t)
err := services.NewUserService(ctx).Create(payload)
coretest.RequireNoError(t, err)
}Import it from _test.go files only — it depends on testing, and every helper takes a *testing.T so failures point at your test rather than at the package.
That context comes with a real in-memory cache and a real logger. Nothing is a mock that does not have to be, so the code under test takes the same path it takes in production.
Configuration without a .env file
core.NewEnv() reads a .env through a package-level viper, so two tests cannot hold different configuration and a stray file in the checkout changes what they see. coretest.NewENV has neither problem:
env := coretest.NewENV(t,
coretest.WithEnv(map[string]string{"jwt_secret": "test-secret"}),
coretest.WithENVConfig(core.ENVConfig{ENV: "test", Service: "orders"}),
)Keys are case-insensitive, as viper's were. The same options work on NewContext, which is usually where you want them:
ctx := coretest.NewContext(t, coretest.WithEnv(map[string]string{"jwt_secret": "s"}))Choosing what the context holds
| Option | What it does |
|---|---|
WithCache(c) | replace the in-memory cache (a mock, or NewNoopCache) |
WithCaches(map) | named caches, reached through ctx.Caches(name) |
WithMongo(db) | usually core.NewMockMongoDB() |
WithMQ(mq) | usually core.NewMockMQ() |
WithContextType(t) | consts.HTTP, consts.CRONJOB… (default consts.E2E) |
WithEnv(map) | configuration keys |
WithENVConfig(cfg) | the typed config behind ENV().Config() |
mq := core.NewMockMQ()
mq.On("PublishJSON", mock.Anything, mock.Anything, mock.Anything).Return(nil)
ctx := coretest.NewContext(t, coretest.WithMQ(mq))Use coretest.NewOptions(t, ...) when you need the *core.ContextOptions itself — to build a cronjob context, say, over the same dependencies.
Adding a database
A database is the one thing the fixtures cannot conjure. Point the suite at one and it appears:
TEST_DATABASE_URL=postgres://user:pass@localhost:5432/test go test ./...func TestUserRepository(t *testing.T) {
ctx := coretest.NewContext(t, coretest.WithAutoMigrate(&models.User{}))
repo := repository.New[models.User](ctx)
require.Nil(t, repo.Create(&models.User{ID: "u-1", Email: "[email protected]"}))
found, err := repo.FindOne("id = ?", "u-1")
require.Nil(t, err)
assert.Equal(t, "[email protected]", found.Email)
}Each test gets its own postgres schema, created before it runs and dropped after. A schema is the cheap unit of isolation: creating one costs a statement, tests stay independent enough to run in parallel, and dropping it cascades away every table — so this can point at a development database without disturbing the data already in it.
Without TEST_DATABASE_URL, database tests skip
They skip rather than fail, so go test ./... on a fresh checkout stays green and the message says which variable turns them on — instead of a connection error somebody has to decode.
coretest.HasDatabase() reports which mode this run is in.
Schema from migrations, not from structs
WithAutoMigrate builds the schema your Go structs describe, which is not the one you deploy. For anything whose constraints matter, run the real migrations:
ctx := coretest.NewContext(t, coretest.WithMigrations("./prisma/migrations"))It applies every <dir>/*/migration.sql in name order — the layout prisma and golang-migrate both produce. This is what makes the database run worth having: the tables, types and indexes are the ones production has, so a constraint your code contradicts fails here rather than in staging.
Other helpers: WithDatabase() for a database with no tables, WithDatabaseURL to override the environment, and coretest.NewDB(t, ...) for the *gorm.DB alone.
HTTP tests
coretest.NewServer builds a server on a test context and drives it through the real middleware stack — the context middleware, request id, recovery, CORS — so what you assert is what a caller receives, not what a handler returns to a hand-built echo.Context.
func TestCreateUser(t *testing.T) {
srv := coretest.NewServer(t, coretest.WithAutoMigrate(&models.User{}))
srv.POST("/users", core.WithHTTPContext(controller.UserController{}.Create))
body := srv.Post("/users", map[string]string{"email": "[email protected]", "full_name": "Ann"}).
RequireStatus(http.StatusCreated).
Map()
assert.Equal(t, "[email protected]", body["email"])
}Get, Post, Put, Patch and Delete all take an optional headers map. Bodies may be a string, []byte, or any value to encode as JSON:
srv.Get("/me", map[string]string{"Authorization": "Bearer " + token})
srv.Post("/users", `{"email":"[email protected]"}`)Asserting on rejections
res := srv.Post("/users", `{}`).RequireStatus(http.StatusBadRequest)
assert.Equal(t, map[string]string{
"email": "REQUIRED",
"full_name": "REQUIRED",
}, res.FieldCodes())FieldCodes() reduces a validation failure to field → code so the whole set is one comparison. Error() gives the decoded body when you want the messages too.
srv.Context() returns a context on the same dependencies, for seeding rows before a request or asserting after one.
Assertions on core.IError
A plain require.Error says nothing useful about a framework error. What matters is the status a caller receives and the code they branch on:
coretest.RequireNoError(t, err)
coretest.RequireStatus(t, err, http.StatusNotFound)
coretest.RequireCode(t, err, "USER_NOT_FOUND")
coretest.RequireError(t, err)RequireNoError takes core.IError rather than error deliberately: a nil *Error stored in an error interface is not nil, and this signature makes that mistake impossible to write. On failure it prints the status, code and message rather than an address.
For validation errors:
assert.Equal(t, map[string]string{"email": "REQUIRED"}, coretest.FieldCodes(t, err))
assert.Equal(t, "REQUIRED", coretest.Fields(t, err)["email"].Code)Both render the error and read it back the way a client receives it, so what you assert is the wire shape rather than the framework's internals.
Testing rules that touch the database
Uniqueness and existence rules need a real database — that is the whole reason they are worth testing:
func TestUserCreate_rejectsATakenEmail(t *testing.T) {
ctx := coretest.NewContext(t, coretest.WithAutoMigrate(&models.User{}))
require.Nil(t, repository.New[models.User](ctx).Create(
&models.User{ID: "u-1", Email: "[email protected]"}))
err := (&requests.UserCreate{Email: utils.ToPointer("[email protected]")}).Valid(ctx)
require.NotNil(t, err)
assert.Equal(t, "UNIQUE", coretest.FieldCodes(t, err)["email"])
}See Validation for what those rules do when the database is unreachable.