feat: 添加服务层单元测试指南,涵盖测试设计、结构和约定
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user