diff --git a/backend/llm.txt b/backend/llm.txt index 14e1297..867f07d 100644 --- a/backend/llm.txt +++ b/backend/llm.txt @@ -230,3 +230,96 @@ Generator will convert snake_case columns to Go struct field names (e.g. `class_ - `atomctl migrate create ...` / `atomctl migrate up` - `atomctl gen model` / `atomctl gen enum` / `atomctl gen service` - `make init` (full refresh) + +--- + +## 5) Service Layer Unit Testing Guidelines (Generic) + +This section is framework-agnostic and applies to any Go service layer (regardless of DI container, ORM, or web framework). + +### 5.1 Decide what you are testing + +- **Pure unit tests**: no DB/network/filesystem; dependencies are mocked/faked; tests are fast and deterministic. +- **DB-backed tests (recommended whenever the feature touches the database)**: exercise a real database to validate SQL, constraints, transactions, and ORM behavior. +- Always state which tier the test belongs to and keep the scope consistent. + +### 5.2 Design the service for testability + +- Inject dependencies via constructor or fields; depend on **interfaces**, not concrete DB clients. +- Keep domain logic **pure** where possible: parse/validate/compute should be testable without IO. +- Make time/UUID/randomness deterministic by injecting `Clock`/`IDGenerator` when needed. +- If the feature requires database access, **do not mock the database**; test with an **actual database** (ideally same engine/version as production) to ensure data accuracy. Use mocks/fakes only for non-DB external dependencies when appropriate (e.g., HTTP, SMS, third-party APIs). + +### 5.3 Test structure and conventions + +- Prefer `*_test.go` with table-driven tests and subtests: `t.Run("case", func(t *testing.T) { ... })`. +- Prefer testing the public API from an external package (`package xxx_test`) unless you must access unexported helpers. +- Avoid “focused” tests in committed code (e.g. `FocusConvey`, `FIt`, `fit`, `it.only`, or equivalent), because they silently skip other tests. + +### 5.4 Isolation rules + +- Each test must be independent and order-agnostic. +- For integration tests: + - Use transaction rollback per test when possible; otherwise use truncate + deterministic fixtures. + - Never depend on developer-local state; prefer ephemeral DB (container) or a dedicated test database/schema. + +### 5.5 Assertions and error checks + +- Always assert both **result** and **error** (and error types via `errors.Is` / `errors.As` when wrapping is used). +- Keep assertions minimal but complete: verify behavior, not implementation details. +- Use the standard library (`testing`) or a single assertion library consistently across the repo. + +### 5.6 Minimal test file template (DI-bootstrapped, DB-backed) + +This template matches a common pattern where tests boot a DI container and run against a real database. Replace the bootstrap (`testx.Default/Serve`, `Provide`) and cleanup (`database.Truncate`) with your project's equivalents. + +```go +package services + +import ( + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + "quyun/v2/database" + "quyun/v2/database/models" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type XxxTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` +} + +type XxxTestSuite struct { + suite.Suite + XxxTestSuiteInjectParams +} + +func Test_Xxx(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p XxxTestSuiteInjectParams) { + suite.Run(t, &XxxTestSuite{XxxTestSuiteInjectParams: p}) + }) +} + +func (s *XxxTestSuite) Test_Method() { + Convey("describe behavior here", s.T(), func() { + ctx := s.T().Context() + + database.Truncate(ctx, s.DB, models.TableNameUser) + + got, err := User.FindByUsername(ctx, "alice") + So(err, ShouldNotBeNil) + So(got, ShouldBeNil) + }) +} +```