Repository: gavv/httpexpect
Pull request #299 (2023-02-20 14:23:09)

Array/Object transform canonization
PR for issue #297 
Implement canonization for 
- [x] Array Transform Canonization
- [x] Object Transform Canonization

coveralls: 
[![Coverage Status](https://coveralls.io/builds/57102620/badge)](https://coveralls.io/builds/57102620)

Coverage: 95.218%. Remained the same when pulling **384598f413d400b071c83c5ea97aaf5336269198 on Darrellbor:297-canonization_of_array_transform_and_object_transform** into **30a0c13c69b68598f51c37aae0ff1d63fe72d5c7 on gavv:master**.

Darrellbor: @gavv completed and ready for review
gavv: Thanks for PR!

There is one minor issue. In this test, we want to check how Transform performs canonization, and we do NOT want to check how IsEqual performs canonization.

So instead of calling IsEqual with various types, we should call Transform with various types and then check that newArray.Raw() always returns canonized value. We don't need IsEqual at all in this test.
Darrellbor: @gavv I have adjusted the array transform to `assert` equals rather than direct equals; although i can no longer use wrapped types like 
```
type (
 myArray []interface{}
 myFloat float64
)
```
As it complains that `float64` does not equal `myFloat` for example.

Do i also do the same for object transform?
gavv: Wrapped types should be returned from Transform, not passed to assert.Equal. myArray is not relevant, but myFloat can be used.

> Do i also do the same for object transform?

Yep.

Patch:
diff --git a/environment.go b/environment.go
index 9f2dc9d..b2d4da6 100644
--- a/environment.go
+++ b/environment.go
@@ -2,8 +2,11 @@ package httpexpect
 
 import (
 	"errors"
+	"sort"
 	"sync"
 	"time"
+
+	"github.com/gobwas/glob"
 )
 
 // Environment provides a container for arbitrary data shared between tests.
@@ -424,6 +427,78 @@ func (e *Environment) GetTime(key string) time.Time {
 	return casted
 }
 
+// List returns a sorted slice of keys.
+//
+// Example:
+//
+//	env := NewEnvironment(t)
+//
+//	for _, key := range env.List() {
+//		...
+//	}
+func (e *Environment) List() []string {
+	opChain := e.chain.enter("List()")
+	defer opChain.leave()
+
+	e.mu.RLock()
+	defer e.mu.RUnlock()
+
+	keys := []string{}
+
+	for key := range e.data {
+		keys = append(keys, key)
+	}
+
+	sort.Slice(keys, func(i, j int) bool {
+		return keys[i] < keys[j]
+	})
+	return keys
+}
+
+// Glob accepts a glob pattern and returns a sorted slice of
+// keys that match the pattern.
+//
+// If the pattern is invalid, reports failure and returns an
+// empty slice.
+//
+// Example:
+//
+//	env := NewEnvironment(t)
+//
+//	for _, key := range env.Glob("foo.*") {
+//		...
+//	}
+func (e *Environment) Glob(pattern string) []string {
+	opChain := e.chain.enter("Glob(%q)", pattern)
+	defer opChain.leave()
+
+	e.mu.RLock()
+	defer e.mu.RUnlock()
+
+	glb, err := glob.Compile(pattern)
+	if err != nil {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected invalid glob pattern"),
+			},
+		})
+		return []string{}
+	}
+
+	keys := []string{}
+	for key := range e.data {
+		if glb.Match(key) {
+			keys = append(keys, key)
+		}
+	}
+
+	sort.Slice(keys, func(i, j int) bool {
+		return keys[i] < keys[j]
+	})
+	return keys
+}
+
 func envValue(chain *chain, env map[string]interface{}, key string) (interface{}, bool) {
 	v, ok := env[key]
 
diff --git a/go.mod b/go.mod
index 014b30f..058b1ba 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/gobwas/glob v0.2.3
 	github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
 	github.com/mattn/go-colorable v0.1.2 // indirect
 	github.com/onsi/ginkgo v1.10.1 // indirect
diff --git a/go.sum b/go.sum
index 782fb0b..37dd4c1 100644
--- a/go.sum
+++ b/go.sum
@@ -13,6 +13,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=


Test patch:
diff --git a/array_test.go b/array_test.go
index 990eb4c..2953bc8 100644
--- a/array_test.go
+++ b/array_test.go
@@ -1583,6 +1583,26 @@ func TestArray_Transform(t *testing.T) {
 
 		newArray.chain.assertFailed(t)
 	})
