feat: 添加服务层单元测试指南,涵盖测试设计、结构和约定

This commit is contained in:
2025-12-18 14:32:51 +08:00
parent 163851cb5a
commit 5fe669fef6

View File

@@ -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)
})
}
```