Chapter 04: Subtest Naming Strategies¶
Description¶
Using package-level constants for test case names ensures consistency across multiple test functions that exercise the same domain concept. When the same constant (orderID_001) appears in TestOrder_Confirm, TestOrder_Cancel, and TestOrder_Ship, it signals these tests are testing the same entity. Constants also serve as documentation — they name the test case and the test value simultaneously.
hexago/pkg/version/version_test.go:8-13— constants likeversion_0_0_1,version_0_0_1_rc_1reuse acrossTestVersionParseVersion,TestVersionParseDate,TestVersionString
Code¶
type OrderStatus string
const (
StatusPending OrderStatus = "pending"
StatusConfirmed OrderStatus = "confirmed"
StatusShipped OrderStatus = "shipped"
StatusDelivered OrderStatus = "delivered"
StatusCancelled OrderStatus = "cancelled"
)
type Order struct {
Status OrderStatus
ID string
Amount float64
}
func NewOrder(id string, amount float64) *Order {
return &Order{
ID: id,
Amount: amount,
Status: StatusPending,
}
}
func (o *Order) Confirm() error {
if o.Status != StatusPending {
return fmt.Errorf("cannot confirm order in status: %s", o.Status)
}
o.Status = StatusConfirmed
return nil
}
func (o *Order) Cancel() error {
if o.Status == StatusDelivered || o.Status == StatusCancelled {
return fmt.Errorf("cannot cancel order in status: %s", o.Status)
}
o.Status = StatusCancelled
return nil
}
func (o *Order) Ship() error {
if o.Status != StatusConfirmed {
return fmt.Errorf("cannot ship order in status: %s", o.Status)
}
o.Status = StatusShipped
return nil
}
func (o *Order) Deliver() error {
if o.Status != StatusShipped {
return fmt.Errorf("cannot deliver order in status: %s", o.Status)
}
o.Status = StatusDelivered
return nil
}
Test¶
const (
orderID_001 = "ORD-001"
orderAmount = 99.50
)
type checkOrderCancelFn func(*testing.T, *Order, error)
var checkOrderCancel = func(fns ...checkOrderCancelFn) []checkOrderCancelFn { return fns }
func checkCancelError(want string) checkOrderCancelFn {
return func(t *testing.T, s *Order, err error) {
t.Helper()
if want == "" {
assert.NoErrorf(t, err, "checkCancelError: expected no error, got %v", err)
return
}
if assert.Errorf(t, err, "checkCancelError: expected error %q", want) {
assert.Containsf(t, err.Error(), want, "checkCancelError mismatch")
}
}
}
func TestOrder_Cancel(t *testing.T) {
checkCancelled := func(want OrderStatus) checkOrderCancelFn {
return func(t *testing.T, o *Order, err error) {
t.Helper()
assert.Equal(t, want, o.Status)
}
}
tests := []struct {
name string
checks []checkOrderCancelFn
id string
amount float64
before func(*Order)
}{
{
name: orderID_001,
checks: checkOrderCancel(checkCancelError(""), checkCancelled(StatusCancelled)),
},
{
name: "confirm then cancel",
id: "ORD-002",
amount: 50.0,
before: func(o *Order) { o.Confirm() },
checks: checkOrderCancel(checkCancelError(""), checkCancelled(StatusCancelled)),
},
{
name: "shipped then cancel",
id: "ORD-003",
amount: 200.0,
before: func(o *Order) { o.Confirm(); o.Ship() },
checks: checkOrderCancel(checkCancelError(""), checkCancelled(StatusCancelled)),
},
{
name: "delivered",
id: "ORD-004",
amount: 30.0,
before: func(o *Order) { o.Confirm(); o.Ship(); o.Deliver() },
checks: checkOrderCancel(checkCancelError("cannot cancel order in status: delivered")),
},
{
name: "already cancelled",
id: "ORD-005",
amount: 75.0,
before: func(o *Order) { o.Cancel() },
checks: checkOrderCancel(checkCancelError("cannot cancel order in status: cancelled")),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
s := NewOrder(tt.id, tt.amount)
if tt.before != nil {
tt.before(s)
}
err := s.Cancel()
for _, c := range tt.checks {
c(t, s, err)
}
})
}
}
type checkOrderConfirmFn func(*testing.T, *Order, error)
var checkOrderConfirm = func(fns ...checkOrderConfirmFn) []checkOrderConfirmFn { return fns }
func checkConfirmError(want string) checkOrderConfirmFn {
return func(t *testing.T, s *Order, err error) {
t.Helper()
if want == "" {
assert.NoErrorf(t, err, "checkConfirmError: expected no error, got %v", err)
return
}
if assert.Errorf(t, err, "checkConfirmError: expected error %q", want) {
assert.Containsf(t, err.Error(), want, "checkConfirmError mismatch")
}
}
}
func TestOrder_Confirm(t *testing.T) {
checkConfirmed := func(want OrderStatus) checkOrderConfirmFn {
return func(t *testing.T, o *Order, err error) {
t.Helper()
assert.Equal(t, want, o.Status)
}
}
tests := []struct {
name string
checks []checkOrderConfirmFn
id string
amount float64
before func(*Order)
}{
{
name: orderID_001,
checks: checkOrderConfirm(checkConfirmError(""), checkConfirmed(StatusConfirmed)),
},
{
name: "shipped",
id: "ORD-002",
amount: 200.0,
before: func(o *Order) { o.Confirm(); o.Ship() },
checks: checkOrderConfirm(checkConfirmError("cannot confirm order in status: shipped")),
},
{
name: "delivered",
id: "ORD-003",
amount: 30.0,
before: func(o *Order) { o.Confirm(); o.Ship(); o.Deliver() },
checks: checkOrderConfirm(checkConfirmError("cannot confirm order in status: delivered")),
},
{
name: "already cancelled",
id: "ORD-004",
amount: 75.0,
before: func(o *Order) { o.Cancel() },
checks: checkOrderConfirm(checkConfirmError("cannot confirm order in status: cancelled")),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
s := NewOrder(tt.id, tt.amount)
if tt.before != nil {
tt.before(s)
}
err := s.Confirm()
for _, c := range tt.checks {
c(t, s, err)
}
})
}
}
Scaffold¶
Generate test scaffolding with go-testgen:
Testing Approach¶
Constant-based subtest naming:
- Cross-test consistency — the constant
orderID_001is used inTestOrder_Confirm,TestOrder_Cancel, and could appear inTestOrder_ShipandTestOrder_Deliver. If the ID format ever changes, update one constant. - Dual purpose — a constant serves as both a test value (the order ID) and a subtest name (in
TestOrder_Cancel). This links the value to its test case at a glance. - Domain vocabulary — constants like
StatusPending,StatusConfirmed,StatusShipped,StatusDelivered,StatusCancelleddocument the order lifecycle state machine. Tests read as: "in status Shipped, Cancel should succeed". - Subtest naming as documentation — when
t.Run(orderID_001, ...)runs, it prints=== RUN TestOrder_Cancel/ORD-001. The output immediately tells you which order was being tested. - Combined styles — the example mixes constant-based names (
orderID_001) with descriptive strings ("already delivered"). Use constants for values that appear across tests, descriptive strings for one-off test scenarios.
View source code on GitHub