+
+	t.Run("canonization", func(t *testing.T) {
+		type (
+			myInt int
+		)
+
+		reporter := newMockReporter(t)
+		array := NewArray(reporter, []interface{}{2, 4, 6})
+
+		newArray := array.Transform(func(_ int, val interface{}) interface{} {
+			if val, ok := val.(float64); ok {
+				return myInt(val)
+			}
+			t.Errorf("failed transformation")
+			return nil
+		})
+
+		assert.Equal(t, []interface{}{2.0, 4.0, 6.0}, newArray.Raw())
+		newArray.chain.assertNotFailed(t)
+	})
 }
 
 func TestArray_Filter(t *testing.T) {
diff --git a/body_wrapper_test.go b/body_wrapper_test.go
index cc4af35..13d173a 100644
--- a/body_wrapper_test.go
+++ b/body_wrapper_test.go
@@ -112,7 +112,7 @@ func TestBodyWrapper_OneError(t *testing.T) {
 		assert.Nil(t, rd)
 	}
 
-	t.Run("readerr/read_close", func(t *testing.T) {
+	t.Run("read_err, read_close", func(t *testing.T) {
 		body := newMockBody("test_body")
 		body.readErr = bodyErr
 
@@ -125,7 +125,7 @@ func TestBodyWrapper_OneError(t *testing.T) {
 		checkCloseNoErr(t, wrp)
 	})
 
-	t.Run("readerr/close_read", func(t *testing.T) {
+	t.Run("read_err, close_read", func(t *testing.T) {
 		body := newMockBody("test_body")
 		body.readErr = bodyErr
 
@@ -138,7 +138,7 @@ func TestBodyWrapper_OneError(t *testing.T) {
 		checkReadErr(t, wrp)
 	})
 
-	t.Run("closeerr/read_close", func(t *testing.T) {
+	t.Run("close_err, read_close", func(t *testing.T) {
 		body := newMockBody("test_body")
 		body.closeErr = bodyErr
 
@@ -151,7 +151,7 @@ func TestBodyWrapper_OneError(t *testing.T) {
 		checkCloseErr(t, wrp)
 	})
 
-	t.Run("closeerr/close_read", func(t *testing.T) {
+	t.Run("close_err, close_read", func(t *testing.T) {
 		body := newMockBody("test_body")
 		body.closeErr = bodyErr
 
@@ -164,7 +164,7 @@ func TestBodyWrapper_OneError(t *testing.T) {
 		checkReadErr(t, wrp)
 	})
 
-	t.Run("readerr/getbody", func(t *testing.T) {
+	t.Run("read_err, get_body", func(t *testing.T) {
 		body := newMockBody("test_body")
 		body.readErr = bodyErr
 
@@ -174,7 +174,7 @@ func TestBodyWrapper_OneError(t *testing.T) {
 		checkGetBodyErr(t, wrp)
 	})
 
-	t.Run("closeerr/getbody", func(t *testing.T) {
+	t.Run("close_err, get_body", func(t *testing.T) {
 		body := newMockBody("test_body")
 		body.closeErr = bodyErr
 
diff --git a/canon_test.go b/canon_test.go
index 0001ac7..2fdc8ff 100644
--- a/canon_test.go
+++ b/canon_test.go
@@ -14,21 +14,26 @@ func TestCanon_Number(t *testing.T) {
 	chain := newMockChain(t).enter("test")
 	defer chain.leave()
 
-	d1, ok := canonNumber(chain, 123)
+	var (
+		val float64
+		ok  bool
+	)
+
+	val, ok = canonNumber(chain, 123)
 	assert.True(t, ok)
-	assert.Equal(t, 123.0, d1)
+	assert.Equal(t, 123.0, val)
 	chain.assertNotFailed(t)
 	chain.clearFailed()
 
-	d2, ok := canonNumber(chain, 123.0)
+	val, ok = canonNumber(chain, 123.0)
 	assert.True(t, ok)
-	assert.Equal(t, 123.0, d2)
+	assert.Equal(t, 123.0, val)
 	chain.assertNotFailed(t)
 	chain.clearFailed()
 
-	d3, ok := canonNumber(chain, myInt(123))
+	val, ok = canonNumber(chain, myInt(123))
 	assert.True(t, ok)
-	assert.Equal(t, 123.0, d3)
+	assert.Equal(t, 123.0, val)
 	chain.assertNotFailed(t)
 	chain.clearFailed()
 
@@ -52,15 +57,20 @@ func TestCanon_Array(t *testing.T) {
 	chain := newMockChain(t).enter("test")
 	defer chain.leave()
 
-	d1, ok := canonArray(chain, []interface{}{123.0, 456.0})
+	var (
+		val []interface{}
+		ok  bool
+	)
+
+	val, ok = canonArray(chain, []interface{}{123.0, 456.0})
 	assert.True(t, ok)
-	assert.Equal(t, []interface{}{123.0, 456.0}, d1)
+	assert.Equal(t, []interface{}{123.0, 456.0}, val)
 	chain.assertNotFailed(t)
 	chain.clearFailed()
 
-	d2, ok := canonArray(chain, myArray{myInt(123), 456.0})
+	val, ok = canonArray(chain, myArray{myInt(123), 456.0})
 	assert.True(t, ok)
-	assert.Equal(t, []interface{}{123.0, 456.0}, d2)
+	assert.Equal(t, []interface{}{123.0, 456.0}, val)
 	chain.assertNotFailed(t)
 	chain.clearFailed()
 
@@ -94,15 +104,20 @@ func TestCanon_Map(t *testing.T) {
 	chain := newMockChain(t).enter("test")
 	defer chain.leave()
 
-	d1, ok := canonMap(chain, map[string]interface{}{"foo": 123.0})
+	var (
+		val map[string]interface{}
+		ok  bool
+	)
+
+	val, ok = canonMap(chain, map[string]interface{}{"foo": 123.0})
 	assert.True(t, ok)
-	assert.Equal(t, map[string]interface{}{"foo": 123.0}, d1)
+	assert.Equal(t, map[string]interface{}{"foo": 123.0}, val)
 	chain.assertNotFailed(t)
 	chain.clearFailed()
 
-	d2, ok := canonMap(chain, myMap{"foo": myInt(123)})
+	val, ok = canonMap(chain, myMap{"foo": myInt(123)})
 	assert.True(t, ok)
-	assert.Equal(t, map[string]interface{}{"foo": 123.0}, d2)
+	assert.Equal(t, map[string]interface{}{"foo": 123.0}, val)
 	chain.assertNotFailed(t)
 	chain.clearFailed()
 
diff --git a/chain_test.go b/chain_test.go
index 89d1c07..638bf06 100644
--- a/chain_test.go
+++ b/chain_test.go
@@ -23,7 +23,7 @@ func TestChain_Basic(t *testing.T) {
 		assert.False(t, chain2.treeFailed())
 	})
 
-	t.Run("enter_leave", func(t *testing.T) {
+	t.Run("enter leave", func(t *testing.T) {
 		chain1 := newMockChain(t)
 		chain2 := chain1.enter("test")
 
@@ -39,7 +39,7 @@ func TestChain_Basic(t *testing.T) {
 		chain2.leave()
 	})
 
-	t.Run("enter_leave_fail", func(t *testing.T) {
+	t.Run("enter leave fail", func(t *testing.T) {
 		chain1 := newMockChain(t)
 		chain2 := chain1.enter("test")
 
@@ -66,7 +66,7 @@ func TestChain_Basic(t *testing.T) {
 		chain2.assertFlags(t, flagFailed)
 	})
 
-	t.Run("clone_enter_leave_fail", func(t *testing.T) {
+	t.Run("clone enter leave fail", func(t *testing.T) {
 		chain1 := newMockChain(t)
 		chain2 := chain1.clone()
 		chain3 := chain2.clone()
@@ -107,7 +107,7 @@ func TestChain_Basic(t *testing.T) {
 		chain3e.assertFlags(t, flagFailed)
 	})
 
-	t.Run("two_branches", func(t *testing.T) {
+	t.Run("two branches", func(t *testing.T) {
 		chain1 := newMockChain(t)
 		chain2 := chain1.clone()
 		chain2e := chain2.enter("test")
@@ -157,7 +157,7 @@ func TestChain_Basic(t *testing.T) {
 		chain3e.assertFlags(t, flagFailed)
 	})
 
-	t.Run("set_root_1", func(t *testing.T) {
+	t.Run("set root 1", func(t *testing.T) {
 		chain1 := newMockChain(t)
 		chain2 := chain1.clone()
 		chain2.setRoot()
@@ -199,7 +199,7 @@ func TestChain_Basic(t *testing.T) {
 		chain3e.assertFlags(t, flagFailed)
 	})
 
-	t.Run("set_root_2", func(t *testing.T) {
+	t.Run("set root 2", func(t *testing.T) {
 		chain1 := newMockChain(t)
 		chain2 := chain1.clone()
 		chain3 := chain2.clone()
@@ -243,13 +243,13 @@ func TestChain_Basic(t *testing.T) {
 }
 
 func TestChain_Panics(t *testing.T) {
-	t.Run("nil_reporter", func(t *testing.T) {
+	t.Run("nil reporter", func(t *testing.T) {
 		assert.Panics(t, func() {
 			_ = newChainWithDefaults("test", nil)
 		})
 	})
 
-	t.Run("set_request_twice", func(t *testing.T) {
+	t.Run("set request twice", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -260,7 +260,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("set_response_twice", func(t *testing.T) {
+	t.Run("set response twice", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -271,7 +271,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("leave_without_enter", func(t *testing.T) {
+	t.Run("leave without enter", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		assert.Panics(t, func() {
@@ -279,7 +279,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("leave_on_parent", func(t *testing.T) {
+	t.Run("leave on parent", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		_ = chain.enter("foo")
@@ -289,7 +289,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("double_leave", func(t *testing.T) {
+	t.Run("double leave", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -300,7 +300,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("enter_after_leave", func(t *testing.T) {
+	t.Run("enter after leave", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -311,7 +311,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("replace_without_enter", func(t *testing.T) {
+	t.Run("replace without enter", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		assert.Panics(t, func() {
@@ -319,7 +319,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("replace_after_leave", func(t *testing.T) {
+	t.Run("replace after leave", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -330,7 +330,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("replace_empty_path", func(t *testing.T) {
+	t.Run("replace empty path", func(t *testing.T) {
 		chain := newChainWithDefaults("", newMockReporter(t))
 
 		opChain := chain.enter("")
@@ -340,7 +340,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("replace_empty_aliased_path", func(t *testing.T) {
+	t.Run("replace empty aliased path", func(t *testing.T) {
 		chain := newChainWithDefaults("", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -351,7 +351,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("fail_without_enter", func(t *testing.T) {
+	t.Run("fail without enter", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		assert.Panics(t, func() {
@@ -359,7 +359,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("fail_after_leave", func(t *testing.T) {
+	t.Run("fail after leave", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -370,7 +370,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("clone_after_leave", func(t *testing.T) {
+	t.Run("clone after leave", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -381,7 +381,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("alias_after_leave", func(t *testing.T) {
+	t.Run("alias after leave", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -392,7 +392,7 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
-	t.Run("setters_after_leave", func(t *testing.T) {
+	t.Run("setters after leave", func(t *testing.T) {
 		setterFuncs := []func(chain *chain){
 			func(chain *chain) {
 				chain.setRoot()
@@ -423,7 +423,7 @@ func TestChain_Panics(t *testing.T) {
 		}
 	})
 
-	t.Run("invalid_assertion", func(t *testing.T) {
+	t.Run("invalid assertion", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
 		opChain := chain.enter("foo")
@@ -438,7 +438,7 @@ func TestChain_Panics(t *testing.T) {
 }
 
 func TestChain_Env(t *testing.T) {
-	t.Run("newChainWithConfig_non_nil_env", func(t *testing.T) {
+	t.Run("newChainWithConfig, non-nil env", func(t *testing.T) {
 		env := NewEnvironment(newMockReporter(t))
 
 		chain := newChainWithConfig("root", Config{
@@ -449,7 +449,7 @@ func TestChain_Env(t *testing.T) {
 		assert.Same(t, env, chain.env())
 	})
 
-	t.Run("newChainWithConfig_nil_env", func(t *testing.T) {
+	t.Run("newChainWithConfig, nil env", func(t *testing.T) {
 		chain := newChainWithConfig("root", Config{
 			AssertionHandler: &mockAssertionHandler{},
 			Environment:      nil,
@@ -466,7 +466,7 @@ func TestChain_Env(t *testing.T) {
 }
 
 func TestChain_Root(t *testing.T) {
-	t.Run("newChainWithConfig_non_empty_path", func(t *testing.T) {
+	t.Run("newChainWithConfig, non-empty path", func(t *testing.T) {
 		chain := newChainWithConfig("root", Config{
 			AssertionHandler: &mockAssertionHandler{},
 		}.withDefaults())
@@ -474,7 +474,7 @@ func TestChain_Root(t *testing.T) {
 		assert.Equal(t, []string{"root"}, chain.context.Path)
 	})
 
-	t.Run("newChainWithConfig_empty_path", func(t *testing.T) {
+	t.Run("newChainWithConfig, empty path", func(t *testing.T) {
 		chain := newChainWithConfig("", Config{
 			AssertionHandler: &mockAssertionHandler{},
 		}.withDefaults())
@@ -482,13 +482,13 @@ func TestChain_Root(t *testing.T) {
 		assert.Equal(t, []string{}, chain.context.Path)
 	})
 
-	t.Run("newChainWithDefaults_non_empty_path", func(t *testing.T) {
+	t.Run("newChainWithDefaults, non-empty path", func(t *testing.T) {
 		chain := newChainWithDefaults("root", newMockReporter(t))
 
 		assert.Equal(t, []string{"root"}, chain.context.Path)
 	})
 
-	t.Run("newChainWithDefaults_empty_path", func(t *testing.T) {
+	t.Run("newChainWithDefaults, empty path", func(t *testing.T) {
 		chain := newChainWithDefaults("", newMockReporter(t))
 
 		assert.Equal(t, []string{}, chain.context.Path)
@@ -544,7 +544,7 @@ func TestChain_AliasedPath(t *testing.T) {
 		return strings.Join(c.context.AliasedPath, ".")
 	}
 
-	t.Run("enter_and_leave", func(t *testing.T) {
+	t.Run("enter and leave", func(t *testing.T) {
 		rootChain := newChainWithDefaults("root", newMockReporter(t))
 
 		assert.Equal(t, "root", path(rootChain))
@@ -571,7 +571,7 @@ func TestChain_AliasedPath(t *testing.T) {
 		assert.Equal(t, "alias1.baz", aliasedPath(c3))
 	})
 
-	t.Run("set_empty", func(t *testing.T) {
+	t.Run("set empty", func(t *testing.T) {
 		rootChain := newChainWithDefaults("root", newMockReporter(t))
 
 		assert.Equal(t, "root", path(rootChain))
diff --git a/environment_test.go b/environment_test.go
index dfe698b..77932eb 100644
--- a/environment_test.go
+++ b/environment_test.go
@@ -489,3 +489,65 @@ func TestEnvironment_Time(t *testing.T) {
 			})
 	}
 }
+
+func TestEnvironment_List(t *testing.T) {
+	env := newEnvironment(newMockChain(t))
+
+	assert.Equal(t, []string{}, env.List())
+
+	env.Put("k1", 1)
+	env.Put("k2", 2)
+	env.Put("k3", 3)
+	assert.Equal(t, []string{"k1", "k2", "k3"}, env.List())
+
+	env.Put("abc", 4)
+	assert.Equal(t, []string{"abc", "k1", "k2", "k3"}, env.List())
+
+	env.Delete("k2")
+	env.Delete("k3")
+	env.Put("pqr", 5)
+	assert.Equal(t, []string{"abc", "k1", "pqr"}, env.List())
+}
+
+func TestEnvironment_Glob(t *testing.T) {
+	t.Run("valid glob pattern", func(t *testing.T) {
+		env := newEnvironment(newMockChain(t))
+
+		assert.Equal(t, []string{}, env.Glob("*"))
+
+		env.Put("k1", 1)
+		env.Put("k2", 2)
+		env.Put("k3", 3)
+		assert.Equal(t, []string{"k1", "k2", "k3"}, env.Glob("*"))
+
+		env.Put("abc", 5)
+		assert.Equal(t, []string{"k1", "k2", "k3"}, env.Glob("k*"))
+
+		env.Put("ab", 6)
+		env.Put("ac", 7)
+		assert.Equal(t, []string{"ab", "ac"}, env.Glob("a?"))
+
+		assert.Equal(t, []string{"ab", "abc"}, env.Glob("ab*"))
+
+		env.Put("k4", 8)
+		assert.Equal(t, []string{"k2", "k3", "k4"}, env.Glob("k[2-4]"))
+
+		env.Put("a4", 9)
+		assert.Equal(t, []string{"a4", "ab", "ac", "k4"}, env.Glob("?[!1-3]"))
+		assert.Equal(t, []string{"a4", "k1", "k4"}, env.Glob("?[1,4]"))
+		assert.Equal(t, []string{"ab", "ac", "k2", "k3"}, env.Glob("?[!1,4]"))
+	})
+
+	t.Run("invalid glob pattern", func(t *testing.T) {
+		env := newEnvironment(newMockChain(t))
+		assert.Equal(t, []string{}, env.Glob("k[1-2"))
+		env.chain.assertFailed(t)
+		env.chain.clearFailed()
+
+		env.Put("k1", 1)
+		env.Put("k2", 2)
+		assert.Equal(t, []string{}, env.Glob("k[]"))
+		env.chain.assertFailed(t)
+		env.chain.clearFailed()
+	})
+}
diff --git a/object_test.go b/object_test.go
index 4bacab0..4616c57 100644
--- a/object_test.go
+++ b/object_test.go
@@ -1,6 +1,7 @@
 package httpexpect
 
 import (
+	"strconv"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -1299,6 +1300,36 @@ func TestObject_Transform(t *testing.T) {
 
 		newObject.chain.assertFailed(t)
 	})
+
+	t.Run("canonization", func(ts *testing.T) {
+		type (
+			myInt int
+		)
+
+		reporter := newMockReporter(ts)
+		object := NewObject(reporter, map[string]interface{}{
+			"foo": "123",
+			"bar": "456",
+			"baz": "b",
+		})
+
+		newObject := object.Transform(func(_ string, val interface{}) interface{} {
+			if v, err := strconv.ParseFloat(val.(string), 64); err == nil {
+				return myInt(v)
+			} else {
+				return val
+			}
+		})
+
+		assert.Equal(t,
+			map[string]interface{}{
+				"foo": 123.0,
+				"bar": 456.0,
+				"baz": "b",
+			},
+			newObject.Raw())
+		newObject.chain.assertNotFailed(ts)
+	})
 }
 
 func TestObject_Filter(t *testing.T) {