Repository: gavv/httpexpect
Pull request #256 (2023-01-30 20:33:39)

Add InList and NotInList
This PR does the following:
- Add `InList` and `NotInList` for given types:
  - [x] Value
  - [x] Object
  - [x] Array
  - [x] String
  - [x] Number
  - [x] Boolean (just for completeness)
  - [x] Duration
  - [x] DateTime
-Since `AssertionList` rejects a single slice as a param, single array param in `InList` will behave like `IsEqual` and `NotInList` will behave like `NotEqual`. Addressed in commit https://github.com/gavv/httpexpect/commit/649ee8caf0e8c918aa9a2de8a8d6f6c5c3ec1cf7

https://github.com/gavv/httpexpect/blob/86f366a2d10b8a8ddd3cb1a9b2a5c00f37a1df68/assertion_validation.go#L186-L190

For issue https://github.com/gavv/httpexpect/issues/250

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

Coverage: 94.824% (+0.3%) from 94.555% when pulling **b06cca5905d0d5af35101b4722bd37f3b6d8ef52 on fahimbagar:add-InList-and-NotInList** into **86f366a2d10b8a8ddd3cb1a9b2a5c00f37a1df68 on gavv:master**.

gavv: I think we should have separate check for zero-length list.

Such call will always fail, but AssertBelongs will look confusing.

Since it anyway will fail, let's forbid this case. If length of list is zero, let's fail with AssertUsage with error "unexpected empty list".
fahimbagar: > Since it anyway will fail, let's forbid this case. If length of list is zero, let's fail with AssertUsage with error "unexpected empty list argument".

> Also would be nice to cover these usage checks with tests.

Addressed both, let me know if I miss out anything. Also removed unneeded `canonArray` in https://github.com/gavv/httpexpect/pull/256/commits/b7fc213ce722a152f674015a898290fe3475e965 and https://github.com/gavv/httpexpect/pull/256/commits/b885435c99ef42ba2d7c82b34021ef29f6de229c

Patch:
diff --git a/array.go b/array.go
index 4b9a33a..9d3d946 100644
--- a/array.go
+++ b/array.go
@@ -746,6 +746,115 @@ func (a *Array) Equal(value interface{}) *Array {
 	return a.IsEqual(value)
 }
 
