From 5fe669fef6e9beddd967b944ea9c9c982c07d09d Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 18 Dec 2025 14:32:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=B1=82=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E6=8C=87=E5=8D=97?= =?UTF-8?q?=EF=BC=8C=E6=B6=B5=E7=9B=96=E6=B5=8B=E8=AF=95=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E3=80=81=E7=BB=93=E6=9E=84=E5=92=8C=E7=BA=A6=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/llm.txt | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) 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) + }) +} +```