Repository: goadesign/goa
Pull request #3564 (2024-07-25 13:06:49)

Refactor how generated result types are handled
Make the generated result type root a global variable similar to the expression root. Remove the dependencies on the go-diff package, use testify instead.


Patch:
diff --git a/codegen/doc.go b/codegen/doc.go
index c58b5e0b..62f9a07b 100644
--- a/codegen/doc.go
+++ b/codegen/doc.go
@@ -6,7 +6,7 @@ In particular package codegen defines the data structure that represents a
 generated file (see File) which is composed of sections, each corresponding to a
 Go text template and accompanying data used to render the final code.
 
-THe package also include functions that can generate code that transforms a
+The package also includes functions that generate code to transform a
 given type into another (see GoTransform).
 */
 package codegen
diff --git a/dsl/http.go b/dsl/http.go
index 5a2ef937..58ef0236 100644
--- a/dsl/http.go
+++ b/dsl/http.go
@@ -962,7 +962,7 @@ func Body(args ...any) {
 		if rt, ok := attr.Type.(*expr.ResultTypeExpr); ok && expr.IsArray(rt.Type) {
 			// If the attribute type is a result type collection add the type to the
 			// GeneratedTypes so that the type's DSLFunc is executed.
-			*expr.Root.GeneratedTypes = append(*expr.Root.GeneratedTypes, rt)
+			expr.GeneratedResultTypes.Append(rt)
 		}
 		if len(args) > 1 {
 			var ok bool
diff --git a/dsl/result_type.go b/dsl/result_type.go
index 796ed532..21d9245d 100644
--- a/dsl/result_type.go
+++ b/dsl/result_type.go
@@ -106,19 +106,19 @@ func ResultType(identifier string, args ...any) *expr.ResultTypeExpr {
 	}
 	canonicalID := expr.CanonicalIdentifier(identifier)
 	// Validate that result type identifier doesn't clash
-	for _, rt := range expr.Root.ResultTypes {
-		if re := rt.(*expr.ResultTypeExpr); re.Identifier == canonicalID {
+	for _, rt := range *expr.GeneratedResultTypes {
+		if rt.Identifier == canonicalID {
 			eval.ReportError(
 				"result type %#v with canonical identifier %#v is defined twice",
 				identifier, canonicalID)
 			return nil
 		}
 	}
-	// Now save the type in the API result types map
-	mt := expr.NewResultTypeExpr(typeName, identifier, fn)
-	expr.Root.ResultTypes = append(expr.Root.ResultTypes, mt)
+	// Add the type to the generated types root for later evaluation.
+	rt := expr.NewResultTypeExpr(typeName, identifier, fn)
+	expr.Root.ResultTypes = append(expr.Root.ResultTypes, rt)
 
-	return mt
+	return rt
 }
 
 // TypeName makes it possible to set the Go struct name for a type or result
@@ -201,7 +201,7 @@ func View(name string, adsl ...func()) {
 	switch e := eval.Current().(type) {
 	case *expr.ResultTypeExpr:
 		if e.View(name) != nil {
-			eval.ReportError("multiple expressions for view %#v in result type %#v", name, e.TypeName)
+			eval.ReportError("view %q is defined multiple times in result type %q", name, e.TypeName)
 			return
 		}
 		at := &expr.AttributeExpr{}
@@ -340,11 +340,11 @@ func CollectionOf(v any, adsl ...func()) *expr.ResultTypeExpr {
 	}
 	id = mime.FormatMediaType(rtype, params)
 	canonical := expr.CanonicalIdentifier(id)
-	if mt := expr.Root.GeneratedResultType(canonical); mt != nil {
+	if mt := expr.GeneratedResultType(canonical); mt != nil {
 		// Already have a type for this collection, reuse it.
 		return mt
 	}
-	mt := expr.NewResultTypeExpr("", id, func() {
+	rt := expr.NewResultTypeExpr("", id, func() {
 		rt, ok := eval.Current().(*expr.ResultTypeExpr)
 		if !ok {
 			eval.IncompatibleDSL()
@@ -371,8 +371,8 @@ func CollectionOf(v any, adsl ...func()) *expr.ResultTypeExpr {
 	})
 	// do not execute the DSL right away, will be done last to make sure
 	// the element DSL has run first.
-	*expr.Root.GeneratedTypes = append(*expr.Root.GeneratedTypes, mt)
-	return mt
+	expr.GeneratedResultTypes.Append(rt)
+	return rt
 }
 
 // Reference sets a type or result type reference. The value itself can be a
diff --git a/expr/dup.go b/expr/dup.go
index 4d432e5c..f00641ff 100644
--- a/expr/dup.go
+++ b/expr/dup.go
@@ -1,6 +1,8 @@
 package expr
 
-import "fmt"
+import (
+	"fmt"
+)
 
 // Dup creates a copy the given data type.
 func Dup(d DataType) DataType {
@@ -103,8 +105,8 @@ func (d *dupper) DupType(t DataType) DataType {
 		// the dup in the generated type list so that it gets properly
 		// eval'd.
 		if rt, ok := dp.(*ResultTypeExpr); ok {
-			if Root.GeneratedResultType(rt.Identifier) != nil {
-				*Root.GeneratedTypes = append(*Root.GeneratedTypes, dp.(*ResultTypeExpr))
+			if GeneratedResultType(rt.Identifier) != nil {
+				GeneratedResultTypes.Append(rt)
 			}
 		}
 
diff --git a/expr/generated.go b/expr/generated.go
deleted file mode 100644
index e0bad4fd..00000000
--- a/expr/generated.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package expr
-
-import "goa.design/goa/v3/eval"
-
-type (
-	// GeneratedRoot records the generated result types and is a DSL root
-	// evaluated after Root.
-	GeneratedRoot []UserType
-)
-
-// EvalName is the name of the expression used by eval.
-func (*GeneratedRoot) EvalName() string {
-	return "generated result types"
-}
-
-// WalkSets returns the generated result types for evaluation.
-func (r *GeneratedRoot) WalkSets(w eval.SetWalker) {
-	if r == nil {
-		return
-	}
-	set := make(eval.ExpressionSet, len(*r))
-	for i, t := range *r {
-		rt := t.(*ResultTypeExpr)
-		Root.ResultTypes = append(Root.ResultTypes, rt)
-		set[i] = rt
-	}
-	w(set)
-}
-
-// DependsOn ensures that Root executes first.
-func (*GeneratedRoot) DependsOn() []eval.Root {
-	return []eval.Root{Root}
-}
-
-// Packages returns the Go import path to this and the dsl packages.
-func (*GeneratedRoot) Packages() []string {
-	return []string{
-		"goa.design/goa/v3/expr",
-		"goa.design/goa/v3/dsl",
-	}
-}
diff --git a/expr/init.go b/expr/init.go
index 7b6d626a..cfa3305c 100644
--- a/expr/init.go
+++ b/expr/init.go
@@ -9,7 +9,7 @@ func init() {
 	if err := eval.Register(Root); err != nil {
 		panic(err) // bug
 	}
-	if err := eval.Register(Root.GeneratedTypes); err != nil {
+	if err := eval.Register(GeneratedResultTypes); err != nil {
 		panic(err) // bug
 	}
 }
diff --git a/expr/result_types_root.go b/expr/result_types_root.go
new file mode 100644
index 00000000..e4f49760
--- /dev/null
+++ b/expr/result_types_root.go
@@ -0,0 +1,66 @@
+package expr
+
+import (
+	"goa.design/goa/v3/eval"
+)
+
+type (
+	// ResultTypesRoot is a DSL root that records the result types
+	// generated by the DSL.
+	ResultTypesRoot []*ResultTypeExpr
+)
+
+// GeneratedResultTypes records the generated result types and is a DSL
+// root evaluated after Root.
+var GeneratedResultTypes = new(ResultTypesRoot)
+
+// GeneratedResultType returns the generated result type expression with the given
+// id, nil if there isn't one.
+func GeneratedResultType(id string) *ResultTypeExpr {
+	for _, rt := range *GeneratedResultTypes {
+		if rt.Identifier == id {
+			return rt
+		}
+	}
+	return nil
+}
+
+// Append adds the given type to the generated result types if not already
+// added. It returns the type that was added or already present.
+func (r *ResultTypesRoot) Append(t *ResultTypeExpr) {
+	*r = append(*r, t)
+}
+
+// EvalName is the name of the expression used by eval.
+func (*ResultTypesRoot) EvalName() string {
+	return "generated result types"
+}
+
+// WalkSets returns the generated result types for evaluation.
+func (r *ResultTypesRoot) WalkSets(w eval.SetWalker) {
+	set := make(eval.ExpressionSet, len(*r))
+	for i, rt := range *r {
+		set[i] = rt
+
+		// This is a bit tricky: the DSL evaluation starts by running the expression DSLs.
+		// In this first call to WalkSets we want to initialize the root expression
+		// result types with the generated result types so that in future passes (prepare,
+		// validate, etc.) the generated result types are walked when the root expression
+		// is walked (see the root expression WalkSets implementation).
+		Root.ResultTypes = append(Root.ResultTypes, rt)
+	}
+	w(set)
+}
+
+// DependsOn ensures that Root executes first.
+func (*ResultTypesRoot) DependsOn() []eval.Root {
+	return []eval.Root{Root}
+}
+
+// Packages returns the Go import path to this and the dsl packages.
+func (*ResultTypesRoot) Packages() []string {
+	return []string{
+		"goa.design/goa/v3/expr",
+		"goa.design/goa/v3/dsl",
+	}
+}
diff --git a/expr/root.go b/expr/root.go
index 6cc0a74a..0240e54e 100644
--- a/expr/root.go
+++ b/expr/root.go
@@ -9,7 +9,7 @@ import (
 )
 
 // Root is the root object built by the DSL.
-var Root = &RootExpr{GeneratedTypes: &GeneratedRoot{}}
+var Root = new(RootExpr)
 
 type (
 	// RootExpr is the struct built by the DSL on process start.
@@ -23,11 +23,9 @@ type (
 		Errors []*ErrorExpr
 		// Types contains the user types described in the DSL.
 		Types []UserType
-		// ResultTypes contains the result types described in the DSL.
-		ResultTypes []UserType
-		// GeneratedTypes contains the types generated during DSL
+		// ResultTypes contains the result types generated during DSL
 		// execution.
-		GeneratedTypes *GeneratedRoot
+		ResultTypes []*ResultTypeExpr
 		// Conversions list the user type to external type mappings.
 		Conversions []*TypeMap
 		// Creations list the external type to user type mappings.
@@ -72,12 +70,14 @@ func (r *RootExpr) WalkSets(walk eval.SetWalker) {
 	}
 	walk(types)
 
-	// Result types
-	mtypes := make(eval.ExpressionSet, len(r.ResultTypes))
-	for i, mt := range r.ResultTypes {
-		mtypes[i] = mt.(*ResultTypeExpr)
+	// Result types, note: initialized when the generated result types are
+	// walked so empty the first time around (in the first pass DSL
+	// execution). See ResultTypesRoot.WalkSets for details.
+	rtypes := make(eval.ExpressionSet, len(r.ResultTypes))
+	for i, rt := range r.ResultTypes {
+		rtypes[i] = rt
 	}
-	walk(mtypes)
+	walk(rtypes)
 
 	// Services
 	walk(eval.ToExpressionSet(r.Services))
@@ -157,18 +157,6 @@ func (r *RootExpr) UserType(name string) UserType {
 	return nil
 }
 
-// GeneratedResultType returns the generated result type expression with the given
-// id, nil if there isn't one.
-func (r *RootExpr) GeneratedResultType(id string) *ResultTypeExpr {
-	for _, t := range *r.GeneratedTypes {
-		mt := t.(*ResultTypeExpr)
-		if mt.Identifier == id {
-			return mt
-		}
-	}
-	return nil
-}
-
 // Service returns the service with the given name.
 func (r *RootExpr) Service(name string) *ServiceExpr {
 	for _, s := range r.Services {
diff --git a/go.mod b/go.mod
index 434a4ec6..c8f26cd0 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,6 @@ require (
 	github.com/gorilla/websocket v1.5.3
 	github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
 	github.com/pkg/errors v0.9.1
-	github.com/sergi/go-diff v1.3.1
 	github.com/stretchr/testify v1.9.0
 	golang.org/x/text v0.16.0
 	golang.org/x/tools v0.23.0
@@ -34,5 +33,5 @@ require (
 	golang.org/x/net v0.27.0 // indirect
 	golang.org/x/sync v0.7.0 // indirect
 	golang.org/x/sys v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect
 )
diff --git a/go.sum b/go.sum
index 13c7d4d5..9ad6ddb1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,3 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI=
@@ -23,11 +22,8 @@ github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
 github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -46,10 +42,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
-github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
-github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
@@ -66,17 +58,14 @@ golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
 golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
 google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
 google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
 google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
 google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


Test patch:
diff --git a/codegen/go_transform_helpers_test.go b/codegen/go_transform_helpers_test.go
index ecd57dd6..4ce41949 100644
--- a/codegen/go_transform_helpers_test.go
+++ b/codegen/go_transform_helpers_test.go
@@ -3,6 +3,9 @@ package codegen
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen/testdata"
 	"goa.design/goa/v3/expr"
 )
@@ -29,40 +32,27 @@ func TestGoTransformHelpers(t *testing.T) {
 		Type        expr.DataType
 		HelperNames []string
 	}{
-		{"simple", simple, []string{}},
+		{"simple", simple, nil},
 		{"recursive", recursive, []string{"transformRecursiveToRecursive"}},
 		{"composite", composite, []string{"transformSimpleToSimple"}},
 		{"deep", deep, []string{"transformCompositeToComposite", "transformSimpleToSimple"}},
 		{"deep-array", deepArray, []string{"transformCompositeToComposite", "transformSimpleToSimple"}},
-		{"simple-alias", simpleAlias, []string{}},
-		{"nested-map-alias", mapAlias, []string{}},
-		{"array-map-alias", arrayMapAlias, []string{}},
+		{"simple-alias", simpleAlias, nil},
+		{"nested-map-alias", mapAlias, nil},
+		{"array-map-alias", arrayMapAlias, nil},
 		{"result-type-collection", collection, []string{"transformResultTypeToResultType"}},
 	}
 	for _, c := range tc {
 		t.Run(c.Name, func(t *testing.T) {
-			if c.Type == nil {
-				t.Fatal("source type not found in testdata")
-			}
+			require.NotNil(t, c.Type, "source type not found in testdata")
 			_, funcs, err := GoTransform(&expr.AttributeExpr{Type: c.Type}, &expr.AttributeExpr{Type: c.Type}, "source", "target", defaultCtx, defaultCtx, "", true)
-			if err != nil {
-				t.Fatal(err)
-			}
-			if len(funcs) != len(c.HelperNames) {
-				t.Errorf("invalid helpers count, got: %d, expected %d", len(funcs), len(c.HelperNames))
-			} else {
-				var diffs []string
-				actual := make([]string, len(funcs))
-				for i, f := range funcs {
-					actual[i] = f.Name
-					if c.HelperNames[i] != f.Name {
-						diffs = append(diffs, f.Name)
-					}
-				}
-				if len(diffs) > 0 {
-					t.Errorf("invalid helper names, got: %v, expected: %v", actual, c.HelperNames)
-				}
+			require.NoError(t, err)
+			assert.Equal(t, len(c.HelperNames), len(funcs), "invalid helpers count")
+			var actual []string
+			for _, f := range funcs {
+				actual = append(actual, f.Name)
 			}
+			assert.Equal(t, c.HelperNames, actual, "invalid helper names")
 		})
 	}
 }
diff --git a/codegen/go_transform_test.go b/codegen/go_transform_test.go
index 9ad3c268..ca380d10 100644
--- a/codegen/go_transform_test.go
+++ b/codegen/go_transform_test.go
@@ -3,6 +3,9 @@ package codegen
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen/testdata"
 	"goa.design/goa/v3/expr"
 )
@@ -182,20 +185,12 @@ func TestGoTransform(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			for _, c := range cases {
 				t.Run(c.Name, func(t *testing.T) {
-					if c.Source == nil {
-						t.Fatal("source type not found in testdata")
-					}
-					if c.Target == nil {
-						t.Fatal("target type not found in testdata")
-					}
+					require.NotNil(t, c.Source)
+					require.NotNil(t, c.Target)
 					code, _, err := GoTransform(&expr.AttributeExpr{Type: c.Source}, &expr.AttributeExpr{Type: c.Target}, "source", "target", c.SourceCtx, c.TargetCtx, "", true)
-					if err != nil {
-						t.Fatal(err)
-					}
+					require.NoError(t, err)
 					code = FormatTestCode(t, "package foo\nfunc transform(){\n"+code+"}")
-					if code != c.Code {
-						t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, Diff(t, code, c.Code))
-					}
+					assert.Equal(t, c.Code, code)
 				})
 			}
 		})
diff --git a/codegen/go_transform_union_test.go b/codegen/go_transform_union_test.go
index 4d02aec1..3e01bd0a 100644
--- a/codegen/go_transform_union_test.go
+++ b/codegen/go_transform_union_test.go
@@ -3,6 +3,9 @@ package codegen
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen/testdata"
 	"goa.design/goa/v3/expr"
 )
@@ -41,14 +44,9 @@ func TestGoTransformUnion(t *testing.T) {
 	for _, c := range tc {
 		t.Run(c.Name, func(t *testing.T) {
 			code, _, err := GoTransform(c.Source, c.Target, "source", "target", defaultCtx, defaultCtx, "", true)
-			if err != nil {
-				t.Errorf("unexpected error %s", err)
-				return
-			}
+			require.NoError(t, err)
 			code = FormatTestCode(t, "package foo\nfunc transform(){\n"+code+"}")
-			if code != c.Expected {
-				t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, Diff(t, code, c.Expected))
-			}
+			assert.Equal(t, c.Expected, code)
 		})
 	}
 }
diff --git a/codegen/service/convert_test.go b/codegen/service/convert_test.go
index 85431169..9edfdbc3 100644
--- a/codegen/service/convert_test.go
+++ b/codegen/service/convert_test.go
@@ -261,8 +261,9 @@ func runDSL(t *testing.T, dsl func()) *expr.RootExpr {
 	Services = make(ServicesData)
 	eval.Reset()
 	expr.Root = new(expr.RootExpr)
-	err := eval.Register(expr.Root)
-	require.NoError(t, err)
+	expr.GeneratedResultTypes = new(expr.ResultTypesRoot)
+	require.NoError(t, eval.Register(expr.Root))
+	require.NoError(t, eval.Register(expr.GeneratedResultTypes))
 	expr.Root.API = expr.NewAPIExpr("test api", func() {})
 	expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()}
 
@@ -270,8 +271,7 @@ func runDSL(t *testing.T, dsl func()) *expr.RootExpr {
 	require.True(t, eval.Execute(dsl, nil))
 
 	// run DSL (second pass)
-	err = eval.RunDSL()
-	require.NoError(t, err)
+	require.NoError(t, eval.RunDSL())
 
 	// return generated root
 	return expr.Root
diff --git a/codegen/service/endpoint_test.go b/codegen/service/endpoint_test.go
index f1f5597d..0356bf95 100644
--- a/codegen/service/endpoint_test.go
+++ b/codegen/service/endpoint_test.go
@@ -38,7 +38,6 @@ func TestEndpoint(t *testing.T) {
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
 			codegen.RunDSL(t, c.DSL)
-			expr.Root.GeneratedTypes = &expr.GeneratedRoot{}
 			require.Len(t, expr.Root.Services, 1)
 			fs := EndpointFile("goa.design/goa/example", expr.Root.Services[0])
 			require.NotNil(t, fs)
diff --git a/codegen/service/example_svc_test.go b/codegen/service/example_svc_test.go
index 2f75e262..a45b600a 100644
--- a/codegen/service/example_svc_test.go
+++ b/codegen/service/example_svc_test.go
@@ -33,7 +33,6 @@ func TestExampleServiceFiles(t *testing.T) {
 		for _, c := range cases {
 			t.Run(c.Name, func(t *testing.T) {
 				codegen.RunDSL(t, c.DSL)
-				expr.Root.GeneratedTypes = &expr.GeneratedRoot{}
 				require.Len(t, expr.Root.Services, 3)
 				fs := ExampleServiceFiles("", expr.Root)
 				require.Len(t, fs, 3)
diff --git a/codegen/testing.go b/codegen/testing.go
index 058447b3..5a4c0a26 100644
--- a/codegen/testing.go
+++ b/codegen/testing.go
@@ -4,14 +4,12 @@ import (
 	"bytes"
 	"fmt"
 	"os"
-	"os/exec"
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/require"
 	"goa.design/goa/v3/eval"
 	"goa.design/goa/v3/expr"
-
-	"github.com/sergi/go-diff/diffmatchpatch"
 )
 
 // RunDSL returns the DSL root resulting from running the given DSL.
@@ -19,21 +17,13 @@ func RunDSL(t *testing.T, dsl func()) *expr.RootExpr {
 	t.Helper()
 	eval.Reset()
 	expr.Root = new(expr.RootExpr)
-	expr.Root.GeneratedTypes = &expr.GeneratedRoot{}
-	if err := eval.Register(expr.Root); err != nil {
-		t.Fatal(err)
-	}
-	if err := eval.Register(expr.Root.GeneratedTypes); err != nil {
-		t.Fatal(err)
-	}
+	expr.GeneratedResultTypes = new(expr.ResultTypesRoot)
+	require.NoError(t, eval.Register(expr.Root))
+	require.NoError(t, eval.Register(expr.GeneratedResultTypes))
 	expr.Root.API = expr.NewAPIExpr("test api", func() {})
 	expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()}
-	if !eval.Execute(dsl, nil) {
-		t.Fatal(eval.Context.Error())
-	}
-	if err := eval.RunDSL(); err != nil {
-		t.Fatal(err)
-	}
+	require.True(t, eval.Execute(dsl, nil), eval.Context.Error())
+	require.NoError(t, eval.RunDSL())
 	return expr.Root
 }
 
@@ -52,22 +42,16 @@ func SectionsCode(t *testing.T, sections []*SectionTemplate) string {
 }
 
 // SectionCodeFromImportsAndMethods generates and formats the code for given import and method definition sections.
-func SectionCodeFromImportsAndMethods(t *testing.T, importSection *SectionTemplate, methodSection *SectionTemplate) string {
+func SectionCodeFromImportsAndMethods(t *testing.T, importSection, methodSection *SectionTemplate) string {
 	t.Helper()
 	var code bytes.Buffer
-	if err := importSection.Write(&code); err != nil {
-		t.Fatal(err)
-	}
-
+	require.NoError(t, importSection.Write(&code))
 	return sectionCodeWithPrefix(t, methodSection, code.String())
 }
 
 func sectionCodeWithPrefix(t *testing.T, section *SectionTemplate, prefix string) string {
 	var code bytes.Buffer
-	if err := section.Write(&code); err != nil {
-		t.Fatal(err)
-	}
-
+	require.NoError(t, section.Write(&code))
 	codestr := code.String()
 
 	if len(prefix) > 0 {
@@ -83,50 +67,23 @@ func FormatTestCode(t *testing.T, code string) string {
 	t.Helper()
 	tmp := CreateTempFile(t, code)
 	defer os.Remove(tmp)
-	if err := finalizeGoSource(tmp); err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, finalizeGoSource(tmp))
 	content, err := os.ReadFile(tmp)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	return strings.Join(strings.Split(string(content), "\n")[2:], "\n")
 }
 
-// Diff returns a diff between s1 and s2. It uses the diff tool if installed
-// otherwise degrades to using the dmp package.
-func Diff(t *testing.T, s1, s2 string) string {
-	_, err := exec.LookPath("diff")
-	supportsDiff := (err == nil)
-	if !supportsDiff {
-		dmp := diffmatchpatch.New()
-		diffs := dmp.DiffMain(s1, s2, false)
-		return dmp.DiffPrettyText(diffs)
-	}
-	left := CreateTempFile(t, s1)
-	right := CreateTempFile(t, s2)
-	defer os.Remove(left)
-	defer os.Remove(right)
-	cmd := exec.Command("diff", left, right)
-	diffb, _ := cmd.CombinedOutput()
-	return strings.ReplaceAll(string(diffb), "\t", " ␉ ")
-}
-
 // CreateTempFile creates a temporary file and writes the given content.
 // It is used only for testing.
 func CreateTempFile(t *testing.T, content string) string {
 	t.Helper()
 	f, err := os.CreateTemp("", "")
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	_, err = f.WriteString(content)
 	if err != nil {
 		os.Remove(f.Name())
 		t.Fatal(err)
 	}
-	if err := f.Close(); err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, f.Close())
 	return f.Name()
 }
diff --git a/codegen/validation_test.go b/codegen/validation_test.go
index 84b99140..73617820 100644
--- a/codegen/validation_test.go
+++ b/codegen/validation_test.go
@@ -3,6 +3,8 @@ package codegen
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	"goa.design/goa/v3/codegen/testdata"
 	"goa.design/goa/v3/expr"
 )
@@ -66,9 +68,7 @@ func TestRecursiveValidationCode(t *testing.T) {
 			ctx := NewAttributeContext(c.Pointer, false, c.UseDefault, "", scope)
 			code := ValidationCode(&expr.AttributeExpr{Type: c.Type}, nil, ctx, c.Required, expr.IsAlias(c.Type), false, "target")
 			code = FormatTestCode(t, "package foo\nfunc Validate() (err error){\n"+code+"}")
-			if code != c.Code {
-				t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 	// Special case of unions with views
@@ -76,8 +76,6 @@ func TestRecursiveValidationCode(t *testing.T) {
 		ctx := NewAttributeContext(false, false, false, "", scope)
 		code := ValidationCode(&expr.AttributeExpr{Type: unionT}, nil, ctx, true, false, true, "target")
 		code = FormatTestCode(t, "package foo\nfunc Validate() (err error){\n"+code+"}")
-		if code != testdata.UnionWithViewValidationCode {
-			t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, Diff(t, code, testdata.UnionWithViewValidationCode))
-		}
+		assert.Equal(t, testdata.UnionWithViewValidationCode, code)
 	})
 }
diff --git a/dsl/result_type_test.go b/dsl/result_type_test.go
index 377f2ab8..61a941af 100644
--- a/dsl/result_type_test.go
+++ b/dsl/result_type_test.go
@@ -55,7 +55,7 @@ func TestView(t *testing.T) {
 		{"view", baseRT, viewDSL, []string{viewName}, map[string][]string{viewName: {"att"}}, ""},
 		{"view2", baseRT, view2DSL, []string{view2Name}, map[string][]string{view2Name: {"att2"}}, ""},
 		{"all views", baseRT, allViewsDSL, []string{viewName, view2Name}, map[string][]string{viewName: {"att"}, view2Name: {"att2"}}, ""},
-		{"duplicate view", baseRT, func() { viewDSL(); viewDSL() }, nil, nil, `[result_type_test.go:29] multiple expressions for view "test" in result type "test" in attribute`},
+		{"duplicate view", baseRT, func() { viewDSL(); viewDSL() }, nil, nil, `[result_type_test.go:29] view "test" is defined multiple times in result type "test" in attribute`},
 	}
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
diff --git a/expr/method_test.go b/expr/method_test.go
index a3c5ca41..bd2bef86 100644
--- a/expr/method_test.go
+++ b/expr/method_test.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/expr/testdata"
 )
@@ -39,9 +41,7 @@ service "AnotherInvalidSecuritySchemesService" method "Method": payload of metho
 				expr.RunDSL(t, tc.DSL)
 			} else {
 				err := expr.RunInvalidDSL(t, tc.DSL)
-				if tc.Error != err.Error() {
-					t.Errorf("invalid error:\ngot:\n%s\n\ngot vs expected:\n%s", err.Error(), expr.Diff(t, err.Error(), tc.Error))
-				}
+				assert.EqualError(t, err, tc.Error)
 			}
 		})
 	}
diff --git a/expr/service_test.go b/expr/service_test.go
index dfce8c6e..6cf39903 100644
--- a/expr/service_test.go
+++ b/expr/service_test.go
@@ -3,6 +3,8 @@ package expr_test
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/expr/testdata"
 )
@@ -99,9 +101,7 @@ func TestServiceExprValidate(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.Name, func(t *testing.T) {
 			err := expr.RunInvalidDSL(t, tc.DSL)
-			if tc.Error != err.Error() {
-				t.Errorf("invalid error:\ngot:\n%s\n\ngot vs expected:\n%s", err.Error(), expr.Diff(t, err.Error(), tc.Error))
-			}
+			assert.EqualError(t, err, tc.Error)
 		})
 	}
 }
@@ -128,9 +128,7 @@ attribute: type "ErrorType" is used to define multiple errors and must identify
 				expr.RunDSL(t, tc.DSL)
 			} else {
 				err := expr.RunInvalidDSL(t, tc.DSL)
-				if tc.Error != err.Error() {
-					t.Errorf("invalid error:\ngot:\n%s\n\ngot vs expected:\n%s", err.Error(), expr.Diff(t, err.Error(), tc.Error))
-				}
+				assert.EqualError(t, err, tc.Error)
 			}
 		})
 	}
diff --git a/expr/testing.go b/expr/testing.go
index 609cd18f..fce810a1 100644
--- a/expr/testing.go
+++ b/expr/testing.go
@@ -2,11 +2,10 @@ package expr
 
 import (
 	"os"
-	"os/exec"
-	"strings"
 	"testing"
 
-	"github.com/sergi/go-diff/diffmatchpatch"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/eval"
 )
 
@@ -14,17 +13,13 @@ import (
 // Used only in tests.
 func RunDSL(t *testing.T, dsl func()) *RootExpr {
 	t.Helper()
-	setupDSLRun()
+	setupDSLRun(t)
 
 	// run DSL (first pass)
-	if !eval.Execute(dsl, nil) {
-		t.Fatal(eval.Context.Error())
-	}
+	require.True(t, eval.Execute(dsl, nil), eval.Context.Error())
 
 	// run DSL (second pass)
-	if err := eval.RunDSL(); err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, eval.RunDSL())
 
 	// return generated root
 	return Root
@@ -34,7 +29,7 @@ func RunDSL(t *testing.T, dsl func()) *RootExpr {
 // It is used only in tests.
 func RunInvalidDSL(t *testing.T, dsl func()) error {
 	t.Helper()
-	setupDSLRun()
+	setupDSLRun(t)
 
 	// run DSL (first pass)
 	if !eval.Execute(dsl, nil) {
@@ -52,25 +47,6 @@ func RunInvalidDSL(t *testing.T, dsl func()) error {
 	return nil
 }
 
-// Diff returns a diff between s1 and s2. It uses the diff tool if installed
-// otherwise degrades to using the dmp package.
-func Diff(t *testing.T, s1, s2 string) string {
-	_, err := exec.LookPath("diff")
-	supportsDiff := (err == nil)
-	if !supportsDiff {
-		dmp := diffmatchpatch.New()
-		diffs := dmp.DiffMain(s1, s2, false)
-		return dmp.DiffPrettyText(diffs)
-	}
-	left := CreateTempFile(t, s1)
-	right := CreateTempFile(t, s2)
-	defer os.Remove(left)
-	defer os.Remove(right)
-	cmd := exec.Command("diff", left, right)
-	diffb, _ := cmd.CombinedOutput()
-	return strings.ReplaceAll(string(diffb), "\t", " ␉ ")
-}
-
 // CreateTempFile creates a temporary file and writes the given content.
 // It is used only for testing.
 func CreateTempFile(t *testing.T, content string) string {
@@ -90,17 +66,13 @@ func CreateTempFile(t *testing.T, content string) string {
 	return f.Name()
 }
 
-func setupDSLRun() {
+func setupDSLRun(t *testing.T) {
 	// reset all roots and codegen data structures
 	eval.Reset()
 	Root = new(RootExpr)
-	Root.GeneratedTypes = &GeneratedRoot{}
-	if err := eval.Register(Root); err != nil {
-		panic(err)
-	}
-	if err := eval.Register(Root.GeneratedTypes); err != nil {
-		panic(err)
-	}
+	GeneratedResultTypes = new(ResultTypesRoot)
+	require.NoError(t, eval.Register(Root))
+	require.NoError(t, eval.Register(GeneratedResultTypes))
 	Root.API = NewAPIExpr("test api", func() {})
 	Root.API.Servers = []*ServerExpr{Root.API.DefaultServer()}
 }
diff --git a/grpc/codegen/client_cli_test.go b/grpc/codegen/client_cli_test.go
index 1199ac41..35bae4f4 100644
--- a/grpc/codegen/client_cli_test.go
+++ b/grpc/codegen/client_cli_test.go
@@ -4,6 +4,9 @@ import (
 	"bytes"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/grpc/codegen/testdata"
@@ -23,22 +26,14 @@ func TestClientCLIFiles(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ClientCLIFiles("", expr.Root)
-			if len(fs) == 0 {
-				t.Fatalf("got 0 files, expected 1")
-			}
-			if len(fs[1].SectionTemplates) == 0 {
-				t.Fatalf("got 0 sections, expected at least 1")
-			}
+			require.Greater(t, len(fs), 1, "expected at least 2 files")
+			require.NotEmpty(t, fs[1].SectionTemplates)
 			var buf bytes.Buffer
 			for _, s := range fs[1].SectionTemplates {
-				if err := s.Write(&buf); err != nil {
-					t.Fatal(err)
-				}
+				require.NoError(t, s.Write(&buf))
 			}
 			code := codegen.FormatTestCode(t, buf.String())
-			if code != c.Code {
-				t.Errorf("invalid code: got\n%s\ngot vs. expected:\n%s", code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
diff --git a/grpc/codegen/client_test.go b/grpc/codegen/client_test.go
index 09bad865..93edbf23 100644
--- a/grpc/codegen/client_test.go
+++ b/grpc/codegen/client_test.go
@@ -3,6 +3,9 @@ package codegen
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/grpc/codegen/testdata"
@@ -31,17 +34,13 @@ func TestClientEndpointInit(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ClientFiles("", expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
-			}
+			require.Len(t, fs, 2)
 			sections := fs[0].Section("client-endpoint-init")
 			if len(sections) == 0 {
 				t.Fatalf("got zero sections, expected at least one")
 			}
 			code := codegen.SectionsCode(t, sections)
-			if code != c.Code {
-				t.Errorf("%s: got\n%s\ngot vs. expected:\n%s", c.Name, code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
@@ -66,17 +65,11 @@ func TestRequestEncoder(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ClientFiles("", expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
-			}
+			require.Len(t, fs, 2)
 			sections := fs[1].Section("request-encoder")
-			if len(sections) == 0 {
-				t.Fatalf("got zero sections, expected at least one")
-			}
+			require.NotEmpty(t, sections)
 			code := codegen.SectionsCode(t, sections)
-			if code != c.Code {
-				t.Errorf("%s: got\n%s\ngot vs. expected:\n%s", c.Name, code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
@@ -103,17 +96,11 @@ func TestResponseDecoder(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ClientFiles("", expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
-			}
+			require.Len(t, fs, 2)
 			sections := fs[1].Section("response-decoder")
-			if len(sections) == 0 {
-				t.Fatalf("got zero sections, expected at least one")
-			}
+			require.NotEmpty(t, sections)
 			code := codegen.SectionsCode(t, sections)
-			if code != c.Code {
-				t.Errorf("%s: got\n%s\ngot vs. expected:\n%s", c.Name, code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
diff --git a/grpc/codegen/client_types_test.go b/grpc/codegen/client_types_test.go
index d6ed0d54..549ef062 100644
--- a/grpc/codegen/client_types_test.go
+++ b/grpc/codegen/client_types_test.go
@@ -4,6 +4,9 @@ import (
 	"bytes"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/grpc/codegen/testdata"
@@ -30,19 +33,13 @@ func TestClientTypeFiles(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ClientTypeFiles("", expr.Root)
-			if len(fs) != 1 {
-				t.Fatalf("got %d files, expected one", len(fs))
-			}
+			require.Len(t, fs, 1)
 			var buf bytes.Buffer
 			for _, s := range fs[0].SectionTemplates[1:] {
-				if err := s.Write(&buf); err != nil {
-					t.Fatal(err)
-				}
+				require.NoError(t, s.Write(&buf))
 			}
 			code := codegen.FormatTestCode(t, "package foo\n"+buf.String())
-			if code != c.Code {
-				t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
diff --git a/grpc/codegen/protobuf_transform_test.go b/grpc/codegen/protobuf_transform_test.go
index 247f4f3f..3c6579bf 100644
--- a/grpc/codegen/protobuf_transform_test.go
+++ b/grpc/codegen/protobuf_transform_test.go
@@ -3,6 +3,9 @@ package codegen
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen"
 	ctestdata "goa.design/goa/v3/codegen/testdata"
 	"goa.design/goa/v3/expr"
@@ -172,13 +175,9 @@ func TestProtoBufTransform(t *testing.T) {
 						srcCtx = pbCtx
 					}
 					code, _, err := protoBufTransform(source, target, "source", "target", srcCtx, tgtCtx, c.ToProto, true)
-					if err != nil {
-						t.Fatal(err)
-					}
+					require.NoError(t, err)
 					code = codegen.FormatTestCode(t, "package foo\nfunc transform(){\n"+code+"}")
-					if code != c.Code {
-						t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, codegen.Diff(t, code, c.Code))
-					}
+					assert.Equal(t, c.Code, code)
 				})
 			}
 		})
diff --git a/grpc/codegen/server_test.go b/grpc/codegen/server_test.go
index a14fe9b1..83dac456 100644
--- a/grpc/codegen/server_test.go
+++ b/grpc/codegen/server_test.go
@@ -3,6 +3,9 @@ package codegen
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/grpc/codegen/testdata"
@@ -30,17 +33,11 @@ func TestServerGRPCInterface(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ServerFiles("", expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
-			}
+			require.Len(t, fs, 2)
 			sections := fs[0].Section("server-grpc-interface")
-			if len(sections) == 0 {
-				t.Fatalf("got zero sections, expected at least one")
-			}
+			require.NotEmpty(t, sections)
 			code := codegen.SectionsCode(t, sections)
-			if code != c.Code {
-				t.Errorf("%s: got\n%s\ngot vs. expected:\n%s", c.Name, code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
@@ -64,17 +61,11 @@ func TestServerHandlerInit(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ServerFiles("", expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
-			}
+			require.Len(t, fs, 2)
 			sections := fs[0].Section("grpc-handler-init")
-			if len(sections) == 0 {
-				t.Fatalf("got zero sections, expected at least one")
-			}
+			require.NotEmpty(t, sections)
 			code := codegen.SectionsCode(t, sections)
-			if code != c.Code {
-				t.Errorf("%s: got\n%s\ngot vs. expected:\n%s", c.Name, code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
@@ -99,17 +90,11 @@ func TestRequestDecoder(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ServerFiles("", expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
-			}
+			require.Len(t, fs, 2)
 			sections := fs[1].Section("request-decoder")
-			if len(sections) == 0 {
-				t.Fatalf("got zero sections, expected at least one")
-			}
+			require.NotEmpty(t, sections)
 			code := codegen.SectionsCode(t, sections)
-			if code != c.Code {
-				t.Errorf("%s: got\n%s\ngot vs. expected:\n%s", c.Name, code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
@@ -133,17 +118,11 @@ func TestResponseEncoder(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ServerFiles("", expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
-			}
+			require.Len(t, fs, 2)
 			sections := fs[1].Section("response-encoder")
-			if len(sections) == 0 {
-				t.Fatalf("got zero sections, expected at least one")
-			}
+			require.NotEmpty(t, sections)
 			code := codegen.SectionsCode(t, sections)
-			if code != c.Code {
-				t.Errorf("%s: got\n%s\ngot vs. expected:\n%s", c.Name, code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
diff --git a/grpc/codegen/server_types_test.go b/grpc/codegen/server_types_test.go
index f6e52d01..f557b1da 100644
--- a/grpc/codegen/server_types_test.go
+++ b/grpc/codegen/server_types_test.go
@@ -4,6 +4,9 @@ import (
 	"bytes"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/grpc/codegen/testdata"
@@ -32,19 +35,13 @@ func TestServerTypeFiles(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			RunGRPCDSL(t, c.DSL)
 			fs := ServerTypeFiles("", expr.Root)
-			if len(fs) != 1 {
-				t.Fatalf("got %d files, expected one", len(fs))
-			}
+			require.Len(t, fs, 1)
 			var buf bytes.Buffer
 			for _, s := range fs[0].SectionTemplates[1:] {
-				if err := s.Write(&buf); err != nil {
-					t.Fatal(err)
-				}
+				require.NoError(t, s.Write(&buf))
 			}
 			code := codegen.FormatTestCode(t, "package foo\n"+buf.String())
-			if code != c.Code {
-				t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, codegen.Diff(t, code, c.Code))
-			}
+			assert.Equal(t, c.Code, code)
 		})
 	}
 }
diff --git a/grpc/codegen/streaming_test.go b/grpc/codegen/streaming_test.go
index cad00449..1e3c4b36 100644
--- a/grpc/codegen/streaming_test.go
+++ b/grpc/codegen/streaming_test.go
@@ -4,6 +4,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/expr"
 	"goa.design/goa/v3/grpc/codegen/testdata"
@@ -140,9 +142,7 @@ func TestStreaming(t *testing.T) {
 					t.Errorf("invalid code for %s: got %d %s sections, expected 0.\n%s", path, seclen, s.Name, code)
 				default:
 					gen := strings.Join(code, "\n")
-					if gen != *s.Code {
-						t.Errorf("invalid code for %s %s section, got:\n%s\ngot vs. expected:\n%s", path, s.Name, gen, codegen.Diff(t, gen, *s.Code))
-					}
+					assert.Equal(t, *s.Code, gen, "invalid code for %s %s section", path, s.Name)
 				}
 			}
 		})
diff --git a/http/codegen/openapi/v2/files_test.go b/http/codegen/openapi/v2/files_test.go
index 7e9ecab5..9335ab37 100644
--- a/http/codegen/openapi/v2/files_test.go
+++ b/http/codegen/openapi/v2/files_test.go
@@ -12,7 +12,8 @@ import (
 	"text/template"
 
 	"github.com/getkin/kin-openapi/openapi2"
-	"goa.design/goa/v3/codegen"
+	"github.com/stretchr/testify/assert"
+
 	httpgen "goa.design/goa/v3/http/codegen"
 	"goa.design/goa/v3/http/codegen/openapi"
 	openapiv2 "goa.design/goa/v3/http/codegen/openapi/v2"
@@ -106,8 +107,7 @@ func TestSections(t *testing.T) {
 							left = buf.String()
 							right = string(want)
 						}
-						diff := codegen.Diff(t, left, right)
-						t.Errorf("result does not match the golden file, diff:\n%s\n", diff)
+						assert.Equal(t, left, right)
 					}
 				})
 			}
diff --git a/http/codegen/openapi/v3/files_test.go b/http/codegen/openapi/v3/files_test.go
index 59350af7..f90e9a77 100644
--- a/http/codegen/openapi/v3/files_test.go
+++ b/http/codegen/openapi/v3/files_test.go
@@ -13,7 +13,8 @@ import (
 	"text/template"
 
 	"github.com/getkin/kin-openapi/openapi3"
-	"goa.design/goa/v3/codegen"
+	"github.com/stretchr/testify/assert"
+
 	httpgen "goa.design/goa/v3/http/codegen"
 	"goa.design/goa/v3/http/codegen/openapi"
 	openapiv3 "goa.design/goa/v3/http/codegen/openapi/v3"
@@ -116,8 +117,7 @@ func TestFiles(t *testing.T) {
 							left = buf.String()
 							right = string(want)
 						}
-						diff := codegen.Diff(t, left, right)
-						t.Errorf("result does not match the golden file, got vs. expected:\n%s\n", diff)
+						assert.Equal(t, left, right)
 					}
 				})
 			}