From ffcb77e7dc16abf54e97d74b1ed4c74b0cc35637 Mon Sep 17 00:00:00 2001 From: Erik Nilsen Date: Thu, 22 Oct 2026 07:13:01 -0700 Subject: [PATCH] feat: add objectidtest for deterministic IDs in tests Signed-off-by: Erik Nilsen --- README.md | 10 +++++ objectidtest/example_test.go | 16 ++++++++ objectidtest/objectidtest.go | 32 ++++++++++++++++ objectidtest/objectidtest_test.go | 62 +++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 objectidtest/example_test.go create mode 100644 objectidtest/objectidtest.go create mode 100644 objectidtest/objectidtest_test.go diff --git a/README.md b/README.md index a5a898a..27a0953 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,16 @@ A value without one of the type's prefixes fails with a `*WrongPrefixError`, whi `FromUUID[T](u)` builds an ID from a `uuid.UUID`, and `id.UUID()` returns the bare value as a `uuid.UUID`. `Scan` and `DecodeSpanner` also accept `uuid.UUID` and `[16]byte`, for drivers and columns that carry native UUIDs. These need `StorageFormat: FormatDropPrefix`, because a native UUID has no prefix. +### Deterministic IDs in tests + +Package `objectidtest` gives a test the same valid ID for the same seed on every run: + +```go +buyer := objectidtest.FromSeed[fanType]("buyer") // fan_ +``` + +`FromSeedIn` takes another UUIDv5 namespace, to match IDs that another service's test helpers derive. + ## Emitted forms | Method | Emits | Controlled by | diff --git a/objectidtest/example_test.go b/objectidtest/example_test.go new file mode 100644 index 0000000..3241e6f --- /dev/null +++ b/objectidtest/example_test.go @@ -0,0 +1,16 @@ +package objectidtest_test + +import ( + "fmt" + + "github.com/TrogonStack/trogonobjectid/objectidtest" +) + +func ExampleFromSeed() { + buyer := objectidtest.FromSeed[fanType]("buyer") + fmt.Println(buyer == objectidtest.FromSeed[fanType]("buyer")) + fmt.Println(buyer.Prefix()) + // Output: + // true + // fan_ +} diff --git a/objectidtest/objectidtest.go b/objectidtest/objectidtest.go new file mode 100644 index 0000000..16537c5 --- /dev/null +++ b/objectidtest/objectidtest.go @@ -0,0 +1,32 @@ +// Package objectidtest builds deterministic object IDs for tests, so a test can name +// "the buyer" or "the seller" and get the same valid ID on every run: +// +// buyer := objectidtest.FromSeed[fanType]("buyer") +// +// The bare value is a UUIDv5 of the seed, so it passes ValidateUUID. +package objectidtest + +import ( + "github.com/google/uuid" + + "github.com/TrogonStack/trogonobjectid" +) + +// Namespace is the UUIDv5 namespace that FromSeed uses. +var Namespace = uuid.NewSHA1(uuid.NameSpaceURL, []byte("https://github.com/TrogonStack/trogonobjectid/objectidtest")) + +// FromSeed returns the ID whose bare value is the UUIDv5 of seed in Namespace. It +// panics if the type rejects the value, because a test fixture must be valid. +func FromSeed[T trogonobjectid.Definer](seed string) trogonobjectid.ID[T] { + return FromSeedIn[T](Namespace, seed) +} + +// FromSeedIn is like FromSeed in another namespace, for matching IDs that another +// service's test helpers derive. +func FromSeedIn[T trogonobjectid.Definer](namespace uuid.UUID, seed string) trogonobjectid.ID[T] { + id, err := trogonobjectid.FromUUID[T](uuid.NewSHA1(namespace, []byte(seed))) + if err != nil { + panic(err) + } + return id +} diff --git a/objectidtest/objectidtest_test.go b/objectidtest/objectidtest_test.go new file mode 100644 index 0000000..f896690 --- /dev/null +++ b/objectidtest/objectidtest_test.go @@ -0,0 +1,62 @@ +package objectidtest_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/TrogonStack/trogonobjectid" + "github.com/TrogonStack/trogonobjectid/objectidtest" +) + +type fanType struct{} + +func (fanType) Definition() trogonobjectid.Definition { + return trogonobjectid.Definition{ObjectType: "fan", Validate: trogonobjectid.ValidateUUID} +} + +type shopType struct{} + +func (shopType) Definition() trogonobjectid.Definition { + return trogonobjectid.Definition{ObjectType: "shop", Validate: trogonobjectid.ValidateUUID} +} + +type seqType struct{} + +func (seqType) Definition() trogonobjectid.Definition { + return trogonobjectid.Definition{ObjectType: "seq", Validate: trogonobjectid.ValidateInteger} +} + +func TestFromSeed(t *testing.T) { + t.Run("the same seed gives the same ID", func(t *testing.T) { + assert.Equal(t, objectidtest.FromSeed[fanType]("buyer"), objectidtest.FromSeed[fanType]("buyer")) + }) + + t.Run("different seeds give different IDs", func(t *testing.T) { + assert.NotEqual(t, objectidtest.FromSeed[fanType]("buyer"), objectidtest.FromSeed[fanType]("seller")) + }) + + t.Run("the bare value is a UUIDv5 in Namespace", func(t *testing.T) { + got := objectidtest.FromSeed[fanType]("buyer") + assert.Equal(t, uuid.NewSHA1(objectidtest.Namespace, []byte("buyer")).String(), got.Bare()) + u, err := got.UUID() + require.NoError(t, err) + assert.Equal(t, uuid.Version(5), u.Version()) + }) + + t.Run("types share bare values for the same seed", func(t *testing.T) { + assert.Equal(t, objectidtest.FromSeed[fanType]("x").Bare(), objectidtest.FromSeed[shopType]("x").Bare()) + }) + + t.Run("panics for a type that rejects UUIDs", func(t *testing.T) { + assert.Panics(t, func() { objectidtest.FromSeed[seqType]("x") }) + }) +} + +func TestFromSeedIn(t *testing.T) { + namespace := uuid.MustParse("75211e1f-6d69-5d80-b91f-9e9c5c8aa697") + got := objectidtest.FromSeedIn[fanType](namespace, "buyer") + assert.Equal(t, uuid.NewSHA1(namespace, []byte("buyer")).String(), got.Bare()) +}