Repository: gavv/httpexpect (go)

Issue:
Support colored output
Modify DefaultFormatter to support colorized output.

It should provide three color modes, controlled by an option:

* auto (used by default) - enable colors if testing.Verbose() is true and stdout is a tty / console
* always - always use colors
* never - never use colors

Color support should work for both unix and windows.

Colors should be injected into failure template using template functions, but should not be present in FormatData. This way users that use DefaultFormatter with custom templates won't be affected.

Pull request:
Support colored output
close #161 

tried using https://github.com/TylerBrock/colorjson but not really sure since all jsonObj inputs are string (all color become green as default)
use configured color in template instead

need to change e2e_report_test.go because the color attribute add more length to string

Patch:
diff --git a/assertion.go b/assertion.go
index 6354a43..a66a007 100644
--- a/assertion.go
+++ b/assertion.go
@@ -134,6 +134,10 @@ type AssertionContext struct {
 	// Environment shared between tests
 	// Comes from Expect instance
 	Environment *Environment
+
+	// Whether reporter is known to output to testing.TB
+	// For example, true when reporter is testing.T or testify-based reporter.
+	TestingTB bool
 }
 
 // AssertionFailure provides detailed information about failed assertion.
diff --git a/chain.go b/chain.go
index cb54ae0..6d99a94 100644
--- a/chain.go
+++ b/chain.go
@@ -126,6 +126,8 @@ func newChainWithConfig(name string, config Config) *chain {
 		c.context.Environment = newEnvironment(c)
 	}
 
+	c.context.TestingTB = isTestingTB(c.handler)
+
 	return c
 }
 
@@ -154,6 +156,8 @@ func newChainWithDefaults(name string, reporter Reporter) *chain {
 
 	c.context.Environment = newEnvironment(c)
 
+	c.context.TestingTB = isTestingTB(c.handler)
+
 	return c
 }
 
@@ -486,3 +490,16 @@ func (c *chain) assertFlags(t testing.TB, flags chainFlags) {
 	assert.Equal(t, flags, c.flags,
 		"expected: chain has specified flags")
 }
+
+// Whether handler outputs to testing.TB
+func isTestingTB(in AssertionHandler) bool {
+	h, ok := in.(*DefaultAssertionHandler)
+	if !ok {
+		return false
+	}
+	switch h.Reporter.(type) {
+	case *AssertReporter, *RequireReporter, *FatalReporter, testing.TB:
+		return true
+	}
+	return false
+}
diff --git a/formatter.go b/formatter.go
index aaf53dc..4f692a9 100644
--- a/formatter.go
+++ b/formatter.go
@@ -5,16 +5,26 @@ import (
 	"encoding/json"
 	"fmt"
 	"math"
+	"os"
 	"strconv"
 	"strings"
+	"sync"
+	"testing"
 	"text/template"
 
+	"github.com/fatih/color"
+	"github.com/mattn/go-isatty"
 	"github.com/mitchellh/go-wordwrap"
 	"github.com/sanity-io/litter"
 	"github.com/yudai/gojsondiff"
 	"github.com/yudai/gojsondiff/formatter"
 )
 
