Repository: gavv/httpexpect
Pull request #227 (2023-01-15 14:03:23)

implement alias support
close https://github.com/gavv/httpexpect/issues/171

* Inspector structs which added Alias() are follows:
  * Array
  * Boolean
  * Cookie
  * DateTime
  * Duration
  * Match
  * Number
  * Object
  * Response
  * String
  * Value
  * WebSocketMessage
  * WebSocket

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

Coverage: 95.683% (+0.04%) from 95.64% when pulling **a8d352aa6483ef10c3dde425d88a9ee74f8a27e7 on k4n4ry:feature/impl-alias-support** into **b5a5bdc2a0453f36c386015835990587dc46a126 on gavv:master**.

gavv: Thanks for PR! Will review in upcoming days.
github-actions[bot]: :umbrella: The latest upstream change (presumably [these](https://github.com/gavv/httpexpect/compare/29fd41d071c4...fbc6d58c794a)) made this pull request unmergeable. Please resolve the merge conflicts.
gavv: Hi, please rebase your PR on fresh master. It has reworked chain interface.

Instead of:

```go
	r.chain.enter("XXX()")
	defer r.leave()

	if r.failed() {
		return r
	}
```

we should now use:

```go
	opChain := r.chain.enter("Decode()")
	defer opChain.leave()

	if opChain.failed() {
		return r
	}
```

In other words, chain.enter() now returns a new chain instance, and we should use it for all other operations with chain. This change is part of making this lib thread-safe.

Related commits:

* a96122195174a5b9df2804e41c8abe92a90fc52b
* 90675e11729f5dfa5121bbf41a1e071dbf9e52e7
gavv: The following changes in chain affect this PR:

* enter does not modify the chain now; instead it creates a clone, modifies this clone, and returns it
* leave does not modify the chain now; since enter returns a clone, we can just drop this clone
* fail is allowed only between enter/leave
* it's not allowed to call enter two times in a row; after first enter you should make a clone, and you can call enter again only on that clone
* replace also returns a clone, just like enter
gavv: Sorry for creating conflicts. I was waiting with my changes for a while, in hope that I'll find a time window when there are no active PRs and I can merge them without creating conflicts. However it seems that currently new PRs are coming too often and there is no such window :)
k4n4ry: @gavv  I rebased and fixed, so please check when you have time. Thank you.

Patch:
diff --git a/README.md b/README.md
index c5c6541..b2b53b5 100644
--- a/README.md
+++ b/README.md
@@ -695,6 +695,24 @@ e.POST("/fruits").
 	Status(http.StatusOK)
 ```
 
+##### Support for alias in a failure message
+
+```go
+// when test is failed, assertion in the failure message is Request("GET").Expect().JSON().Array().Empty()
+e.GET("/fruits").
+	Expect().
+	Status(http.StatusOK).JSON().Array().Empty()
+
+
+// add alias named fruits
+fruits := e.GET("/fruits").
+	Expect().
+	Status(http.StatusOK).JSON().Array().Alias("fruits")
+
+// assertion is fruits.Empty()
+fruits.Empty()
+```
+
 ##### Printing requests and responses
 
 ```go
diff --git a/array.go b/array.go
index ca2ba24..81a6f69 100644
--- a/array.go
+++ b/array.go
@@ -70,6 +70,15 @@ func (a *Array) Raw() []interface{} {
 	return a.value
 }
 
+// Alias is similar to Value.Alias.
+func (a *Array) Alias(name string) *Array {
+	opChain := a.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	a.chain.setAlias(name)
+	return a
+}
+
 // Decode unmarshals the underlying value attached to the Array to a target variable.
 // target should be one of these:
 //
diff --git a/assertion.go b/assertion.go
index 445aab9..6199978 100644
--- a/assertion.go
+++ b/assertion.go
@@ -117,6 +117,12 @@ type AssertionContext struct {
 	//   {`Request("GET")`, `Expect()`, `JSON()`, `NotNull()`}
 	Path []string
 
+	// Chain of nested assertion names starting from alias
+	// When alias is not set, AliasedPath has the same value as Path
+	// Example value:
+	//   {`foo`, `NotNull()`} // alias named foo
+	AliasedPath []string
+
 	// Request being sent
 	// May be nil if request was not yet sent
 	Request *Request
diff --git a/boolean.go b/boolean.go
index 38edaf2..4bea77d 100644
--- a/boolean.go
+++ b/boolean.go
@@ -49,6 +49,15 @@ func (b *Boolean) Raw() bool {
 	return b.value
 }
 
+// Alias is similar to Value.Alias.
+func (b *Boolean) Alias(name string) *Boolean {
+	opChain := b.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	b.chain.setAlias(name)
+	return b
+}
+
 // Decode unmarshals the underlying value attached to the Boolean to a target variable.
 // target should be one of these:
 //
diff --git a/chain.go b/chain.go
index 7837d4a..8a7d2f4 100644
--- a/chain.go
+++ b/chain.go
@@ -113,8 +113,10 @@ func newChainWithConfig(name string, config Config) *chain {
 
 	if name != "" {
 		c.context.Path = []string{name}
+		c.context.AliasedPath = []string{name}
 	} else {
 		c.context.Path = []string{}
+		c.context.AliasedPath = []string{}
 	}
 
 	if config.Environment != nil {
@@ -143,8 +145,10 @@ func newChainWithDefaults(name string, reporter Reporter) *chain {
 
 	if name != "" {
 		c.context.Path = []string{name}
+		c.context.AliasedPath = []string{name}
 	} else {
 		c.context.Path = []string{}
+		c.context.AliasedPath = []string{}
 	}
 
 	c.context.Environment = newEnvironment(c)
@@ -248,6 +252,7 @@ func (c *chain) clone() *chain {
 
 	contextCopy := c.context
 	contextCopy.Path = append(([]string)(nil), contextCopy.Path...)
+	contextCopy.AliasedPath = append(([]string)(nil), c.context.AliasedPath...)
 
 	return &chain{
 		parent: c,
@@ -270,6 +275,8 @@ func (c *chain) enter(name string, args ...interface{}) *chain {
 	chainCopy.state = stateEntered
 	if name != "" {
 		chainCopy.context.Path = append(chainCopy.context.Path, fmt.Sprintf(name, args...))
+		chainCopy.context.AliasedPath =
+			append(c.context.AliasedPath, fmt.Sprintf(name, args...))
 	}
 
 	return chainCopy
@@ -289,6 +296,9 @@ func (c *chain) replace(name string, args ...interface{}) *chain {
 			if len(c.context.Path) == 0 {
 				panic("replace allowed only if path is non-empty")
 			}
+			if len(c.context.AliasedPath) == 0 {
+				panic("replace allowed only if aliased path is non-empty")
+			}
 		}()
 	}
 
@@ -296,7 +306,12 @@ func (c *chain) replace(name string, args ...interface{}) *chain {
 
 	chainCopy.state = stateEntered
 	if len(chainCopy.context.Path) != 0 {
-		chainCopy.context.Path[len(chainCopy.context.Path)-1] = fmt.Sprintf(name, args...)
+		last := len(chainCopy.context.Path) - 1
+		chainCopy.context.Path[last] = fmt.Sprintf(name, args...)
+	}
+	if len(chainCopy.context.AliasedPath) != 0 {
+		last := len(chainCopy.context.AliasedPath) - 1
+		chainCopy.context.AliasedPath[last] = fmt.Sprintf(name, args...)
 	}
 
 	return chainCopy
@@ -356,6 +371,22 @@ func (c *chain) leave() {
 	}
 }
 
+// Initialize and set name to aliased path.
+func (c *chain) setAlias(name string) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if chainValidation && c.state == stateLeaved {
+		panic("can't use chain after leave")
+	}
+
+	if name != "" {
+		c.context.AliasedPath = []string{name}
+	} else {
+		c.context.AliasedPath = []string{}
+	}
+}
+
 // Report assertion failure and mark chain as failed.
 // Must be called between enter() and leave().
 func (c *chain) fail(failure AssertionFailure) {
diff --git a/cookie.go b/cookie.go
index 65743af..114a6e7 100644
--- a/cookie.go
+++ b/cookie.go
@@ -71,6 +71,15 @@ func (c *Cookie) Raw() *http.Cookie {
 	return c.value
 }
 
+// Alias is similar to Value.Alias.
+func (c *Cookie) Alias(name string) *Cookie {
+	opChain := c.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	c.chain.setAlias(name)
+	return c
+}
+
 // Name returns a new String instance with cookie name.
 //
 // Example:
diff --git a/datetime.go b/datetime.go
index a4d8d85..c8e097a 100644
--- a/datetime.go
+++ b/datetime.go
@@ -51,6 +51,15 @@ func (dt *DateTime) Raw() time.Time {
 	return dt.value
 }
 
+// Alias is similar to Value.Alias.
+func (dt *DateTime) Alias(name string) *DateTime {
+	opChain := dt.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	dt.chain.setAlias(name)
+	return dt
+}
+
 // GetZone returns a new String instance with datetime zone.
 //
 // Example:
diff --git a/duration.go b/duration.go
index d335695..7d55f85 100644
--- a/duration.go
+++ b/duration.go
@@ -54,6 +54,15 @@ func (d *Duration) Raw() time.Duration {
 	return *d.value
 }
 
+// Alias is similar to Value.Alias.
+func (d *Duration) Alias(name string) *Duration {
+	opChain := d.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	d.chain.setAlias(name)
+	return d
+}
+
 // Deprecated: support for unset durations will be removed. The only method that
 // can create unset duration is Cookie.MaxAge. Instead of Cookie.MaxAge().IsSet(),
 // please use Cookie.HaveMaxAge().
diff --git a/formatter.go b/formatter.go
index e139816..c02ed9a 100644
--- a/formatter.go
+++ b/formatter.go
@@ -39,6 +39,9 @@ type DefaultFormatter struct {
 	// Exclude assertion path from failure report.
 	DisablePaths bool
 
+	// Exclude aliased assertion path from failure report.
+	DisableAliases bool
+
 	// Exclude diff from failure report.
 	DisableDiffs bool
 
@@ -199,7 +202,11 @@ func (f *DefaultFormatter) fillDescription(
 	}
 
 	if !f.DisablePaths {
-		data.AssertPath = ctx.Path
+		if !f.DisableAliases {
+			data.AssertPath = ctx.AliasedPath
+		} else {
+			data.AssertPath = ctx.Path
+		}
 	}
 
 	if f.LineWidth != 0 {
diff --git a/match.go b/match.go
index bab3e7f..c9aa91e 100644
--- a/match.go
+++ b/match.go
@@ -77,6 +77,16 @@ func (m *Match) Raw() []string {
 	return m.submatches
 }
 
+// Alias is similar to Value.Alias.
+func (m *Match) Alias(name string) *Match {
+	opChain := m.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	m.chain.setAlias(name)
+
+	return m
+}
+
 // Length returns a new Number instance with number of submatches.
 //
 // Example:
diff --git a/number.go b/number.go
index 2fd673f..f09cc2d 100644
--- a/number.go
+++ b/number.go
@@ -50,6 +50,15 @@ func (n *Number) Raw() float64 {
 	return n.value
 }
 
+// Alias is similar to Value.Alias.
+func (n *Number) Alias(name string) *Number {
+	opChain := n.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	n.chain.setAlias(name)
+	return n
+}
+
 // Decode unmarshals the underlying value attached to the Number to a target variable.
 // target should be one of these:
 //
diff --git a/object.go b/object.go
index d3186a2..70c8c60 100644
--- a/object.go
+++ b/object.go
@@ -71,6 +71,15 @@ func (o *Object) Raw() map[string]interface{} {
 	return o.value
 }
 
+// Alias is similar to Value.Alias.
+func (o *Object) Alias(name string) *Object {
+	opChain := o.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	o.chain.setAlias(name)
+	return o
+}
+
 // Decode unmarshals the underlying value attached to the Object to a target variable
 // target should be one of this:
 //
diff --git a/response.go b/response.go
index 60bc04c..ee1b5f2 100644
--- a/response.go
+++ b/response.go
@@ -165,6 +165,16 @@ func (r *Response) Raw() *http.Response {
 	return r.httpResp
 }
 
+// Alias is similar to Value.Alias.
+func (r *Response) Alias(name string) *Response {
+	opChain := r.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	r.chain.setAlias(name)
+
+	return r
+}
+
 // RoundTripTime returns a new Duration instance with response round-trip time.
 //
 // The returned duration is the time interval starting just before request is
diff --git a/string.go b/string.go
index 0931c32..c8247ad 100644
--- a/string.go
+++ b/string.go
@@ -56,6 +56,15 @@ func (s *String) Raw() string {
 	return s.value
 }
 
+// Alias is similar to Value.Alias.
+func (s *String) Alias(name string) *String {
+	opChain := s.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	s.chain.setAlias(name)
+	return s
+}
+
 // Decode unmarshals the underlying value attached to the String to a target variable.
 // target should be one of these:
 //
diff --git a/value.go b/value.go
index e2c8001..360c6ea 100644
--- a/value.go
+++ b/value.go
@@ -75,6 +75,35 @@ func (v *Value) Raw() interface{} {
 	return v.value
 }
 
+// Alias returns a new Value object with alias.
+// When a test of Value object with alias is failed,
+// an assertion is displayed as a chain starting from the alias.
+//
+// Example:
+//
+//	// In this example, GET /example responds "foo"
+//	foo := e.GET("/example").Expect().Status(http.StatusOK).JSON().Object()
+//
+//	// When a test is failed, an assertion without alias is
+//	// Request("GET").Expect().JSON().Object().Equal()
+//	foo.Equal("bar")
+//
+//	// Set Alias
+//	fooWithAlias := e.GET("/example").
+//		Expect().
+//		Status(http.StatusOK).JSON().Object().Alias("foo")
+//
+//	// When a test is failed, an assertion with alias is
+//	// foo.Equal()
+//	fooWithAlias.Equal("bar")
+func (v *Value) Alias(name string) *Value {
+	opChain := v.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	v.chain.setAlias(name)
+	return v
+}
+
 // Path returns a new Value object for child object(s) matching given
 // JSONPath expression.
 //
diff --git a/websocket.go b/websocket.go
index e21fa9f..7a93dcf 100644
--- a/websocket.go
+++ b/websocket.go
@@ -85,6 +85,15 @@ func (ws *Websocket) Raw() *websocket.Conn {
 	return conn
 }
 
+// Alias is similar to Value.Alias.
+func (ws *Websocket) Alias(name string) *Websocket {
+	opChain := ws.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	ws.chain.setAlias(name)
+	return ws
+}
+
 // WithReadTimeout sets timeout duration for WebSocket connection reads.
 //
 // By default no timeout is used.
diff --git a/websocket_message.go b/websocket_message.go
index 8a159e8..5979676 100644
--- a/websocket_message.go
+++ b/websocket_message.go
@@ -84,6 +84,15 @@ func (wm *WebsocketMessage) Raw() (typ int, content []byte, closeCode int) {
 	return wm.typ, wm.content, wm.closeCode
 }
 
+// Alias is similar to Value.Alias.
+func (wm *WebsocketMessage) Alias(name string) *WebsocketMessage {
+	opChain := wm.chain.enter("Alias(%s)", name)
+	defer opChain.leave()
+
+	wm.chain.setAlias(name)
+	return wm
+}
+
 // CloseMessage is a shorthand for m.Type(websocket.CloseMessage).
 func (wm *WebsocketMessage) CloseMessage() *WebsocketMessage {
 	opChain := wm.chain.enter("CloseMessage()")


Test patch:
diff --git a/array_test.go b/array_test.go
index c90c05a..1a25e94 100644
--- a/array_test.go
+++ b/array_test.go
@@ -12,6 +12,7 @@ func TestArray_Failed(t *testing.T) {
 
 		value.Path("$")
 		value.Schema("")
+		value.Alias("foo")
 
 		assert.NotNil(t, value.Length())
 		assert.NotNil(t, value.Element(0))
@@ -240,6 +241,23 @@ func TestArray_Getters(t *testing.T) {
 	value.chain.clearFailed()
 }
 
+func TestArray_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewArray(reporter, []interface{}{1, 2})
+	assert.Equal(t, []string{"Array()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Array()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Array()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+
+	value3 := value2.Filter(func(index int, value *Value) bool {
+		return value.Number().Raw() > 1
+	})
+	assert.Equal(t, []string{"Array()", "Filter()"}, value3.chain.context.Path)
+	assert.Equal(t, []string{"foo", "Filter()"}, value3.chain.context.AliasedPath)
+}
+
 func TestArray_Empty(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/boolean_test.go b/boolean_test.go
index 4380b62..f5c584d 100644
--- a/boolean_test.go
+++ b/boolean_test.go
@@ -14,6 +14,7 @@ func TestBoolean_Failed(t *testing.T) {
 
 	value.Path("$")
 	value.Schema("")
+	value.Alias("foo")
 
 	var target interface{}
 	value.Decode(&target)
@@ -117,6 +118,18 @@ func TestBoolean_Getters(t *testing.T) {
 	value.chain.clearFailed()
 }
 
+func TestBoolean_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value1 := NewBoolean(reporter, true)
+	assert.Equal(t, []string{"Boolean()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Boolean()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Boolean()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+}
+
 func TestBoolean_True(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/chain_test.go b/chain_test.go
index 8d784a4..05096a3 100644
--- a/chain_test.go
+++ b/chain_test.go
@@ -241,6 +241,53 @@ func TestChain_Basic(t *testing.T) {
 	})
 }
 
+func TestChain_AliasedPath(t *testing.T) {
+	path := func(c *chain) string {
+		return strings.Join(c.context.Path, ".")
+	}
+	aliasedPath := func(c *chain) string {
+		return strings.Join(c.context.AliasedPath, ".")
+	}
+
+	t.Run("enter_and_leave", func(t *testing.T) {
+		rootChain := newChainWithDefaults("root", newMockReporter(t))
+
+		assert.Equal(t, "root", path(rootChain))
+		assert.Equal(t, "root", aliasedPath(rootChain))
+
+		c1 := rootChain.enter("foo")
+		assert.Equal(t, "root.foo", path(c1))
+		assert.Equal(t, "root.foo", aliasedPath(c1))
+
+		c2 := c1.enter("bar")
+		assert.Equal(t, "root.foo.bar", path(c2))
+		assert.Equal(t, "root.foo.bar", aliasedPath(c2))
+
+		c2.setAlias("alias1")
+		assert.Equal(t, "root.foo.bar", path(c2))
+		assert.Equal(t, "alias1", aliasedPath(c2))
+
+		c3 := c2.enter("baz")
+		assert.Equal(t, "root.foo.bar.baz", path(c3))
+		assert.Equal(t, "alias1.baz", aliasedPath(c3))
+
+		c3.leave()
+		assert.Equal(t, "root.foo.bar.baz", path(c3))
+		assert.Equal(t, "alias1.baz", aliasedPath(c3))
+	})
+
+	t.Run("set_empty", func(t *testing.T) {
+		rootChain := newChainWithDefaults("root", newMockReporter(t))
+
+		assert.Equal(t, "root", path(rootChain))
+		assert.Equal(t, "root", aliasedPath(rootChain))
+
+		rootChain.setAlias("")
+		assert.Equal(t, "root", path(rootChain))
+		assert.Equal(t, "", aliasedPath(rootChain))
+	})
+}
+
 func TestChain_Panics(t *testing.T) {
 	t.Run("nil_reporter", func(t *testing.T) {
 		assert.Panics(t, func() {
@@ -317,6 +364,17 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
+	t.Run("replace_empty_aliased_path", func(t *testing.T) {
+		chain := newChainWithDefaults("", newMockReporter(t))
+
+		opChain := chain.enter("foo")
+		opChain.setAlias("")
+
+		assert.Panics(t, func() {
+			opChain.replace("bar")
+		})
+	})
+
 	t.Run("fail_without_enter", func(t *testing.T) {
 		chain := newChainWithDefaults("test", newMockReporter(t))
 
@@ -347,6 +405,17 @@ func TestChain_Panics(t *testing.T) {
 		})
 	})
 
+	t.Run("alias_after_leave", func(t *testing.T) {
+		chain := newChainWithDefaults("test", newMockReporter(t))
+
+		opChain := chain.enter("foo")
+		opChain.leave()
+
+		assert.Panics(t, func() {
+			opChain.setAlias("bar")
+		})
+	})
+
 	t.Run("setters_after_leave", func(t *testing.T) {
 		setterFuncs := []func(chain *chain){
 			func(chain *chain) {
diff --git a/cookie_test.go b/cookie_test.go
index 1ea1dd6..795e37c 100644
--- a/cookie_test.go
+++ b/cookie_test.go
@@ -25,6 +25,8 @@ func TestCookie_Failed(t *testing.T) {
 		assert.NotNil(t, value.Expires())
 		assert.NotNil(t, value.MaxAge())
 
+		value.Alias("foo")
+
 		value.HaveMaxAge()
 		value.NotHaveMaxAge()
 	}
@@ -129,6 +131,24 @@ func TestCookie_Getters(t *testing.T) {
 	value.chain.assertNotFailed(t)
 }
 
+func TestCookie_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewCookie(reporter, &http.Cookie{
+		MaxAge: 0,
+	})
+	assert.Equal(t, []string{"Cookie()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Cookie()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Cookie()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+
+	value3 := value2.Domain()
+	assert.Equal(t, []string{"Cookie()", "Domain()"},
+		value3.chain.context.Path)
+	assert.Equal(t, []string{"foo", "Domain()"}, value3.chain.context.AliasedPath)
+}
+
 func TestCookie_MaxAge(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/datetime_test.go b/datetime_test.go
index c14565f..c2613c7 100644
--- a/datetime_test.go
+++ b/datetime_test.go
@@ -37,6 +37,7 @@ func TestDateTime_Failed(t *testing.T) {
 	value.GetNanosecond()
 	value.AsUTC()
 	value.AsLocal()
+	value.Alias("foo")
 }
 
 func TestDateTime_Constructors(t *testing.T) {
@@ -66,6 +67,17 @@ func TestDateTime_Constructors(t *testing.T) {
 	})
 }
 
+func TestDateTime_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewDateTime(reporter, time.Unix(0, 1234))
+	assert.Equal(t, []string{"DateTime()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"DateTime()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"DateTime()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+}
+
 func TestDateTime_Equal(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/duration_test.go b/duration_test.go
index 88d6f79..caa37de 100644
--- a/duration_test.go
+++ b/duration_test.go
@@ -22,6 +22,7 @@ func TestDuration_Failed(t *testing.T) {
 	value.Le(tm)
 	value.InRange(tm, tm)
 	value.NotInRange(tm, tm)
+	value.Alias("foo")
 }
 
 func TestDuration_Constructors(t *testing.T) {
@@ -52,6 +53,17 @@ func TestDuration_Constructors(t *testing.T) {
 
 }
 
+func TestDuration_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewDuration(reporter, time.Second)
+	assert.Equal(t, []string{"Duration()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Duration()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Duration()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+}
+
 func TestDuration_Set(t *testing.T) {
 	chain := newMockChain(t)
 
diff --git a/e2e_report_test.go b/e2e_report_test.go
index 03d4668..f1f4158 100644
--- a/e2e_report_test.go
+++ b/e2e_report_test.go
@@ -44,3 +44,34 @@ func TestE2EReport_Names(t *testing.T) {
 	assert.Contains(t, rep.reported, "TestExample")
 	assert.Contains(t, rep.reported, "RequestExample")
 }
+
+func TestE2EReport_Aliases(t *testing.T) {
+	mux := http.NewServeMux()
+
+	mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		_, _ = w.Write([]byte(`{"foo":123}`))
+	})
+
+	server := httptest.NewServer(mux)
+	defer server.Close()
+
+	rep := &recordingReporter{}
+
+	e := WithConfig(Config{
+		TestName: "TestExample",
+		BaseURL:  server.URL,
+		Reporter: rep,
+	})
+
+	foo := e.GET("/test").
+		WithName("RequestExample").
+		Expect().
+		JSON().Alias("foo")
+
+	foo.Object().ContainsKey("bar") // will fail
+
+	t.Logf("%s", rep.reported)
+
+	assert.Contains(t, rep.reported, "foo.Object().ContainsKey()")
+}
diff --git a/match_test.go b/match_test.go
index 84922fd..cfd1ec6 100644
--- a/match_test.go
+++ b/match_test.go
@@ -20,6 +20,7 @@ func TestMatch_Failed(t *testing.T) {
 	value.NotEmpty()
 	value.Values("")
 	value.NotValues("")
+	value.Alias("foo")
 }
 
 func TestMatch_Constructors(t *testing.T) {
@@ -50,6 +51,24 @@ func TestMatch_Constructors(t *testing.T) {
 	})
 }
 
+func TestMatch_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	matches := []string{"m0", "m1", "m2"}
+	names := []string{"", "n1", "n2"}
+
+	value1 := NewMatch(reporter, matches, names)
+	assert.Equal(t, []string{"Match()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Match()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Match()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+
+	value3 := value2.Index(0)
+	assert.Equal(t, []string{"Match()", "Index(0)"}, value3.chain.context.Path)
+	assert.Equal(t, []string{"foo", "Index(0)"}, value3.chain.context.AliasedPath)
+}
+
 func TestMatch_Getters(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/number_test.go b/number_test.go
index 526c187..af0fcef 100644
--- a/number_test.go
+++ b/number_test.go
@@ -18,6 +18,7 @@ func TestNumber_Failed(t *testing.T) {
 
 	var target interface{}
 	value.Decode(&target)
+	value.Alias("foo")
 
 	value.Equal(0)
 	value.NotEqual(0)
@@ -56,6 +57,17 @@ func TestNumber_Constructors(t *testing.T) {
 	})
 }
 
+func TestNumber_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewNumber(reporter, 123)
+	assert.Equal(t, []string{"Number()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Number()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Number()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+}
+
 func TestNumber_Decode(t *testing.T) {
 	t.Run("Decode into empty interface", func(t *testing.T) {
 		reporter := newMockReporter(t)
diff --git a/object_test.go b/object_test.go
index 9a7d69f..6e6e25b 100644
--- a/object_test.go
+++ b/object_test.go
@@ -12,6 +12,7 @@ func TestObject_Failed(t *testing.T) {
 
 		value.Path("$")
 		value.Schema("")
+		value.Alias("foo")
 
 		assert.NotNil(t, value.Keys())
 		assert.NotNil(t, value.Values())
@@ -113,6 +114,24 @@ func TestObject_Constructors(t *testing.T) {
 	})
 }
 
+func TestObject_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewObject(reporter, map[string]interface{}{
+		"foo": 100.0,
+	})
+	assert.Equal(t, []string{"Object()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Object()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("bar")
+	assert.Equal(t, []string{"Object()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"bar"}, value2.chain.context.AliasedPath)
+
+	value3 := value2.Values()
+	assert.Equal(t, []string{"Object()", "Values()"},
+		value3.chain.context.Path)
+	assert.Equal(t, []string{"bar", "Values()"}, value3.chain.context.AliasedPath)
+}
+
 func TestObject_Decode(t *testing.T) {
 	t.Run("Decode into empty interface", func(t *testing.T) {
 		reporter := newMockReporter(t)
diff --git a/response_test.go b/response_test.go
index 001136a..f221217 100644
--- a/response_test.go
+++ b/response_test.go
@@ -47,6 +47,7 @@ func TestResponse_Failed(t *testing.T) {
 		resp.ContentType("", "")
 		resp.ContentEncoding("")
 		resp.TransferEncoding("")
+		resp.Alias("foo")
 	}
 
 	t.Run("failed_chain", func(t *testing.T) {
@@ -125,6 +126,19 @@ func TestResponse_Constructors(t *testing.T) {
 	})
 }
 
+func TestResponse_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	duration := time.Second
+	value1 := NewResponse(reporter, &http.Response{}, duration)
+	assert.Equal(t, []string{"Response()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Response()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Response()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+}
+
 func TestResponse_RoundTripTime(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/string_test.go b/string_test.go
index f0cee15..57b6d56 100644
--- a/string_test.go
+++ b/string_test.go
@@ -19,6 +19,7 @@ func TestString_Failed(t *testing.T) {
 
 	var target interface{}
 	value.Decode(target)
+	value.Alias("foo")
 
 	value.Length()
 	value.AsBoolean()
@@ -120,6 +121,22 @@ func TestString_Decode(t *testing.T) {
 	})
 }
 
+func TestString_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewString(reporter, "123")
+	assert.Equal(t, []string{"String()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"String()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"String()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+
+	value3 := value2.AsNumber(10)
+	assert.Equal(t, []string{"String()", "AsNumber()"},
+		value3.chain.context.Path)
+	assert.Equal(t, []string{"foo", "AsNumber()"}, value3.chain.context.AliasedPath)
+}
+
 func TestString_Getters(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/value_test.go b/value_test.go
index 3db002e..6360bc7 100644
--- a/value_test.go
+++ b/value_test.go
@@ -18,6 +18,7 @@ func TestValue_Failed(t *testing.T) {
 
 	value.Path("$")
 	value.Schema("")
+	value.Alias("foo")
 
 	assert.NotNil(t, value.Path("/"))
 
@@ -179,6 +180,21 @@ func TestValue_CastBoolean(t *testing.T) {
 	NewValue(reporter, data).Null().chain.assertFailed(t)
 }
 
+func TestValue_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewValue(reporter, 123)
+	assert.Equal(t, []string{"Value()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Value()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Value()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+
+	value3 := value2.Number()
+	assert.Equal(t, []string{"Value()", "Number()"}, value3.chain.context.Path)
+	assert.Equal(t, []string{"foo", "Number()"}, value3.chain.context.AliasedPath)
+}
+
 func TestValue_GetObject(t *testing.T) {
 	type (
 		myMap map[string]interface{}
diff --git a/websocket_message_test.go b/websocket_message_test.go
index a0e6297..9d4ff0b 100644
--- a/websocket_message_test.go
+++ b/websocket_message_test.go
@@ -26,6 +26,7 @@ func TestWebsocketMessage_Failed(t *testing.T) {
 	msg.Code(0)
 	msg.NotCode(0)
 	msg.NoContent()
+	msg.Alias("foo")
 
 	msg.Body().chain.assertFailed(t)
 	msg.JSON().chain.assertFailed(t)
@@ -56,6 +57,22 @@ func TestWebsocketMessage_Constructors(t *testing.T) {
 	})
 }
 
+func TestWebsocketMessage_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewWebsocketMessage(reporter, websocket.CloseMessage, nil)
+	assert.Equal(t, []string{"WebsocketMessage()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"WebsocketMessage()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"WebsocketMessage()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+
+	value3 := value2.Body()
+	assert.Equal(t, []string{"WebsocketMessage()", "Body()"},
+		value3.chain.context.Path)
+	assert.Equal(t, []string{"foo", "Body()"}, value3.chain.context.AliasedPath)
+}
+
 func TestWebsocketMessage_BadUsage(t *testing.T) {
 	chain := newMockChain(t)
 
diff --git a/websocket_test.go b/websocket_test.go
index bd82d0f..fb88c7b 100644
--- a/websocket_test.go
+++ b/websocket_test.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/gorilla/websocket"
+	"github.com/stretchr/testify/assert"
 )
 
 func noWsPreSteps(ws *Websocket) {}
@@ -21,6 +22,7 @@ func TestWebsocket_Failed(t *testing.T) {
 
 	ws.Conn()
 	ws.Raw()
+	ws.Alias("foo")
 	ws.WithReadTimeout(0)
 	ws.WithoutReadTimeout()
 	ws.WithWriteTimeout(0)
@@ -44,6 +46,17 @@ func TestWebsocket_Failed(t *testing.T) {
 	ws.Close()
 }
 
+func TestWebsocket_Alias(t *testing.T) {
+	reporter := newMockReporter(t)
+	value1 := NewWebsocketC(Config{Reporter: reporter}, newMockWebsocketConn())
+	assert.Equal(t, []string{"Websocket()"}, value1.chain.context.Path)
+	assert.Equal(t, []string{"Websocket()"}, value1.chain.context.AliasedPath)
+
+	value2 := value1.Alias("foo")
+	assert.Equal(t, []string{"Websocket()"}, value2.chain.context.Path)
+	assert.Equal(t, []string{"foo"}, value2.chain.context.AliasedPath)
+}
+
 func TestWebsocket_NilConn(t *testing.T) {
 	config := Config{
 		Reporter: newMockReporter(t),