+// InList succeeds if whole array is equal to one of the elements from given
+// list of arrays.
+// Before comparison, both array and each value are converted to canonical
+// form.
+//
+// Each value should be a slice of any type.
+//
+// Example:
+//
+//	array := NewArray(t, []interface{}{"foo", 123})
+//	array.InList([]interface{}{"foo", 123}, []interface{}{"bar", "456"})
+func (a *Array) InList(values ...interface{}) *Array {
+	opChain := a.chain.enter("InList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return a
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return a
+	}
+
+	var isListed bool
+	for _, v := range values {
+		expected, ok := canonArray(opChain, v)
+		if !ok {
+			return a
+		}
+
+		if reflect.DeepEqual(expected, a.value) {
+			isListed = true
+			break
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{a.value},
+			Expected: &AssertionValue{AssertionList(values)},
+			Errors: []error{
+				errors.New("expected: arrays is equal to one of the values"),
+			},
+		})
+	}
+
+	return a
+}
+
+// NotInList succeeds if whole array is not equal to any of the elements
+// from given list of arrays.
+// Before comparison, both array and each value are converted to canonical
+// form.
+//
+// Each value should be a slice of any type.
+//
+// Example:
+//
+//	array := NewArray(t, []interface{}{"foo", 123})
+//	array.NotInList([]interface{}{"bar", 456}, []interface{}{"baz", "foo"})
+func (a *Array) NotInList(values ...interface{}) *Array {
+	opChain := a.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return a
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return a
+	}
+
+	for _, v := range values {
+		expected, ok := canonArray(opChain, v)
+		if !ok {
+			return a
+		}
+
+		if reflect.DeepEqual(expected, a.value) {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{a.value},
+				Expected: &AssertionValue{AssertionList(values)},
+				Errors: []error{
+					errors.New("expected: arrays is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return a
+}
+
 // IsEqualUnordered succeeds if array is equal to another array, ignoring element
 // order. Before comparison, both arrays are converted to canonical form.
 //
diff --git a/assertion_validation.go b/assertion_validation.go
index 3edcb4f..275c670 100644
--- a/assertion_validation.go
+++ b/assertion_validation.go
@@ -3,7 +3,6 @@ package httpexpect
 import (
 	"errors"
 	"fmt"
-	"reflect"
 )
 
 func validateAssertion(failure *AssertionFailure) error {
@@ -11,6 +10,14 @@ func validateAssertion(failure *AssertionFailure) error {
 		return errors.New("AssertionFailure should have non-empty Errors list")
 	}
 
+	for _, err := range failure.Errors {
+		if err == nil {
+			return errors.New("AssertionFailure should not have nil in Errors")
+		} else if isNil(err) {
+			return errors.New("AssertionFailure should not have typed-nil in Errors")
+		}
+	}
+
 	if err := validateType(failure); err != nil {
 		return err
 	}
@@ -182,12 +189,6 @@ func validateTraits(failure *AssertionFailure, traits fieldTraits) error {
 				if len(lst) == 0 {
 					return errors.New("AssertionList should be non-empty")
 				}
-
-				if len(lst) == 1 && reflect.ValueOf(lst[0]).Kind() == reflect.Slice {
-					return errors.New(
-						"AssertionList should contain a list of values," +
-							" but it contains a single element which itself is a list")
-				}
 			}
 		}
 	}
diff --git a/boolean.go b/boolean.go
index 86120b1..ac977d0 100644
--- a/boolean.go
+++ b/boolean.go
@@ -162,6 +162,97 @@ func (b *Boolean) Equal(value bool) *Boolean {
 	return b.IsEqual(value)
 }
 
+// InList succeeds if boolean is equal to one of the elements from given
+// list of booleans.
+//
+// Example:
+//
+//	boolean := NewBoolean(t, true)
+//	boolean.InList(true, false)
+func (b *Boolean) InList(values ...bool) *Boolean {
+	opChain := b.chain.enter("InList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return b
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return b
+	}
+
+	var isListed bool
+	for _, v := range values {
+		if b.value == v {
+			isListed = true
+			break
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{b.value},
+			Expected: &AssertionValue{AssertionList(boolList(values))},
+			Errors: []error{
+				errors.New("expected: boolean is equal to one of the values"),
+			},
+		})
+	}
+
+	return b
+}
+
+// NotInList succeeds if boolean is not equal to any of the elements from
+// given list of booleans.
+//
+// Example:
+//
+//	boolean := NewBoolean(t, true)
+//	boolean.NotInList(true, false)
+func (b *Boolean) NotInList(values ...bool) *Boolean {
+	opChain := b.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return b
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return b
+	}
+
+	for _, v := range values {
+		if b.value == v {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{b.value},
+				Expected: &AssertionValue{AssertionList(boolList(values))},
+				Errors: []error{
+					errors.New("expected: boolean is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return b
+}
+
 // True succeeds if boolean is true.
 //
 // Example:
@@ -217,3 +308,12 @@ func (b *Boolean) False() *Boolean {
 
 	return b
 }
+
+func boolList(values []bool) []interface{} {
+	l := make([]interface{}, 0, len(values))
+	for _, v := range values {
+		l = append(l, v)
+	}
+
+	return l
+}
diff --git a/datetime.go b/datetime.go
index 5de349e..fa6af2e 100644
--- a/datetime.go
+++ b/datetime.go
@@ -422,6 +422,97 @@ func (dt *DateTime) NotInRange(min, max time.Time) *DateTime {
 	return dt
 }
 
+// InList succeeds if DateTime is equal to one of the elements from given
+// list of time.Time.
+//
+// Example:
+//
+//	dt := NewDateTime(t, time.Unix(0, 2))
+//	dt.InRange(time.Unix(0, 1), time.Unix(0, 2))
+func (dt *DateTime) InList(values ...time.Time) *DateTime {
+	opChain := dt.chain.enter("InRange()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return dt
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return dt
+	}
+
+	var isListed bool
+	for _, v := range values {
+		if dt.value.Equal(v) {
+			isListed = true
+			break
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{dt.value},
+			Expected: &AssertionValue{AssertionList(timeList(values))},
+			Errors: []error{
+				errors.New("expected: time point is equal to one of the values"),
+			},
+		})
+	}
+
+	return dt
+}
+
+// NotInList succeeds if DateTime is not equal to any of the elements from
+// given list of time.Time.
+//
+// Example:
+//
+//	dt := NewDateTime(t, time.Unix(0, 2))
+//	dt.InRange(time.Unix(0, 1), time.Unix(0, 3))
+func (dt *DateTime) NotInList(values ...time.Time) *DateTime {
+	opChain := dt.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return dt
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return dt
+	}
+
+	for _, v := range values {
+		if dt.value.Equal(v) {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{dt.value},
+				Expected: &AssertionValue{AssertionList(timeList(values))},
+				Errors: []error{
+					errors.New("expected: time point is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return dt
+}
+
 // Gt succeeds if DateTime is greater than given value.
 //
 // Example:
@@ -569,3 +660,12 @@ func (dt *DateTime) AsLocal() *DateTime {
 
 	return newDateTime(opChain, dt.value.Local())
 }
+
+func timeList(values []time.Time) []interface{} {
+	l := make([]interface{}, 0, len(values))
+	for _, v := range values {
+		l = append(l, v)
+	}
+
+	return l
+}
diff --git a/duration.go b/duration.go
index 35e64d8..78d02e6 100644
--- a/duration.go
+++ b/duration.go
@@ -429,3 +429,126 @@ func (d *Duration) NotInRange(min, max time.Duration) *Duration {
 
 	return d
 }
+
+// InList succeeds if Duration is equal to one of the elements from given
+// list of time.Duration.
+//
+// Example:
+//
+//	d := NewDuration(t, time.Minute)
+//	d.InList(time.Minute, time.Hour)
+func (d *Duration) InList(values ...time.Duration) *Duration {
+	opChain := d.chain.enter("InList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return d
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return d
+	}
+
+	if d.value == nil {
+		opChain.fail(AssertionFailure{
+			Type:   AssertNotNil,
+			Actual: &AssertionValue{d.value},
+			Errors: []error{
+				errors.New("expected: duration is present"),
+			},
+		})
+		return d
+	}
+
+	var isListed bool
+	for _, v := range values {
+		if *d.value == v {
+			isListed = true
+			break
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{d.value},
+			Expected: &AssertionValue{AssertionList(durationList(values))},
+			Errors: []error{
+				errors.New("expected: duration is equal to one of the values"),
+			},
+		})
+	}
+
+	return d
+}
+
+// NotInList succeeds if Duration is not equal to any of the elements from
+// given list of time.Duration.
+//
+// Example:
+//
+//	d := NewDuration(t, time.Minute)
+//	d.NotInList(time.Second, time.Hour)
+func (d *Duration) NotInList(values ...time.Duration) *Duration {
+	opChain := d.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return d
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return d
+	}
+
+	if d.value == nil {
+		opChain.fail(AssertionFailure{
+			Type:   AssertNotNil,
+			Actual: &AssertionValue{d.value},
+			Errors: []error{
+				errors.New("expected: duration is present"),
+			},
+		})
+
+		return d
+	}
+
+	for _, v := range values {
+		if *d.value == v {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{d.value},
+				Expected: &AssertionValue{AssertionList(durationList(values))},
+				Errors: []error{
+					errors.New("expected: duration is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return d
+}
+
+func durationList(values []time.Duration) []interface{} {
+	l := make([]interface{}, 0, len(values))
+	for _, v := range values {
+		l = append(l, v)
+	}
+
+	return l
+}
diff --git a/formatter.go b/formatter.go
index c02ed9a..e094158 100644
--- a/formatter.go
+++ b/formatter.go
@@ -4,8 +4,8 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"net/http"
-	"reflect"
+	"math"
+	"strconv"
 	"strings"
 	"text/template"
 
@@ -45,6 +45,10 @@ type DefaultFormatter struct {
 	// Exclude diff from failure report.
 	DisableDiffs bool
 
+	// Float printing format.
+	// Default is FloatFormatAuto.
+	FloatFormat FloatFormat
+
 	// Wrap text to keep lines below given width.
 	// Use zero for default width, and negative value to disable wrapping.
 	LineWidth int
@@ -87,6 +91,27 @@ func (f *DefaultFormatter) FormatFailure(
 	}
 }
 
+// FloatFormat defines the format in which all floats are printed.
+type FloatFormat int
+
+const (
+	// Print floats in scientific notation for large exponents,
+	// otherwise print in decimal notation.
+	// Precision is the smallest needed to identify the value uniquely.
+	// Similar to %g format.
+	FloatFormatAuto FloatFormat = iota
+
+	// Always print floats in decimal notation.
+	// Precision is the smallest needed to identify the value uniquely.
+	// Similar to %f format.
+	FloatFormatDecimal
+
+	// Always print floats in scientific notation.
+	// Precision is the smallest needed to identify the value uniquely.
+	// Similar to %e format.
+	FloatFormatScientific
+)
+
 // FormatData defines data passed to template engine when DefaultFormatter
 // formats assertion. You can use these fields in your custom templates.
 type FormatData struct {
@@ -220,7 +245,7 @@ func (f *DefaultFormatter) fillErrors(
 	data *FormatData, ctx *AssertionContext, failure *AssertionFailure,
 ) {
 	for _, err := range failure.Errors {
-		if err == nil {
+		if isNil(err) {
 			continue
 		}
 		data.Errors = append(data.Errors, err.Error())
@@ -236,7 +261,7 @@ func (f *DefaultFormatter) fillActual(
 
 	case AssertType, AssertNotType:
 		data.HaveActual = true
-		data.Actual = f.formatTyped(failure.Actual.Value)
+		data.Actual = f.formatTypedValue(failure.Actual.Value)
 
 	default:
 		data.HaveActual = true
@@ -278,27 +303,27 @@ func (f *DefaultFormatter) fillExpected(
 	case AssertInRange, AssertNotInRange:
 		data.HaveExpected = true
 		data.ExpectedKind = kindRange
-		data.Expected = f.formatRange(failure.Expected.Value)
+		data.Expected = f.formatRangeValue(failure.Expected.Value)
 
 	case AssertMatchSchema, AssertNotMatchSchema:
 		data.HaveExpected = true
 		data.ExpectedKind = kindSchema
 		data.Expected = []string{
-			f.formatBareString(failure.Expected.Value),
+			f.formatMatchValue(failure.Expected.Value),
 		}
 
 	case AssertMatchPath, AssertNotMatchPath:
 		data.HaveExpected = true
 		data.ExpectedKind = kindPath
 		data.Expected = []string{
-			f.formatBareString(failure.Expected.Value),
+			f.formatMatchValue(failure.Expected.Value),
 		}
 
 	case AssertMatchRegexp, AssertNotMatchRegexp:
 		data.HaveExpected = true
 		data.ExpectedKind = kindRegexp
 		data.Expected = []string{
-			f.formatBareString(failure.Expected.Value),
+			f.formatMatchValue(failure.Expected.Value),
 		}
 
 	case AssertMatchFormat, AssertNotMatchFormat:
@@ -308,7 +333,7 @@ func (f *DefaultFormatter) fillExpected(
 		} else {
 			data.ExpectedKind = kindFormat
 		}
-		data.Expected = f.formatList(failure.Expected.Value)
+		data.Expected = f.formatListValue(failure.Expected.Value)
 
 	case AssertContainsKey, AssertNotContainsKey:
 		data.HaveExpected = true
@@ -334,7 +359,7 @@ func (f *DefaultFormatter) fillExpected(
 	case AssertBelongs, AssertNotBelongs:
 		data.HaveExpected = true
 		data.ExpectedKind = kindValueList
-		data.Expected = f.formatList(failure.Expected.Value)
+		data.Expected = f.formatListValue(failure.Expected.Value)
 	}
 }
 
@@ -401,17 +426,13 @@ func (f *DefaultFormatter) fillDelta(
 	data.Delta = f.formatValue(failure.Delta.Value)
 }
 
-func (f *DefaultFormatter) formatTyped(value interface{}) string {
-	if isNumber(value) {
-		return fmt.Sprintf("%T(%v)", value, f.formatValue(value))
+func (f *DefaultFormatter) formatValue(value interface{}) string {
+	if flt := extractFloat32(value); flt != nil {
+		return f.formatFloatValue(*flt, 32)
 	}
 
-	return fmt.Sprintf("%T(%#v)", value, value)
-}
-
-func (f *DefaultFormatter) formatValue(value interface{}) string {
-	if isNumber(value) {
-		return fmt.Sprintf("%v", value)
+	if flt := extractFloat64(value); flt != nil {
+		return f.formatFloatValue(*flt, 64)
 	}
 
 	if !isNil(value) && !isHTTP(value) {
@@ -428,20 +449,46 @@ func (f *DefaultFormatter) formatValue(value interface{}) string {
 	sq := litter.Options{
 		Separator: defaultIndent,
 	}
-
 	return sq.Sdump(value)
 }
 
-func (f *DefaultFormatter) formatBareString(value interface{}) string {
-	switch v := value.(type) {
-	case string:
-		return v
+func (f *DefaultFormatter) formatFloatValue(value float64, bits int) string {
+	switch f.FloatFormat {
+	case FloatFormatAuto:
+		if _, frac := math.Modf(value); frac != 0 {
+			return strconv.FormatFloat(value, 'g', -1, bits)
+		} else {
+			return strconv.FormatFloat(value, 'f', -1, bits)
+		}
+
+	case FloatFormatDecimal:
+		return strconv.FormatFloat(value, 'f', -1, bits)
+
+	case FloatFormatScientific:
+		return strconv.FormatFloat(value, 'e', -1, bits)
+
+	default:
+		return fmt.Sprintf("%v", value)
+	}
+}
+
+func (f *DefaultFormatter) formatTypedValue(value interface{}) string {
+	if isNumber(value) {
+		return fmt.Sprintf("%T(%v)", value, f.formatValue(value))
+	}
+
+	return fmt.Sprintf("%T(%#v)", value, value)
+}
+
+func (f *DefaultFormatter) formatMatchValue(value interface{}) string {
+	if str := extractString(value); str != nil {
+		return *str
 	}
 
 	return f.formatValue(value)
 }
 
-func (f *DefaultFormatter) formatRange(value interface{}) []string {
+func (f *DefaultFormatter) formatRangeValue(value interface{}) []string {
 	if rng := exctractRange(value); rng != nil {
 		if isNumber(rng.Min) && isNumber(rng.Max) {
 			return []string{
@@ -460,7 +507,7 @@ func (f *DefaultFormatter) formatRange(value interface{}) []string {
 	}
 }
 
-func (f *DefaultFormatter) formatList(value interface{}) []string {
+func (f *DefaultFormatter) formatListValue(value interface{}) []string {
 	if lst := extractList(value); lst != nil {
 		s := make([]string, 0, len(*lst))
 		for _, e := range *lst {
@@ -514,6 +561,34 @@ func (f *DefaultFormatter) formatDiff(expected, actual interface{}) (string, boo
 	return diffText, true
 }
 
+func extractString(value interface{}) *string {
+	switch s := value.(type) {
+	case string:
+		return &s
+	default:
+		return nil
+	}
+}
+
+func extractFloat32(value interface{}) *float64 {
+	switch f := value.(type) {
+	case float32:
+		ff := float64(f)
+		return &ff
+	default:
+		return nil
+	}
+}
+
+func extractFloat64(value interface{}) *float64 {
+	switch f := value.(type) {
+	case float64:
+		return &f
+	default:
+		return nil
+	}
+}
+
 func exctractRange(value interface{}) *AssertionRange {
 	switch rng := value.(type) {
 	case AssertionRange:
@@ -536,35 +611,6 @@ func extractList(value interface{}) *AssertionList {
 	}
 }
 
-func isNil(value interface{}) bool {
-	defer func() {
-		_ = recover()
-	}()
-	return value == nil || reflect.ValueOf(value).IsNil()
-}
-
-func isNumber(value interface{}) bool {
-	defer func() {
-		_ = recover()
-	}()
-	reflect.ValueOf(value).Convert(reflect.TypeOf(float64(0))).Float()
-	return true
-}
-
-func isHTTP(value interface{}) bool {
-	switch value.(type) {
-	case *http.Client, http.Client,
-		*http.Transport, http.Transport,
-		*http.Request, http.Request,
-		*http.Response, http.Response,
-		*http.Header, http.Header,
-		*http.Cookie, http.Cookie:
-		return true
-	default:
-		return false
-	}
-}
-
 const (
 	defaultIndent    = "  "
 	defaultLineWidth = 60
diff --git a/number.go b/number.go
index 0ab4a0f..4606fd0 100644
--- a/number.go
+++ b/number.go
@@ -366,6 +366,113 @@ func (n *Number) NotInRange(min, max interface{}) *Number {
 	return n
 }
 
+// InList succeeds if number is equal to one of the elements from given
+// list of numbers.
+// Before comparison, each value is converted to canonical form.
+//
+// Each value should be numeric type convertible to float64.
+//
+// Example:
+//
+//	number := NewNumber(t, 123)
+//	number.InList(float64(123), int32(123))
+func (n *Number) InList(values ...interface{}) *Number {
+	opChain := n.chain.enter("IsList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return n
+	}
+
+	var isListed bool
+	for _, v := range values {
+		num, ok := canonNumber(opChain, v)
+		if !ok {
+			return n
+		}
+
+		if n.value == num {
+			isListed = true
+			break
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{n.value},
+			Expected: &AssertionValue{AssertionList(values)},
+			Errors: []error{
+				errors.New("expected: number is equal to one of the values"),
+			},
+		})
+	}
+
+	return n
+}
+
+// NotInList succeeds if whole array is not equal to any of the elements
+// from given list of numbers.
+// Before comparison, each value is converted to canonical form.
+//
+// Each value should be numeric type convertible to float64.
+//
+// Example:
+//
+//	number := NewNumber(t, 123)
+//	number.NotInList(float64(456), int32(456))
+func (n *Number) NotInList(values ...interface{}) *Number {
+	opChain := n.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return n
+	}
+
+	for _, v := range values {
+		num, ok := canonNumber(opChain, v)
+		if !ok {
+			return n
+		}
+
+		if n.value == num {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{n.value},
+				Expected: &AssertionValue{AssertionList(values)},
+				Errors: []error{
+					errors.New("expected: number is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return n
+}
+
 // Gt succeeds if number is greater than given value.
 //
 // value should have numeric type convertible to float64. Before comparison,
diff --git a/object.go b/object.go
index df80dc2..cfe21af 100644
--- a/object.go
+++ b/object.go
@@ -744,6 +744,119 @@ func (o *Object) Equal(value interface{}) *Object {
 	return o.IsEqual(value)
 }
 
+// InList succeeds if whole object is equal to one of the elements from given
+// list of objects.
+// Before comparison, each object is converted to canonical form.
+//
+// Each value should be map[string]interface{} or struct.
+//
+// Example:
+//
+//	object := NewObject(t, map[string]interface{}{"foo": 123})
+//	object.InList(
+//		map[string]interface{}{"foo": 123},
+//		map[string]interface{}{"bar": 456},
+//	)
+func (o *Object) InList(values ...interface{}) *Object {
+	opChain := o.chain.enter("InList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return o
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return o
+	}
+
+	var isListed bool
+	for _, v := range values {
+		expected, ok := canonMap(opChain, v)
+		if !ok {
+			return o
+		}
+
+		if reflect.DeepEqual(expected, o.value) {
+			isListed = true
+			break
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{o.value},
+			Expected: &AssertionValue{AssertionList(values)},
+			Errors: []error{
+				errors.New("expected: map is equal to one of the values"),
+			},
+		})
+	}
+
+	return o
+}
+
+// NotInList succeeds if whole object is equal to any of the elements from
+// given list of objects.
+// Before comparison, each object is converted to canonical form.
+//
+// Each value should be map[string]interface{} or struct.
+//
+// Example:
+//
+//	object := NewObject(t, map[string]interface{}{"foo": 123})
+//	object.NotInList(
+//		map[string]interface{}{"bar": 456},
+//		map[string]interface{}{"baz": 789},
+//	)
+func (o *Object) NotInList(values ...interface{}) *Object {
+	opChain := o.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return o
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return o
+	}
+
+	for _, v := range values {
+		expected, ok := canonMap(opChain, v)
+		if !ok {
+			return o
+		}
+
+		if reflect.DeepEqual(expected, o.value) {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{o.value},
+				Expected: &AssertionValue{AssertionList(values)},
+				Errors: []error{
+					errors.New("expected: map is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return o
+}
+
 // ContainsKey succeeds if object contains given key.
 //
 // Example:
diff --git a/string.go b/string.go
index f6b9757..834c5ec 100644
--- a/string.go
+++ b/string.go
@@ -245,6 +245,96 @@ func (s *String) Equal(value string) *String {
 	return s.IsEqual(value)
 }
 
+// InList succeeds if string is equal to one of the elements from given
+// list of strings.
+//
+// Example:
+//
+//	str := NewString(t, "Hello")
+//	str.InList("Hello", "Goodbye")
+func (s *String) InList(values ...string) *String {
+	opChain := s.chain.enter("InList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return s
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return s
+	}
+
+	var isListed bool
+	for _, v := range values {
+		if s.value == v {
+			isListed = true
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{AssertionList(stringList(values))},
+			Errors: []error{
+				errors.New("expected: string is equal to one of the values"),
+			},
+		})
+	}
+
+	return s
+}
+
+// NotInList succeeds if string is not equal to any of the elements from
+// given list of strings.
+//
+// Example:
+//
+//	str := NewString(t, "Hello")
+//	str.NotInList("NotInList", "Goodbye")
+func (s *String) NotInList(values ...string) *String {
+	opChain := s.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return s
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return s
+	}
+
+	for _, v := range values {
+		if s.value == v {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{s.value},
+				Expected: &AssertionValue{AssertionList(stringList(values))},
+				Errors: []error{
+					errors.New("expected: string is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return s
+}
+
 // IsEqualFold succeeds if string is equal to given Go string after applying Unicode
 // case-folding (so it's a case-insensitive match).
 //
@@ -1142,3 +1232,12 @@ func (s *String) Number() *Number {
 func (s *String) DateTime(layout ...string) *DateTime {
 	return s.AsDateTime(layout...)
 }
+
+func stringList(values []string) []interface{} {
+	s := make([]interface{}, 0, len(values))
+	for _, v := range values {
+		s = append(s, v)
+	}
+
+	return s
+}
diff --git a/traits.go b/traits.go
new file mode 100644
index 0000000..3c46da8
--- /dev/null
+++ b/traits.go
@@ -0,0 +1,35 @@
+package httpexpect
+
+import (
+	"net/http"
+	"reflect"
+)
+
+func isNil(value interface{}) bool {
+	defer func() {
+		_ = recover()
+	}()
+	return value == nil || reflect.ValueOf(value).IsNil()
+}
+
+func isNumber(value interface{}) bool {
+	defer func() {
+		_ = recover()
+	}()
+	reflect.ValueOf(value).Convert(reflect.TypeOf(float64(0))).Float()
+	return true
+}
+
+func isHTTP(value interface{}) bool {
+	switch value.(type) {
+	case *http.Client, http.Client,
+		*http.Transport, http.Transport,
+		*http.Request, http.Request,
+		*http.Response, http.Response,
+		*http.Header, http.Header,
+		*http.Cookie, http.Cookie:
+		return true
+	default:
+		return false
+	}
+}
diff --git a/value.go b/value.go
index 19632d5..c32bd1d 100644
--- a/value.go
+++ b/value.go
@@ -523,3 +523,106 @@ func (v *Value) NotEqual(value interface{}) *Value {
 func (v *Value) Equal(value interface{}) *Value {
 	return v.IsEqual(value)
 }
+
+// InList succeeds if whole value is equal to one of the elements from given
+// list of values (e.g. map, slice, string, etc).
+// Before comparison, each value are converted to canonical form.
+//
+// Example:
+//
+//	value := NewValue(t, "foo")
+//	value.InList("foo", map[string]interface{}{"bar": true})
+func (v *Value) InList(values ...interface{}) *Value {
+	opChain := v.chain.enter("InList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return v
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return v
+	}
+
+	var isListed bool
+	for _, val := range values {
+		expected, ok := canonValue(opChain, val)
+		if !ok {
+			return v
+		}
+
+		if reflect.DeepEqual(expected, v.value) {
+			isListed = true
+			break
+		}
+	}
+
+	if !isListed {
+		opChain.fail(AssertionFailure{
+			Type:     AssertBelongs,
+			Actual:   &AssertionValue{v.value},
+			Expected: &AssertionValue{AssertionList(values)},
+			Errors: []error{
+				errors.New("expected: value is equal to one of the values"),
+			},
+		})
+	}
+
+	return v
+}
+
+// NotInList succeeds if whole value is not equal to any of the elements from
+// given list of values (e.g. map, slice, string, etc).
+// Before comparison, each value are converted to canonical form.
+//
+// Example:
+//
+//	value := NewValue(t, "foo")
+//	value.NotInList("bar", map[string]interface{}{"bar": true})
+func (v *Value) NotInList(values ...interface{}) *Value {
+	opChain := v.chain.enter("NotInList()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return v
+	}
+
+	if len(values) == 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected empty list argument"),
+			},
+		})
+
+		return v
+	}
+
+	for _, val := range values {
+		expected, ok := canonValue(opChain, val)
+		if !ok {
+			return v
+		}
+
+		if reflect.DeepEqual(expected, v.value) {
+			opChain.fail(AssertionFailure{
+				Type:     AssertNotBelongs,
+				Actual:   &AssertionValue{v.value},
+				Expected: &AssertionValue{AssertionList(values)},
+				Errors: []error{
+					errors.New("expected: value is not equal to any of the values"),
+				},
+			})
+			break
+		}
+	}
+
+	return v
+}


Test patch:
diff --git a/array_test.go b/array_test.go
index 0206229..dc58c00 100644
--- a/array_test.go
+++ b/array_test.go
@@ -31,6 +31,8 @@ func TestArray_Failed(t *testing.T) {
 		value.NotEmpty()
 		value.IsEqual([]interface{}{})
 		value.NotEqual([]interface{}{})
+		value.InList([]interface{}{})
+		value.NotInList([]interface{}{})
 		value.IsEqualUnordered([]interface{}{})
 		value.NotEqualUnordered([]interface{}{})
 		value.ConsistsOf("foo")
@@ -377,6 +379,62 @@ func TestArray_EqualNotEmpty(t *testing.T) {
 	value.chain.clearFailed()
 }
 
+func TestArray_InList(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewArray(reporter, []interface{}{"foo", "bar"})
+
+	assert.Equal(t, []interface{}{"foo", "bar"}, value.Raw())
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList("foo", "bar")
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList("foo", "bar")
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList([]interface{}{})
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList([]interface{}{})
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList([]interface{}{"bar", "foo"})
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList([]interface{}{"bar", "foo"})
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList([]interface{}{"bar", "foo"}, []interface{}{"foo", "bar"})
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList([]interface{}{"bar", "foo"}, []interface{}{"foo", "bar"})
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList([]interface{}{"bar", "foo"}, []interface{}{"FOO", "BAR"})
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList([]interface{}{"bar", "foo"}, []interface{}{"FOO", "BAR"})
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+}
+
 func TestArray_EqualTypes(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/assertion_test.go b/assertion_test.go
index 765160b..37d650a 100644
--- a/assertion_test.go
+++ b/assertion_test.go
@@ -8,6 +8,12 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+type typedErrorNil int
+
+func (*typedErrorNil) Error() string {
+	return ""
+}
+
 func TestAssertion_Handler(t *testing.T) {
 	type test struct {
 		formatter *mockFormatter
@@ -336,18 +342,6 @@ func TestAssertion_ValidateTraits(t *testing.T) {
 				List: fieldRequired,
 			},
 		},
-		{
-			testName:          "AssertionList should not be slice of single element",
-			errorContainsText: "AssertionList",
-			failure: AssertionFailure{
-				Expected: &AssertionValue{
-					Value: AssertionList{[]int{1}},
-				},
-			},
-			traits: fieldTraits{
-				List: fieldRequired,
-			},
-		},
 	}
 
 	for _, test := range tests {
@@ -382,6 +376,12 @@ func TestAssertion_ValidateTraits(t *testing.T) {
 }
 
 func TestAssertion_ValidateAssertion(t *testing.T) {
+	var tnil *typedErrorNil
+	var tnilPtr error = tnil
+
+	assert.Nil(t, tnilPtr)
+	assert.NotEqual(t, nil, tnilPtr)
+
 	tests := []struct {
 		testName          string
 		errorContainsText string
@@ -406,13 +406,29 @@ func TestAssertion_ValidateAssertion(t *testing.T) {
 			},
 		},
 		{
-			testName:          "empty Errros",
+			testName:          "empty Errors",
 			errorContainsText: "Errors",
 			input: AssertionFailure{
 				Type:   AssertOperation,
 				Errors: []error{},
 			},
 		},
+		{
+			testName:          "nil in Errors",
+			errorContainsText: "Errors",
+			input: AssertionFailure{
+				Type:   AssertOperation,
+				Errors: []error{nil},
+			},
+		},
+		{
+			testName:          "typed nil in Errors",
+			errorContainsText: "Errors",
+			input: AssertionFailure{
+				Type:   AssertOperation,
+				Errors: []error{tnilPtr},
+			},
+		},
 		{
 			testName:          "denied Actual",
 			errorContainsText: "Actual",
@@ -627,18 +643,6 @@ func TestAssertion_ValidateAssertion(t *testing.T) {
 				Expected: &AssertionValue{AssertionList{}},
 			},
 		},
-		{
-			testName:          "List has one element and it's list",
-			errorContainsText: "AssertionList",
-			input: AssertionFailure{
-				Type: AssertBelongs,
-				Errors: []error{
-					errors.New("test"),
-				},
-				Actual:   &AssertionValue{},
-				Expected: &AssertionValue{AssertionList{[]string{"test"}}},
-			},
-		},
 	}
 
 	for _, test := range tests {
diff --git a/boolean_test.go b/boolean_test.go
index 4d52c2c..11e7ddb 100644
--- a/boolean_test.go
+++ b/boolean_test.go
@@ -21,6 +21,8 @@ func TestBoolean_Failed(t *testing.T) {
 
 	value.IsEqual(false)
 	value.NotEqual(false)
+	value.InList(false)
+	value.NotInList(false)
 	value.True()
 	value.False()
 }
@@ -160,6 +162,14 @@ func TestBoolean_True(t *testing.T) {
 	value.False()
 	value.chain.assertFailed(t)
 	value.chain.clearFailed()
+
+	value.InList(true, true)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(true, false)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
 }
 
 func TestBoolean_False(t *testing.T) {
@@ -192,4 +202,26 @@ func TestBoolean_False(t *testing.T) {
 	value.False()
 	value.chain.assertNotFailed(t)
 	value.chain.clearFailed()
+
+	value.InList(true, true)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(true, true)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+}
+
+func TestBoolean_ZeroLengthInList(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewBoolean(reporter, true)
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
 }
diff --git a/datetime_test.go b/datetime_test.go
index fb511bc..8da6702 100644
--- a/datetime_test.go
+++ b/datetime_test.go
@@ -25,6 +25,8 @@ func TestDateTime_Failed(t *testing.T) {
 	value.Le(tm)
 	value.InRange(tm, tm)
 	value.NotInRange(tm, tm)
+	value.InList(tm, tm)
+	value.NotInList(tm, tm)
 	value.Zone()
 	value.Year()
 	value.Month()
@@ -244,3 +246,65 @@ func TestDateTime_InRange(t *testing.T) {
 	value.chain.assertNotFailed(t)
 	value.chain.clearFailed()
 }
+
+func TestDateTime_InList(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewDateTime(reporter, time.Unix(0, 1234))
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Unix(0, 1234), time.Unix(0, 1234))
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Unix(0, 1234), time.Unix(0, 1234))
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Unix(0, 1234-1), time.Unix(0, 1234))
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Unix(0, 1234-1), time.Unix(0, 1234))
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Unix(0, 1234), time.Unix(0, 1234+1))
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Unix(0, 1234), time.Unix(0, 1234+1))
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Unix(0, 1234+1), time.Unix(0, 1234+2))
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Unix(0, 1234+1), time.Unix(0, 1234+2))
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Unix(0, 1234-2), time.Unix(0, 1234-1))
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Unix(0, 1234-2), time.Unix(0, 1234-1))
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Unix(0, 1234+1), time.Unix(0, 1234-1))
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Unix(0, 1234+1), time.Unix(0, 1234-1))
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+}
diff --git a/duration_test.go b/duration_test.go
index add4e74..4144736 100644
--- a/duration_test.go
+++ b/duration_test.go
@@ -16,6 +16,8 @@ func TestDuration_Failed(t *testing.T) {
 
 	value.IsEqual(tm)
 	value.NotEqual(tm)
+	value.InList(tm)
+	value.NotInList(tm)
 	value.Gt(tm)
 	value.Ge(tm)
 	value.Lt(tm)
@@ -223,3 +225,52 @@ func TestDuration_InRange(t *testing.T) {
 	value.chain.assertNotFailed(t)
 	value.chain.clearFailed()
 }
+
+func TestDuration_InList(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	newDuration(newMockChain(t), nil).InList(time.Second).chain.assertFailed(t)
+	newDuration(newMockChain(t), nil).NotInList(time.Second).chain.assertFailed(t)
+
+	value := NewDuration(reporter, time.Second)
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Second, time.Minute)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Second, time.Minute)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Second-1, time.Minute)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Second-1, time.Minute)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Second, time.Second+1)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Second, time.Second+1)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(time.Second+1, time.Second-1)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(time.Second+1, time.Second-1)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+}
diff --git a/formatter_test.go b/formatter_test.go
index 8955649..fed1609 100644
--- a/formatter_test.go
+++ b/formatter_test.go
@@ -7,19 +7,20 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-var mockDefaultFormatter = &DefaultFormatter{}
+type typedStingerNil int
 
-type typedNil int
-
-func (*typedNil) String() string {
+func (*typedStingerNil) String() string {
 	return ""
 }
 
 func TestFormat_Values(t *testing.T) {
+	var mockDefaultFormatter = &DefaultFormatter{}
+
 	checkAll := func(t *testing.T, fn func(interface{}) string) {
-		var tnil *typedNil
+		var tnil *typedStingerNil
 		var tnilPtr fmt.Stringer = tnil
 
 		assert.Nil(t, tnilPtr)
@@ -34,6 +35,8 @@ func TestFormat_Values(t *testing.T) {
 		check(fn(tnil))
 		check(fn(tnilPtr))
 		check(fn(123))
+		check(fn(float32(123)))
+		check(fn(float64(123)))
 		check(fn("hello"))
 		check(fn(time.Second))
 		check(fn(time.Unix(0, 0)))
@@ -46,32 +49,34 @@ func TestFormat_Values(t *testing.T) {
 		check(fn(AssertionList([]interface{}{1, 2})))
 	}
 
-	t.Run("formatTypes", func(t *testing.T) {
-		checkAll(t, mockDefaultFormatter.formatTyped)
-	})
-
 	t.Run("formatValue", func(t *testing.T) {
 		checkAll(t, mockDefaultFormatter.formatValue)
 	})
 
-	t.Run("formatBareString", func(t *testing.T) {
-		checkAll(t, mockDefaultFormatter.formatBareString)
+	t.Run("formatTypedValue", func(t *testing.T) {
+		checkAll(t, mockDefaultFormatter.formatTypedValue)
+	})
+
+	t.Run("formatMatchValue", func(t *testing.T) {
+		checkAll(t, mockDefaultFormatter.formatMatchValue)
 	})
 
-	t.Run("formatRange", func(t *testing.T) {
+	t.Run("formatRangeValue", func(t *testing.T) {
 		checkAll(t, func(v interface{}) string {
-			return strings.Join(mockDefaultFormatter.formatRange(v), "")
+			return strings.Join(mockDefaultFormatter.formatRangeValue(v), "")
 		})
 	})
 
-	t.Run("formatList", func(t *testing.T) {
+	t.Run("formatListValue", func(t *testing.T) {
 		checkAll(t, func(v interface{}) string {
-			return strings.Join(mockDefaultFormatter.formatList(v), "")
+			return strings.Join(mockDefaultFormatter.formatListValue(v), "")
 		})
 	})
 }
 
 func TestFormat_Diff(t *testing.T) {
+	var mockDefaultFormatter = &DefaultFormatter{}
+
 	checkOK := func(a, b interface{}) {
 		s, ok := mockDefaultFormatter.formatDiff(a, b)
 		assert.True(t, ok)
@@ -124,14 +129,14 @@ func TestFormat_FailureActual(t *testing.T) {
 			assertionType:  AssertType,
 			assertionValue: float32(1_000_000),
 			wantHaveActual: true,
-			wantActual:     "float32(1e+06)",
+			wantActual:     "float32(1000000)",
 		},
 		{
 			name:           "AssertType float64",
 			assertionType:  AssertType,
 			assertionValue: float64(1_000_000),
 			wantHaveActual: true,
-			wantActual:     "float64(1e+06)",
+			wantActual:     "float64(1000000)",
 		},
 		{
 			name:           "AssertType string",
@@ -168,14 +173,14 @@ func TestFormat_FailureActual(t *testing.T) {
 			assertionType:  AssertValid,
 			assertionValue: float32(1_000_000),
 			wantHaveActual: true,
-			wantActual:     "1e+06",
+			wantActual:     "1000000",
 		},
 		{
 			name:           "AssertValid float64",
 			assertionType:  AssertValid,
 			assertionValue: float64(1_000_000),
 			wantHaveActual: true,
-			wantActual:     "1e+06",
+			wantActual:     "1000000",
 		},
 		{
 			name:           "AssertValid string",
@@ -216,6 +221,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 		name             string
 		assertionType    AssertionType
 		assertionValue   interface{}
+		formatter        DefaultFormatter
 		wantHaveExpected bool
 		wantExpectedKind string
 		wantExpected     []string
@@ -252,7 +258,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 			},
 			wantHaveExpected: true,
 			wantExpectedKind: kindRange,
-			wantExpected:     []string{"[1e+06; 2e+06]"},
+			wantExpected:     []string{"[1000000; 2000000]"},
 		},
 		{
 			name:          "AssertInRange float64",
@@ -263,7 +269,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 			},
 			wantHaveExpected: true,
 			wantExpectedKind: kindRange,
-			wantExpected:     []string{"[1e+06; 2e+06]"},
+			wantExpected:     []string{"[1000000; 2000000]"},
 		},
 		{
 			name:          "AssertInRange string",
@@ -311,7 +317,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 			assertionValue:   float32(1_000_000),
 			wantHaveExpected: true,
 			wantExpectedKind: kindPath,
-			wantExpected:     []string{"1e+06"},
+			wantExpected:     []string{"1000000"},
 		},
 		{
 			name:             "AssertMatchPath float64",
@@ -319,7 +325,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 			assertionValue:   float64(1_000_000),
 			wantHaveExpected: true,
 			wantExpectedKind: kindPath,
-			wantExpected:     []string{"1e+06"},
+			wantExpected:     []string{"1000000"},
 		},
 		{
 			name:             "AssertMatchPath string",
@@ -370,7 +376,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 			},
 			wantHaveExpected: true,
 			wantExpectedKind: kindFormatList,
-			wantExpected:     []string{"1e+06", "2e+06"},
+			wantExpected:     []string{"1000000", "2000000"},
 		},
 		{
 			name:          "AssertMatchFormat float64",
@@ -381,7 +387,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 			},
 			wantHaveExpected: true,
 			wantExpectedKind: kindFormatList,
-			wantExpected:     []string{"1e+06", "2e+06"},
+			wantExpected:     []string{"1000000", "2000000"},
 		},
 		{
 			name:          "AssertMatchFormat string",
@@ -409,7 +415,6 @@ func TestFormat_FailureExpected(t *testing.T) {
 		},
 	}
 
-	df := &DefaultFormatter{}
 	ctx := &AssertionContext{}
 
 	for _, tc := range tests {
@@ -420,7 +425,7 @@ func TestFormat_FailureExpected(t *testing.T) {
 					Value: tc.assertionValue,
 				},
 			}
-			fd := df.buildFormatData(ctx, fl)
+			fd := tc.formatter.buildFormatData(ctx, fl)
 			assert.Equal(t, tc.wantHaveExpected, fd.HaveExpected)
 			assert.Equal(t, tc.wantExpectedKind, fd.ExpectedKind)
 			assert.Equal(t, tc.wantExpected, fd.Expected)
@@ -451,13 +456,13 @@ func TestFormat_FailureReference(t *testing.T) {
 			name:              "float32",
 			assertionValue:    float32(1_000_000),
 			wantHaveReference: true,
-			wantReference:     "1e+06",
+			wantReference:     "1000000",
 		},
 		{
 			name:              "float64",
 			assertionValue:    float64(1_000_000),
 			wantHaveReference: true,
-			wantReference:     "1e+06",
+			wantReference:     "1000000",
 		},
 		{
 			name:              "string",
@@ -513,13 +518,13 @@ func TestFormat_FailureDelta(t *testing.T) {
 			name:           "float32",
 			assertionValue: float32(1_000_000),
 			wantHaveDelta:  true,
-			wantDelta:      "1e+06",
+			wantDelta:      "1000000",
 		},
 		{
 			name:           "float64",
 			assertionValue: float64(1_000_000),
 			wantHaveDelta:  true,
-			wantDelta:      "1e+06",
+			wantDelta:      "1000000",
 		},
 		{
 			name:           "string",
@@ -551,3 +556,235 @@ func TestFormat_FailureDelta(t *testing.T) {
 		})
 	}
 }
+
+func TestFormatter_FloatFormat(t *testing.T) {
+	type testCase struct {
+		name     string
+		format   FloatFormat
+		value    interface{}
+		wantText string
+	}
+
+	testCases := []testCase{
+		// float32
+		{
+			name:     "float32 auto small exponent",
+			format:   FloatFormatAuto,
+			value:    float32(1.2345678),
+			wantText: "1.2345678",
+		},
+		{
+			name:     "float32 auto large exponent",
+			format:   FloatFormatAuto,
+			value:    float32(1234567.8),
+			wantText: "1.2345678e+06",
+		},
+		{
+			name:     "float32 decimal",
+			format:   FloatFormatDecimal,
+			value:    float32(1234567.8),
+			wantText: "1234567.8",
+		},
+		{
+			name:     "float32 scientific",
+			format:   FloatFormatScientific,
+			value:    float32(1.2345678),
+			wantText: "1.2345678e+00",
+		},
+		// float64
+		{
+			name:     "float64 auto small exponent",
+			format:   FloatFormatAuto,
+			value:    float64(1.23456789),
+			wantText: "1.23456789",
+		},
+		{
+			name:     "float64 auto large exponent",
+			format:   FloatFormatAuto,
+			value:    float64(12345678.9),
+			wantText: "1.23456789e+07",
+		},
+		{
+			name:     "float64 decimal",
+			format:   FloatFormatDecimal,
+			value:    float64(12345678.9),
+			wantText: "12345678.9",
+		},
+		{
+			name:     "float64 scientific",
+			format:   FloatFormatScientific,
+			value:    float64(1.23456789),
+			wantText: "1.23456789e+00",
+		},
+		// no fractional part
+		{
+			name:     "nofrac auto",
+			format:   FloatFormatAuto,
+			value:    float32(12345678),
+			wantText: "12345678",
+		},
+		{
+			name:     "nofrac decimal",
+			format:   FloatFormatDecimal,
+			value:    float32(12345678),
+			wantText: "12345678",
+		},
+		{
+			name:     "nofrac scientific",
+			format:   FloatFormatScientific,
+			value:    float32(12345678),
+			wantText: "1.2345678e+07",
+		},
+		// integer
+		{
+			name:     "integer auto",
+			format:   FloatFormatAuto,
+			value:    int(12345678),
+			wantText: "12345678",
+		},
+		{
+			name:     "integer decimal",
+			format:   FloatFormatDecimal,
+			value:    int(12345678),
+			wantText: "12345678",
+		},
+		{
+			name:     "integer scientific",
+			format:   FloatFormatScientific,
+			value:    int(12345678),
+			wantText: "12345678",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			formatter := DefaultFormatter{
+				FloatFormat: tc.format,
+			}
+			formatData := formatter.buildFormatData(
+				&AssertionContext{},
+				&AssertionFailure{
+					Type:   AssertValid,
+					Actual: &AssertionValue{tc.value},
+				})
+			assert.Equal(t, tc.wantText, formatData.Actual)
+		})
+	}
+}
+
+func TestFormatter_FloatFields(t *testing.T) {
+	type testCase struct {
+		name     string
+		format   FloatFormat
+		value    float64
+		wantText string
+	}
+
+	testCases := []testCase{
+		{
+			name:     "auto",
+			format:   FloatFormatAuto,
+			value:    1.2345678,
+			wantText: "1.2345678",
+		},
+		{
+			name:     "decimal",
+			format:   FloatFormatDecimal,
+			value:    1.2345678,
+			wantText: "1.2345678",
+		},
+		{
+			name:     "scientific",
+			format:   FloatFormatScientific,
+			value:    1.2345678,
+			wantText: "1.2345678e+00",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			formatter := DefaultFormatter{
+				FloatFormat: tc.format,
+			}
+
+			t.Run("actual", func(t *testing.T) {
+				formatData := formatter.buildFormatData(
+					&AssertionContext{},
+					&AssertionFailure{
+						Type:   AssertValid,
+						Actual: &AssertionValue{tc.value},
+					})
+				assert.Equal(t, tc.wantText, formatData.Actual)
+			})
+
+			t.Run("expected", func(t *testing.T) {
+				formatData := formatter.buildFormatData(
+					&AssertionContext{},
+					&AssertionFailure{
+						Type:     AssertEqual,
+						Actual:   &AssertionValue{},
+						Expected: &AssertionValue{tc.value},
+					})
+				require.Equal(t, 1, len(formatData.Expected))
+				assert.Equal(t, tc.wantText, formatData.Expected[0])
+			})
+
+			t.Run("reference", func(t *testing.T) {
+				formatData := formatter.buildFormatData(
+					&AssertionContext{},
+					&AssertionFailure{
+						Type:      AssertEqual,
+						Actual:    &AssertionValue{},
+						Expected:  &AssertionValue{},
+						Reference: &AssertionValue{tc.value},
+					})
+				assert.Equal(t, tc.wantText, formatData.Reference)
+			})
+
+			t.Run("delta", func(t *testing.T) {
+				formatData := formatter.buildFormatData(
+					&AssertionContext{},
+					&AssertionFailure{
+						Type:     AssertEqual,
+						Actual:   &AssertionValue{},
+						Expected: &AssertionValue{},
+						Delta:    &AssertionValue{tc.value},
+					})
+				assert.Equal(t, tc.wantText, formatData.Delta)
+			})
+
+			t.Run("range", func(t *testing.T) {
+				formatData := formatter.buildFormatData(
+					&AssertionContext{},
+					&AssertionFailure{
+						Type:   AssertInRange,
+						Actual: &AssertionValue{},
+						Expected: &AssertionValue{AssertionRange{
+							Min: tc.value,
+							Max: tc.value,
+						}},
+					})
+				require.Equal(t, 1, len(formatData.Expected))
+				assert.Equal(t,
+					fmt.Sprintf("[%s; %s]", tc.wantText, tc.wantText),
+					formatData.Expected[0])
+			})
+
+			t.Run("list", func(t *testing.T) {
+				formatData := formatter.buildFormatData(
+					&AssertionContext{},
+					&AssertionFailure{
+						Type:   AssertBelongs,
+						Actual: &AssertionValue{},
+						Expected: &AssertionValue{AssertionList{
+							tc.value,
+							tc.value,
+						}},
+					})
+				require.Equal(t, 2, len(formatData.Expected))
+				assert.Equal(t, tc.wantText, formatData.Expected[0])
+				assert.Equal(t, tc.wantText, formatData.Expected[1])
+			})
+		})
+	}
+}
diff --git a/number_test.go b/number_test.go
index 7c2a83b..6f7f0ec 100644
--- a/number_test.go
+++ b/number_test.go
@@ -22,6 +22,8 @@ func TestNumber_Failed(t *testing.T) {
 
 	value.IsEqual(0)
 	value.NotEqual(0)
+	value.InList(0)
+	value.NotInList(0)
 	value.InDelta(0, 0)
 	value.NotInDelta(0, 0)
 	value.Gt(0)
@@ -309,6 +311,52 @@ func TestNumber_InRange(t *testing.T) {
 	value.chain.clearFailed()
 }
 
+func TestNumber_InList(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewNumber(reporter, 1234)
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(1234, 4567)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(1234, 4567)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(1234.00, 4567.00)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(1234.00, 4567.00)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(4567.00, 1234.01)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(4567.00, 1234.01)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(1234+1, "1234")
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList("1234+1", 1234+2)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+}
+
 func TestNumber_Greater(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/object_test.go b/object_test.go
index 3e790a7..aa73b98 100644
--- a/object_test.go
+++ b/object_test.go
@@ -26,6 +26,8 @@ func TestObject_Failed(t *testing.T) {
 		value.NotEmpty()
 		value.IsEqual(nil)
 		value.NotEqual(nil)
+		value.InList(nil)
+		value.NotInList(nil)
 		value.ContainsKey("foo")
 		value.NotContainsKey("foo")
 		value.ContainsValue("foo")
@@ -421,6 +423,92 @@ func TestObject_Equal(t *testing.T) {
 	value.chain.clearFailed()
 }
 
+func TestObject_InList(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewObject(reporter, map[string]interface{}{"foo": 123.0})
+
+	assert.Equal(t, map[string]interface{}{"foo": 123.0}, value.Raw())
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(map[string]interface{}{})
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(map[string]interface{}{})
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(
+		map[string]interface{}{"FOO": 123.0},
+		map[string]interface{}{"BAR": 456.0},
+	)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(
+		map[string]interface{}{"FOO": 123.0},
+		map[string]interface{}{"BAR": 456.0},
+	)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(
+		map[string]interface{}{"foo": 456.0},
+		map[string]interface{}{"bar": 123.0},
+	)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(
+		map[string]interface{}{"foo": 456.0},
+		map[string]interface{}{"bar": 123.0},
+	)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(
+		map[string]interface{}{"foo": 123.0},
+		map[string]interface{}{"bar": 456.0},
+	)
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(
+		map[string]interface{}{"foo": 123.0},
+		map[string]interface{}{"bar": 456.0},
+	)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(struct {
+		Foo float64 `json:"foo"`
+	}{Foo: 123.00})
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(struct {
+		Foo float64 `json:"foo"`
+	}{Foo: 123.00})
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList(nil)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList(nil)
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+}
+
 func TestObject_EqualStruct(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/string_test.go b/string_test.go
index 03d7df2..e2a43d6 100644
--- a/string_test.go
+++ b/string_test.go
@@ -29,6 +29,8 @@ func TestString_Failed(t *testing.T) {
 	value.NotEmpty()
 	value.IsEqual("")
 	value.NotEqual("")
+	value.InList("")
+	value.NotInList("")
 	value.IsEqualFold("")
 	value.NotEqualFold("")
 	value.Contains("")
@@ -218,6 +220,38 @@ func TestString_Equal(t *testing.T) {
 	value.chain.clearFailed()
 }
 
+func TestString_List(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewString(reporter, "foo")
+
+	assert.Equal(t, "foo", value.Raw())
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList()
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.InList("foo", "bar")
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.InList("FOO", "BAR")
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList("FOO", "bar")
+	value.chain.assertNotFailed(t)
+	value.chain.clearFailed()
+
+	value.NotInList("foo", "BAR")
+	value.chain.assertFailed(t)
+	value.chain.clearFailed()
+}
+
 func TestString_EqualFold(t *testing.T) {
 	reporter := newMockReporter(t)
 
diff --git a/value_test.go b/value_test.go
index eb684bb..5d0e610 100644
--- a/value_test.go
+++ b/value_test.go
@@ -42,6 +42,9 @@ func TestValue_Failed(t *testing.T) {
 
 	value.IsEqual(nil)
 	value.NotEqual(nil)
+
+	value.InList(nil)
+	value.NotInList(nil)
 }
 
 func TestValue_Constructors(t *testing.T) {
@@ -406,6 +409,39 @@ func TestValue_Equal(t *testing.T) {
 	NewValue(reporter, data1).NotEqual(func() {}).chain.assertFailed(t)
 }
 
+func TestValue_InList(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	data1 := map[string]interface{}{"foo": "bar"}
+	data2 := "baz"
+	data3 := struct {
+		Data []int `json:"data"`
+	}{
+		Data: []int{1, 2, 3, 4},
+	}
+
+	NewValue(reporter, data1).InList().chain.assertFailed(t)
+	NewValue(reporter, data2).NotInList().chain.assertFailed(t)
+
+	NewValue(reporter, data1).InList(data1, data3).chain.assertNotFailed(t)
+	NewValue(reporter, data2).NotInList(data1, data3).chain.assertNotFailed(t)
+
+	NewValue(reporter, data1).InList(data2, data3).chain.assertFailed(t)
+	NewValue(reporter, data2).NotInList(data2, data3).chain.assertFailed(t)
+
+	NewValue(reporter, data1).InList(data2).chain.assertFailed(t)
+	NewValue(reporter, data2).NotInList(data2).chain.assertFailed(t)
+
+	NewValue(reporter, data1).InList(data1).chain.assertNotFailed(t)
+	NewValue(reporter, data2).NotInList(data1).chain.assertNotFailed(t)
+
+	NewValue(reporter, nil).InList(map[string]interface{}(nil)).chain.assertNotFailed(t)
+	NewValue(reporter, nil).NotInList(map[string]interface{}{}).chain.assertNotFailed(t)
+
+	NewValue(reporter, data1).InList(func() {}).chain.assertFailed(t)
+	NewValue(reporter, data1).NotInList(func() {}).chain.assertFailed(t)
+}
+
 func TestValue_PathObject(t *testing.T) {
 	reporter := newMockReporter(t)