+var (
+	colorsSupportedOnce sync.Once
+	colorsSupportedFlag bool
+)
+
 // Formatter is used to format assertion messages into strings.
 type Formatter interface {
 	FormatSuccess(*AssertionContext) string
@@ -65,6 +75,10 @@ type DefaultFormatter struct {
 	// defines the function map passed to template engine.
 	// May be nil.
 	TemplateFuncs template.FuncMap
+
+	// Colorization mode.
+	// Default is ColorModeAuto.
+	ColorMode ColorMode
 }
 
 // FormatSuccess implements Formatter.FormatSuccess.
@@ -112,6 +126,26 @@ const (
 	FloatFormatScientific
 )
 
+// ColorMode defines how the text color is enabled.
+type ColorMode int
+
+const (
+	// Automatically use colors if ALL of the following is true:
+	//  - stdout is a tty / console
+	//  - AssertionHandler is known to output to testing.T
+	//  - testing.Verbose() is true
+	//
+	// Colors are disabled if TERM is "dumb" or NO_COLOR environment
+	// variable is set.
+	ColorModeAuto ColorMode = iota
+
+	// Always use colors.
+	ColorModeAlways
+
+	// Never use colors.
+	ColorModeNever
+)
+
 // FormatData defines data passed to template engine when DefaultFormatter
 // formats assertion. You can use these fields in your custom templates.
 type FormatData struct {
@@ -143,6 +177,8 @@ type FormatData struct {
 	Diff     string
 
 	LineWidth int
+
+	EnableColors bool
 }
 
 const (
@@ -188,7 +224,7 @@ func (f *DefaultFormatter) buildFormatData(
 ) *FormatData {
 	data := FormatData{}
 
-	f.fillDescription(&data, ctx)
+	f.fillGeneral(&data, ctx)
 
 	if failure != nil {
 		data.AssertType = failure.Type.String()
@@ -218,7 +254,7 @@ func (f *DefaultFormatter) buildFormatData(
 	return &data
 }
 
-func (f *DefaultFormatter) fillDescription(
+func (f *DefaultFormatter) fillGeneral(
 	data *FormatData, ctx *AssertionContext,
 ) {
 	if !f.DisableNames {
@@ -239,6 +275,22 @@ func (f *DefaultFormatter) fillDescription(
 	} else {
 		data.LineWidth = defaultLineWidth
 	}
+
+	colorsSupportedOnce.Do(func() {
+		fd := os.Stdout.Fd()
+		colorsSupportedFlag = (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)) &&
+			len(os.Getenv("NO_COLOR")) == 0 &&
+			!strings.HasPrefix(os.Getenv("TERM"), "dumb")
+	})
+
+	switch f.ColorMode {
+	case ColorModeAuto:
+		data.EnableColors = ctx.TestingTB && testing.Verbose() && colorsSupportedFlag
+	case ColorModeAlways:
+		data.EnableColors = true
+	case ColorModeNever:
+		data.EnableColors = false
+	}
 }
 
 func (f *DefaultFormatter) fillErrors(
@@ -618,6 +670,25 @@ const (
 	defaultLineWidth = 60
 )
 
+var defaultColors = map[string]color.Attribute{
+	// regular
+	"Black":   color.FgBlack,
+	"Red":     color.FgRed,
+	"Green":   color.FgGreen,
+	"Yellow":  color.FgYellow,
+	"Magenta": color.FgMagenta,
+	"Cyan":    color.FgCyan,
+	"White":   color.FgWhite,
+	// bright
+	"HiBlack":   color.FgHiBlack,
+	"HiRed":     color.FgHiRed,
+	"HiGreen":   color.FgHiGreen,
+	"HiYellow":  color.FgHiYellow,
+	"HiMagenta": color.FgHiMagenta,
+	"HiCyan":    color.FgHiCyan,
+	"HiWhite":   color.FgHiWhite,
+}
+
 var defaultTemplateFuncs = template.FuncMap{
 	"indent": func(s string) string {
 		var sb strings.Builder
@@ -679,6 +750,16 @@ var defaultTemplateFuncs = template.FuncMap{
 
 		return sb.String()
 	},
+	"color": func(enable bool, colorName, s string) string {
+		if !enable {
+			return s
+		}
+		colorAttr := color.Reset
+		if ca, ok := defaultColors[colorName]; ok {
+			colorAttr = ca
+		}
+		return color.New(colorAttr).Sprint(s)
+	},
 }
 
 var defaultSuccessTemplate = `[OK] {{ join .AssertPath .LineWidth }}`
@@ -686,23 +767,23 @@ var defaultSuccessTemplate = `[OK] {{ join .AssertPath .LineWidth }}`
 var defaultFailureTemplate = `
 {{- range $n, $err := .Errors }}
 {{ if eq $n 0 -}}
-{{ wrap $err $.LineWidth }}
+{{ wrap $err $.LineWidth | color $.EnableColors "Red" }}
 {{- else -}}
-{{ wrap $err $.LineWidth | indent }}
+{{ wrap $err $.LineWidth | indent | color $.EnableColors "Red" }}
 {{- end -}}
 {{- end -}}
 {{- if .TestName }}
 
-test name: {{ .TestName }}
+test name: {{ .TestName | color $.EnableColors "Cyan" }}
 {{- end -}}
 {{- if .RequestName }}
 
-request name: {{ .RequestName }}
+request name: {{ .RequestName | color $.EnableColors "Cyan" }}
 {{- end -}}
 {{- if .AssertPath }}
 
 assertion:
-{{ join .AssertPath .LineWidth | indent }}
+{{ join .AssertPath .LineWidth | indent | color .EnableColors "Yellow" }}
 {{- end -}}
 {{- if .HaveExpected }}
 
@@ -711,23 +792,23 @@ assertion:
 {{- else }}expected
 {{- end }} {{ .ExpectedKind }}:
 {{- range $n, $exp := .Expected }}
-{{ $exp | indent }}
+{{ $exp | indent | color $.EnableColors "HiMagenta" }}
 {{- end -}}
 {{- end -}}
 {{- if .HaveActual }}
 
 actual value:
-{{ .Actual | indent }}
+{{ .Actual | indent | color .EnableColors "HiMagenta" }}
 {{- end -}}
 {{- if .HaveReference }}
 
 reference value:
-{{ .Reference | indent }}
+{{ .Reference | indent | color .EnableColors "HiMagenta" }}
 {{- end -}}
 {{- if .HaveDelta }}
 
 allowed delta:
-{{ .Delta | indent }}
+{{ .Delta | indent | color .EnableColors "HiMagenta" }}
 {{- end -}}
 {{- if .HaveDiff }}
 
diff --git a/go.mod b/go.mod
index 72aff61..739f8b7 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,12 @@ go 1.16
 require (
 	github.com/ajg/form v1.5.1
 	github.com/fasthttp/websocket v1.4.3-rc.6
+	github.com/fatih/color v1.13.0
 	github.com/fatih/structs v1.1.0
 	github.com/google/go-querystring v1.1.0
 	github.com/gorilla/websocket v1.4.2
 	github.com/imkira/go-interpol v1.1.0
+	github.com/mattn/go-isatty v0.0.17
 	github.com/mitchellh/go-wordwrap v1.0.1
 	github.com/sanity-io/litter v1.5.5
 	github.com/stretchr/testify v1.5.0
@@ -16,18 +18,19 @@ require (
 	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0
 	github.com/yudai/gojsondiff v1.0.0
-	golang.org/x/net v0.7.0
 	moul.io/http2curl/v2 v2.3.0
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/gobwas/glob v0.2.3
-	github.com/mattn/go-colorable v0.1.2 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/onsi/ginkgo v1.10.1 // indirect
 	github.com/onsi/gomega v1.7.0 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
 	github.com/yudai/pp v2.0.1+incompatible // indirect
+	golang.org/x/net v0.0.0-20220722155237-a158d28d115b
+	golang.org/x/text v0.7.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 03b4245..6b7653c 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/fasthttp/websocket v1.4.3-rc.6 h1:omHqsl8j+KXpmzRjF8bmzOSYJ8GnS0E3efi1wYT+niY=
 github.com/fasthttp/websocket v1.4.3-rc.6/go.mod h1:43W9OM2T8FeXpCWMsBd9Cb7nE2CACNqNvCqQCoty/Lc=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -30,10 +32,14 @@ github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/C
 github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
 github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
-github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -98,31 +104,31 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=


Test patch:
diff --git a/chain_test.go b/chain_test.go
index 638bf06..0318dc4 100644
--- a/chain_test.go
+++ b/chain_test.go
@@ -701,3 +701,107 @@ func TestChain_Reporting(t *testing.T) {
 	assert.NotNil(t, handler.failure)
 	assert.Equal(t, failure, *handler.failure)
 }
+
+func TestChain_TestingTB(t *testing.T) {
+	type args struct {
+		handler  AssertionHandler
+		reporter Reporter
+	}
+	cases := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "testing.T",
+			args: args{
+				handler: &DefaultAssertionHandler{
+					Formatter: newMockFormatter(t),
+					Reporter:  t,
+					Logger:    newMockLogger(t),
+				},
+				reporter: t,
+			},
+			want: true,
+		},
+		{
+			name: "testing.B",
+			args: args{
+				handler: &DefaultAssertionHandler{
+					Formatter: newMockFormatter(t),
+					Reporter:  &testing.B{},
+					Logger:    newMockLogger(t),
+				},
+				reporter: &testing.B{},
+			},
+			want: true,
+		},
+		{
+			name: "testing.TB",
+			args: args{
+				handler: &DefaultAssertionHandler{
+					Formatter: newMockFormatter(t),
+					Reporter:  testing.TB(t),
+					Logger:    newMockLogger(t),
+				},
+				reporter: testing.TB(t),
+			},
+			want: true,
+		},
+		{
+			name: "AssertReporter",
+			args: args{
+				handler: &DefaultAssertionHandler{
+					Formatter: newMockFormatter(t),
+					Reporter:  NewFatalReporter(t),
+					Logger:    newMockLogger(t),
+				},
+				reporter: NewFatalReporter(t),
+			},
+			want: true,
+		},
+		{
+			name: "AssertReporter",
+			args: args{
+				handler: &DefaultAssertionHandler{
+					Formatter: newMockFormatter(t),
+					Reporter:  NewAssertReporter(t),
+					Logger:    newMockLogger(t),
+				},
+				reporter: NewAssertReporter(t),
+			},
+			want: true,
+		},
+		{
+			name: "RequireReporter",
+			args: args{
+				handler: &DefaultAssertionHandler{
+					Formatter: newMockFormatter(t),
+					Reporter:  NewRequireReporter(t),
+					Logger:    newMockLogger(t),
+				},
+				reporter: NewRequireReporter(t),
+			},
+			want: true,
+		},
+		{
+			name: "mockHandler, mockReporter",
+			args: args{
+				handler:  &mockAssertionHandler{},
+				reporter: newMockReporter(t),
+			},
+			want: false,
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			chain := newChainWithConfig(tc.name, Config{
+				AssertionHandler: tc.args.handler,
+			}.withDefaults())
+			assert.Equal(t, tc.want, chain.context.TestingTB)
+
+			chain = newChainWithDefaults(tc.name, tc.args.reporter)
+			assert.Equal(t, tc.want, chain.context.TestingTB)
+		})
+	}
+}
diff --git a/formatter_test.go b/formatter_test.go
index 46df0b3..527bafb 100644
--- a/formatter_test.go
+++ b/formatter_test.go
@@ -856,3 +856,32 @@ func TestFormatter_FormatDiff(t *testing.T) {
 		check([]interface{}{}, []interface{}{})
 	})
 }
+
+func TestFormatter_ColorMode(t *testing.T) {
+	cases := []struct {
+		name string
+		mode ColorMode
+		want bool
+	}{
+		{
+			name: "always",
+			mode: ColorModeAlways,
+			want: true,
+		},
+		{
+			name: "never",
+			mode: ColorModeNever,
+			want: false,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			f := DefaultFormatter{
+				ColorMode: tc.mode,
+			}
+			fd := f.buildFormatData(&AssertionContext{}, &AssertionFailure{})
+			assert.Equal(t, tc.want, fd.EnableColors)
+		})
+	}
+}
diff --git a/string_test.go b/string_test.go
index 8960829..71daed8 100644
--- a/string_test.go
+++ b/string_test.go
@@ -175,19 +175,19 @@ func TestString_Getters(t *testing.T) {
 
 func TestString_IsEmpty(t *testing.T) {
 	cases := []struct {
-		name      string
-		str       string
-		wantEmpty bool
+		name    string
+		str     string
+		isEmpty bool
 	}{
 		{
-			name:      "empty string",
-			str:       "",
-			wantEmpty: true,
+			name:    "empty string",
+			str:     "",
+			isEmpty: true,
 		},
 		{
-			name:      "populated string",
-			str:       "a",
-			wantEmpty: false,
+			name:    "non-empty string",
+			str:     "foo",
+			isEmpty: false,
 		},
 	}
 
@@ -195,16 +195,14 @@ func TestString_IsEmpty(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			reporter := newMockReporter(t)
 
-			if tc.wantEmpty {
+			if tc.isEmpty {
 				NewString(reporter, tc.str).IsEmpty().
 					chain.assertNotFailed(t)
-
 				NewString(reporter, tc.str).NotEmpty().
 					chain.assertFailed(t)
 			} else {
 				NewString(reporter, tc.str).IsEmpty().
 					chain.assertFailed(t)
-
 				NewString(reporter, tc.str).NotEmpty().
 					chain.assertNotFailed(t)
 			}
@@ -213,349 +211,389 @@ func TestString_IsEmpty(t *testing.T) {
 }
 
 func TestString_IsEqual(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "foo")
-
-	assert.Equal(t, "foo", value.Raw())
-
-	value.IsEqual("foo")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.IsEqual("FOO")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.NotEqual("FOO")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.NotEqual("foo")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-}
-
-func TestString_IsEqualFold(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "foo")
-
-	value.IsEqualFold("foo")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.IsEqualFold("FOO")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.IsEqualFold("foo2")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	cases := []struct {
+		name        string
+		str         string
+		value       string
+		isEqual     bool
+		isEqualFold bool
+	}{
+		{
+			name:        "equivalent string",
+			str:         "foo",
+			value:       "foo",
+			isEqual:     true,
+			isEqualFold: true,
+		},
+		{
+			name:        "non-equivalent string",
+			str:         "foo",
+			value:       "bar",
+			isEqual:     false,
+			isEqualFold: false,
+		},
+		{
+			name:        "different case",
+			str:         "foo",
+			value:       "FOO",
+			isEqual:     false,
+			isEqualFold: true,
+		},
+	}
 
-	value.NotEqualFold("foo")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	for _, tc := range cases {
+		reporter := newMockReporter(t)
 
-	value.NotEqualFold("FOO")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		if tc.isEqual {
+			NewString(reporter, tc.str).IsEqual(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotEqual(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).IsEqual(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotEqual(tc.value).
+				chain.assertNotFailed(t)
+		}
 
-	value.NotEqualFold("foo2")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+		if tc.isEqualFold {
+			NewString(reporter, tc.str).IsEqualFold(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotEqualFold(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).IsEqualFold(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotEqualFold(tc.value).
+				chain.assertNotFailed(t)
+		}
+	}
 }
 
 func TestString_InList(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.NotInList()
-	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_InListFold(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "Foo")
-
-	assert.Equal(t, "Foo", value.Raw())
+	t.Run("basic", func(t *testing.T) {
+		cases := []struct {
+			name       string
+			str        string
+			value      []string
+			inList     bool
+			inListFold bool
+		}{
+			{
+				name:       "in list",
+				str:        "foo",
+				value:      []string{"foo", "bar"},
+				inList:     true,
+				inListFold: true,
+			},
+			{
+				name:       "not in list",
+				str:        "baz",
+				value:      []string{"foo", "bar"},
+				inList:     false,
+				inListFold: false,
+			},
+			{
+				name:       "different case",
+				str:        "FOO",
+				value:      []string{"foo", "bar"},
+				inList:     false,
+				inListFold: true,
+			},
+		}
 
-	value.InListFold()
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		for _, tc := range cases {
+			reporter := newMockReporter(t)
 
-	value.NotInListFold()
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+			if tc.inList {
+				NewString(reporter, tc.str).InList(tc.value...).
+					chain.assertNotFailed(t)
+				NewString(reporter, tc.str).NotInList(tc.value...).
+					chain.assertFailed(t)
+			} else {
+				NewString(reporter, tc.str).InList(tc.value...).
+					chain.assertFailed(t)
+				NewString(reporter, tc.str).NotInList(tc.value...).
+					chain.assertNotFailed(t)
+			}
 
-	value.InListFold("foo", "bar")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+			if tc.inListFold {
+				NewString(reporter, tc.str).InListFold(tc.value...).
+					chain.assertNotFailed(t)
+				NewString(reporter, tc.str).NotInListFold(tc.value...).
+					chain.assertFailed(t)
+			} else {
+				NewString(reporter, tc.str).InListFold(tc.value...).
+					chain.assertFailed(t)
+				NewString(reporter, tc.str).NotInListFold(tc.value...).
+					chain.assertNotFailed(t)
+			}
+		}
+	})
 
-	value.InListFold("FOO", "BAR")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+	t.Run("invalid argument", func(t *testing.T) {
+		reporter := newMockReporter(t)
 
-	value.InListFold("BAR", "BAZ")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		NewString(reporter, "foo").
+			InList(). // empty list
+			chain.assertFailed(t)
 
-	value.NotInListFold("foo", "bar")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		NewString(reporter, "foo").
+			NotInList(). // empty list
+			chain.assertFailed(t)
 
-	value.NotInListFold("FOO", "BAR")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		NewString(reporter, "foo").
+			InListFold(). // empty list
+			chain.assertFailed(t)
 
-	value.NotInListFold("BAR", "BAZ")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+		NewString(reporter, "foo").
+			NotInListFold(). // empty list
+			chain.assertFailed(t)
+	})
 }
 
 func TestString_Contains(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "11-foo-22")
-
-	value.Contains("foo")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.Contains("FOO")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.NotContains("FOO")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.NotContains("foo")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-}
-
-func TestString_ContainsFold(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "11-foo-22")
-
-	value.ContainsFold("foo")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.ContainsFold("FOO")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.ContainsFold("foo3")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	cases := []struct {
+		name         string
+		str          string
+		value        string
+		contains     bool
+		containsFold bool
+	}{
+		{
+			name:         "contains",
+			str:          "11-foo-22",
+			value:        "foo",
+			contains:     true,
+			containsFold: true,
+		},
+		{
+			name:         "not contains",
+			str:          "11-foo-22",
+			value:        "bar",
+			contains:     false,
+			containsFold: false,
+		},
+		{
+			name:         "different case",
+			str:          "11-foo-22",
+			value:        "FOO",
+			contains:     false,
+			containsFold: true,
+		},
+	}
 
-	value.NotContainsFold("foo")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	for _, tc := range cases {
+		reporter := newMockReporter(t)
 
-	value.NotContainsFold("FOO")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		if tc.contains {
+			NewString(reporter, tc.str).Contains(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotContains(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).Contains(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotContains(tc.value).
+				chain.assertNotFailed(t)
+		}
 
-	value.NotContainsFold("foo3")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+		if tc.containsFold {
+			NewString(reporter, tc.str).ContainsFold(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotContainsFold(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).ContainsFold(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotContainsFold(tc.value).
+				chain.assertNotFailed(t)
+		}
+	}
 }
 
 func TestString_HasPrefix(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "Hello World")
-
-	value.HasPrefix("Hello")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefix("Hello World")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefix("")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefix("Hello!")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefix("hello")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefix("World")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	cases := []struct {
+		name          string
+		str           string
+		value         string
+		hasPrefix     bool
+		hasPrefixFold bool
+	}{
+		{
+			name:          "has prefix",
+			str:           "Hello World",
+			value:         "Hello",
+			hasPrefix:     true,
+			hasPrefixFold: true,
+		},
+		{
+			name:          "full match",
+			str:           "Hello World",
+			value:         "Hello World",
+			hasPrefix:     true,
+			hasPrefixFold: true,
+		},
+		{
+			name:          "empty string",
+			str:           "Hello World",
+			value:         "",
+			hasPrefix:     true,
+			hasPrefixFold: true,
+		},
+		{
+			name:          "extra char",
+			str:           "Hello World",
+			value:         "Hello!",
+			hasPrefix:     false,
+			hasPrefixFold: false,
+		},
+		{
+			name:          "different case",
+			str:           "Hello World",
+			value:         "hell",
+			hasPrefix:     false,
+			hasPrefixFold: true,
+		},
+		{
+			name:          "different case extra char",
+			str:           "Hello World",
+			value:         "hella",
+			hasPrefix:     false,
+			hasPrefixFold: false,
+		},
+		{
+			name:          "different case full match",
+			str:           "Hello World",
+			value:         "hELLO wORLD",
+			hasPrefix:     false,
+			hasPrefixFold: true,
+		},
+	}
 
-	value.NotHasPrefix("Bye")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+	for _, tc := range cases {
+		reporter := newMockReporter(t)
 
-	value.NotHasPrefix("Hello")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		if tc.hasPrefix {
+			NewString(reporter, tc.str).HasPrefix(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotHasPrefix(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).HasPrefix(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotHasPrefix(tc.value).
+				chain.assertNotFailed(t)
+		}
 
-	value.NotHasPrefix("hello")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+		if tc.hasPrefixFold {
+			NewString(reporter, tc.str).HasPrefixFold(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotHasPrefixFold(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).HasPrefixFold(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotHasPrefixFold(tc.value).
+				chain.assertNotFailed(t)
+		}
+	}
 }
 
 func TestString_HasSuffix(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "Hello World")
-
-	value.HasSuffix("World")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffix("Hello World")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffix("")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefix("World!")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffix("world")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffix("Hello")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.NotHasSuffix("Bye")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.NotHasSuffix("World")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.NotHasSuffix("world")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-}
-
-func TestString_HasPrefixFold(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "Hello World")
-
-	value.HasPrefixFold("hello")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefixFold("HeLlO wOrLd")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefixFold("")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasPrefixFold("World")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.NotHasPrefixFold("Bye")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.NotHasPrefixFold("world")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.NotHasPrefixFold("world!")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.NotHasPrefixFold("hello")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-}
-
-func TestString_HasSuffixFold(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "Hello World")
-
-	value.HasSuffixFold("world")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffixFold("hElLo WoRlD")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffixFold("")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffixFold("hello")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
-
-	value.HasSuffixFold("world!")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	cases := []struct {
+		name          string
+		str           string
+		value         string
+		hasSuffix     bool
+		hasSuffixFold bool
+	}{
+		{
+			name:          "has suffix",
+			str:           "Hello World",
+			value:         "World",
+			hasSuffix:     true,
+			hasSuffixFold: true,
+		},
+		{
+			name:          "full match",
+			str:           "Hello World",
+			value:         "Hello World",
+			hasSuffix:     true,
+			hasSuffixFold: true,
+		},
+		{
+			name:          "empty string",
+			str:           "Hello World",
+			value:         "",
+			hasSuffix:     true,
+			hasSuffixFold: true,
+		},
+		{
+			name:          "extra char",
+			str:           "Hello World",
+			value:         "!World",
+			hasSuffix:     false,
+			hasSuffixFold: false,
+		},
+		{
+			name:          "different case",
+			str:           "Hello World",
+			value:         "WORLD",
+			hasSuffix:     false,
+			hasSuffixFold: true,
+		},
+		{
+			name:          "different case extra char",
+			str:           "Hello World",
+			value:         "!WORLD",
+			hasSuffix:     false,
+			hasSuffixFold: false,
+		},
+		{
+			name:          "different case full match",
+			str:           "Hello World",
+			value:         "hELLO wORLD",
+			hasSuffix:     false,
+			hasSuffixFold: true,
+		},
+	}
 
-	value.NotHasSuffixFold("Bye")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+	for _, tc := range cases {
+		reporter := newMockReporter(t)
 
-	value.NotHasSuffixFold("world")
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		if tc.hasSuffix {
+			NewString(reporter, tc.str).HasSuffix(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotHasSuffix(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).HasSuffix(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotHasSuffix(tc.value).
+				chain.assertNotFailed(t)
+		}
 
-	value.NotHasSuffixFold("world!")
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+		if tc.hasSuffixFold {
+			NewString(reporter, tc.str).HasSuffixFold(tc.value).
+				chain.assertNotFailed(t)
+			NewString(reporter, tc.str).NotHasSuffixFold(tc.value).
+				chain.assertFailed(t)
+		} else {
+			NewString(reporter, tc.str).HasSuffixFold(tc.value).
+				chain.assertFailed(t)
+			NewString(reporter, tc.str).NotHasSuffixFold(tc.value).
+				chain.assertNotFailed(t)
+		}
+	}
 }
 
-func TestString_MatchOne(t *testing.T) {
-	reporter := newMockReporter(t)
+func TestString_Match(t *testing.T) {
+	t.Run("named", func(t *testing.T) {
+		reporter := newMockReporter(t)
 
-	value := NewString(reporter, "http://example.com/users/john")
+		value := NewString(reporter, "http://example.com/users/john")
 
-	t.Run("named", func(t *testing.T) {
 		m := value.Match(`http://(?P<host>.+)/users/(?P<user>.+)`)
 		m.chain.assertNotFailed(t)
 
@@ -565,6 +603,10 @@ func TestString_MatchOne(t *testing.T) {
 	})
 
 	t.Run("unnamed", func(t *testing.T) {
+		reporter := newMockReporter(t)
+
+		value := NewString(reporter, "http://example.com/users/john")
+
 		m := value.Match(`http://(.+)/users/(.+)`)
 		m.chain.assertNotFailed(t)
 
@@ -572,85 +614,98 @@ func TestString_MatchOne(t *testing.T) {
 			[]string{"http://example.com/users/john", "example.com", "john"},
 			m.submatches)
 	})
-}
 
-func TestString_MatchAll(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter,
-		"http://example.com/users/john http://example.com/users/bob")
-
-	m := value.MatchAll(`http://(\S+)/users/(\S+)`)
-
-	assert.Equal(t, 2, len(m))
-
-	m[0].chain.assertNotFailed(t)
-	m[1].chain.assertNotFailed(t)
-
-	assert.Equal(t,
-		[]string{"http://example.com/users/john", "example.com", "john"},
-		m[0].submatches)
-
-	assert.Equal(t,
-		[]string{"http://example.com/users/bob", "example.com", "bob"},
-		m[1].submatches)
-}
-
-func TestString_MatchStatus(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value := NewString(reporter, "a")
-
-	value.Match(`a`)
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.MatchAll(`a`)
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
-
-	value.NotMatch(`a`)
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	t.Run("all", func(t *testing.T) {
+		reporter := newMockReporter(t)
 
-	value.Match(`[^a]`)
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		value := NewString(reporter,
+			"http://example.com/users/john http://example.com/users/bob")
 
-	value.MatchAll(`[^a]`)
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		m := value.MatchAll(`http://(\S+)/users/(\S+)`)
 
-	value.NotMatch(`[^a]`)
-	value.chain.assertNotFailed(t)
-	value.chain.clearFailed()
+		assert.Equal(t, 2, len(m))
 
-	assert.Equal(t, []string{}, value.Match(`[^a]`).submatches)
-	assert.Equal(t, []Match{}, value.MatchAll(`[^a]`))
-}
+		m[0].chain.assertNotFailed(t)
+		m[1].chain.assertNotFailed(t)
 
-func TestString_MatchInvalid(t *testing.T) {
-	reporter := newMockReporter(t)
+		assert.Equal(t,
+			[]string{"http://example.com/users/john", "example.com", "john"},
+			m[0].submatches)
 
-	value := NewString(reporter, "a")
+		assert.Equal(t,
+			[]string{"http://example.com/users/bob", "example.com", "bob"},
+			m[1].submatches)
+	})
 
-	value.Match(`[`)
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+	t.Run("status", func(t *testing.T) {
+		cases := []struct {
+			str      string
+			re       string
+			match    bool
+			notMatch bool
+		}{
+			{
+				str:      `a`,
+				re:       `a`,
+				match:    true,
+				notMatch: false,
+			},
+			{
+				str:      `a`,
+				re:       `[^a]`,
+				match:    false,
+				notMatch: true,
+			},
+			{
+				str:      `a`,
+				re:       `[`,
+				match:    false,
+				notMatch: false,
+			},
+		}
 
-	value.MatchAll(`[`)
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+		for _, tc := range cases {
+			t.Run(tc.str, func(t *testing.T) {
+				reporter := newMockReporter(t)
 
-	value.NotMatch(`[`)
-	value.chain.assertFailed(t)
-	value.chain.clearFailed()
+				var value *String
+
+				if tc.match {
+					value = NewString(reporter, tc.str)
+					value.Match(tc.re).chain.assertNotFailed(t)
+					value.chain.assertNotFailed(t)
+
+					value = NewString(reporter, tc.str)
+					value.MatchAll(tc.re)
+					value.chain.assertNotFailed(t)
+				} else {
+					value = NewString(reporter, tc.str)
+					value.Match(tc.re).chain.assertFailed(t)
+					value.chain.assertFailed(t)
+
+					value = NewString(reporter, tc.str)
+					value.MatchAll(tc.re)
+					value.chain.assertFailed(t)
+				}
+
+				if tc.notMatch {
+					value = NewString(reporter, tc.str)
+					value.NotMatch(tc.re)
+					value.chain.assertNotFailed(t)
+				} else {
+					value = NewString(reporter, tc.str)
+					value.NotMatch(tc.re)
+					value.chain.assertFailed(t)
+				}
+			})
+		}
+	})
 }
 
 func TestString_IsAscii(t *testing.T) {
 	cases := []struct {
-		str       string
-		wantASCII bool
+		str     string
+		isASCII bool
 	}{
 		{"Ascii", true},
 		{string(rune(127)), true},
@@ -659,21 +714,19 @@ func TestString_IsAscii(t *testing.T) {
 		{string(rune(128)), false},
 	}
 
-	for _, value := range cases {
-		t.Run(value.str, func(t *testing.T) {
+	for _, tc := range cases {
+		t.Run(tc.str, func(t *testing.T) {
 			reporter := newMockReporter(t)
 
-			if value.wantASCII {
-				NewString(reporter, value.str).IsASCII().
+			if tc.isASCII {
+				NewString(reporter, tc.str).IsASCII().
 					chain.assertNotFailed(t)
-
-				NewString(reporter, value.str).NotASCII().
+				NewString(reporter, tc.str).NotASCII().
 					chain.assertFailed(t)
 			} else {
-				NewString(reporter, value.str).IsASCII().
+				NewString(reporter, tc.str).IsASCII().
 					chain.assertFailed(t)
-
-				NewString(reporter, value.str).NotASCII().
+				NewString(reporter, tc.str).NotASCII().
 					chain.assertNotFailed(t)
 			}
 		})