Repository: gavv/httpexpect
Pull request #294 (2023-02-16 16:36:17)

Refactor string unit tests
This PR addresses refactoring the following unit tests from #284:
- [x] TestString_Empty
- [x] TestString_IsAscii
- [x] TestString_NotAscii
- [x] TestString_AsNumber
- [x] TestString_AsDateTime

I updated `TestString_IsAscii` and `TestString_NotAscii` to just reuse the same variable, `value` since it was simple and the formatting was similar to other tests. However, we could also update it to be one of the following formats instead:
* Format would be similar to TestString_AsBoolean and this could also make it easier to add additional test cases in the future
```go
func TestString_IsAscii(t *testing.T) {
	reporter := newMockReporter(t)

	asciiValues := []string{
		"Ascii",
		string(rune(127)),
	}

	notAsciiValues := []string{
		"Ascii is アスキー",
		"アスキー",
		string(rune(128)),
	}

	for _, str := range asciiValues {
		t.Run(str, func(t *testing.T) {
			value := NewString(reporter, str)

			value.IsASCII()
			value.chain.assertNotFailed(t)
		})
	}

	for _, str := range notAsciiValues {
		t.Run(str, func(t *testing.T) {
			value := NewString(reporter, str)

			value.IsASCII()
			value.chain.assertFailed(t)
		})
	}
}
```

* Alternatively, we could use a more table-driven style.
```go
func TestString_IsAscii(t *testing.T) {
	reporter := newMockReporter(t)

	cases := []struct {
		str  string
		fail bool
	}{
		{"Ascii", false},
		{string(rune(127)), false},
		{"Ascii is アスキー", true},
		{"アスキー", true},
		{string(rune(128)), true},
	}

	for _, value := range cases {
		t.Run(value.str, func(t *testing.T) {
			childValue := NewString(reporter, value.str)

			childValue.IsASCII()

			if value.fail {
				childValue.chain.assertFailed(t)
			} else {
				childValue.chain.assertNotFailed(t)
			}
		})
	}
}
```
  
I broke out `TestString_AsNumber` and `TestString_AsDateTime` into additional subtests to address the additional variables. If the subtest names should be updated, let me know.

We could also do something more table driven for those two tests as well. Like this:
```go
func TestString_AsNumber(t *testing.T) {
	reporter := newMockReporter(t)

	cases := map[string]struct {
		str         string
		base        []int
		fail        bool
		expectedNum float64
	}{
		"default_base_integer": {
			str:         "1234567",
			fail:        false,
			expectedNum: float64(1234567),
		},
		"default_base_float": {
			str:         "11.22",
			fail:        false,
			expectedNum: float64(11.22),
		},
		"default_base_bad": {
			str:  "a1",
			fail: true,
		},
		"base10_integer": {
			str:         "100",
			base:        []int{10},
			fail:        false,
			expectedNum: float64(100),
		},
		"base10_float": {
			str:         "11.22",
			base:        []int{10},
			fail:        false,
			expectedNum: float64(11.22),
		},
		"base16_integer": {
			str:         "100",
			base:        []int{16},
			fail:        false,
			expectedNum: float64(0x100),
		},
		"base16_float": {
			str:  "11.22",
			base: []int{16},
			fail: true,
		},
		"base16_large_integer": {
			str:         "4000000000000000",
			base:        []int{16},
			fail:        false,
			expectedNum: float64(0x4000000000000000),
		},
		"default_float_percision_max": {
			str:  "4611686018427387905",
			fail: true,
		},
		"base10_float_percision_max": {
			str:  "4611686018427387905",
			base: []int{10},
			fail: true,
		},
		"base16_float_percision_max": {
			str:  "8000000000000001",
			base: []int{16},
			fail: true,
		},
		"base16_float_percision_min": {
			str:  "-4000000000000001",
			base: []int{16},
			fail: true,
		},
		"multiple_base": {
			str:  "100",
			base: []int{10, 16},
			fail: true,
		},
	}

	for name, value := range cases {
		t.Run(name, func(t *testing.T) {
			childValue := NewString(reporter, value.str)
			num := childValue.AsNumber(value.base...)

			if value.fail {
				childValue.chain.assertFailed(t)
				num.chain.assertFailed(t)
				assert.Equal(t, float64(0), num.Raw())
			} else {
				childValue.chain.assertNotFailed(t)
				num.chain.assertNotFailed(t)
				assert.Equal(t, value.expectedNum, num.Raw())
			}

		})
	}
}
```

Let me know if there's any issues. I plan to use separate PRs for refactoring the other tests mentioned in #284 after this one looks good.

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

Coverage: 94.68%. Remained the same when pulling **c7b3d7542bbd8c0563510a9d38f5cb1b7a71feb9 on gpjservais:284-refactor-string-unit-tests** into **3f5a83c1b6ff8d38e4be7deb52fbf925966837ed on gavv:master**.

gavv: Thanks for PR!

Table-driven style is much appreciated. If you're willing to spend some extra bit of time to make test table-driven, this option is almost always preferred. Actually many of our tests are not tables only because of laziness :)

The patch looks good, the suggested snippets look even better, let's apply them.

Regarding TestString_IsAscii, the second one looks better, and it would be also nice to merge tests for IsAscii and NotAscii into one.
gavv: BTW also welcome to the [discord chat](https://discord.gg/5SCPCuCWA9)
gpjservais: @gavv this is ready for review again.

I ended up merging `TestString_IsAscii` and `TestString_IsAscii` into a single test and converted `TestString_Empty`, `TestString_Ascii`, and `TestString_AsNumber` into a more table-driven style. I kept `TestString_AsDateTime` as it was in my last revision though, just splitting it out into subtests.

Patch:
diff --git a/number.go b/number.go
index 32150bb..389a858 100644
--- a/number.go
+++ b/number.go
@@ -2,7 +2,9 @@ package httpexpect
 
 import (
 	"errors"
+	"fmt"
 	"math"
+	"math/big"
 )
 
 // Number provides methods to inspect attached float64 value
@@ -618,3 +620,474 @@ func (n *Number) Le(value interface{}) *Number {
 
 	return n
 }
+
+// IsInt succeeds if number is a signed integer of the specified bit width
+// as an optional argument.
+//
+// Bits argument defines maximum allowed bitness for the given number.
+// If bits is omitted, boundary check is omitted too.
+//
+// Example:
+//
+//	number := NewNumber(t, 1000000)
+//	number.IsInt()   // success
+//	number.IsInt(32) // success
+//	number.IsInt(16) // failure
+//
+//	number := NewNumber(t, -1000000)
+//	number.IsInt()   // success
+//	number.IsInt(32) // success
+//	number.IsInt(16) // failure
+//
+//	number := NewNumber(t, 0.5)
+//	number.IsInt()   // failure
+func (n *Number) IsInt(bits ...int) *Number {
+	opChain := n.chain.enter("IsInt()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if len(bits) > 1 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected multiple bits arguments"),
+			},
+		})
+		return n
+	}
+
+	if len(bits) == 1 && bits[0] <= 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected non-positive bits argument"),
+			},
+		})
+		return n
+	}
+
+	if math.IsNaN(n.value) {
+		opChain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{n.value},
+			Errors: []error{
+				errors.New("expected: number is signed integer"),
+			},
+		})
+		return n
+	}
+
+	inum, acc := big.NewFloat(n.value).Int(nil)
+	if !(acc == big.Exact) {
+		opChain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{n.value},
+			Errors: []error{
+				errors.New("expected: number is signed integer"),
+			},
+		})
+		return n
+	}
+
+	if len(bits) > 0 {
+		bitSize := bits[0]
+
+		imax := new(big.Int)
+		imax.Lsh(big.NewInt(1), uint(bitSize-1))
+		imax.Sub(imax, big.NewInt(1))
+		imin := new(big.Int)
+		imin.Neg(imax)
+		imin.Sub(imin, big.NewInt(1))
+		if inum.Cmp(imin) < 0 || inum.Cmp(imax) > 0 {
+			opChain.fail(AssertionFailure{
+				Type:   AssertInRange,
+				Actual: &AssertionValue{n.value},
+				Expected: &AssertionValue{AssertionRange{
+					Min: intBoundary{imin, -1, bitSize - 1},
+					Max: intBoundary{imax, +1, bitSize - 1},
+				}},
+				Errors: []error{
+					fmt.Errorf("expected: number is %d-bit signed integer", bitSize),
+				},
+			})
+			return n
+		}
+	}
+
+	return n
+}
+
+// NotInt succeeds if number is not a signed integer of the specified bit
+// width as an optional argument.
+//
+// Bits argument defines maximum allowed bitness for the given number.
+// If bits is omitted, boundary check is omitted too.
+//
+// Example:
+//
+//	number := NewNumber(t, 1000000)
+//	number.NotInt()   // failure
+//	number.NotInt(32) // failure
+//	number.NotInt(16) // success
+//
+//	number := NewNumber(t, -1000000)
+//	number.NotInt()   // failure
+//	number.NotInt(32) // failure
+//	number.NotInt(16) // success
+//
+//	number := NewNumber(t, 0.5)
+//	number.NotInt()   // success
+func (n *Number) NotInt(bits ...int) *Number {
+	opChain := n.chain.enter("NotInt()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if len(bits) > 1 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected multiple bits arguments"),
+			},
+		})
+		return n
+	}
+
+	if len(bits) == 1 && bits[0] <= 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected non-positive bits argument"),
+			},
+		})
+		return n
+	}
+
+	if !math.IsNaN(n.value) {
+		inum, acc := big.NewFloat(n.value).Int(nil)
+		if acc == big.Exact {
+			if len(bits) == 0 {
+				opChain.fail(AssertionFailure{
+					Type:   AssertValid,
+					Actual: &AssertionValue{n.value},
+					Errors: []error{
+						errors.New("expected: number is not signed integer"),
+					},
+				})
+				return n
+			}
+
+			bitSize := bits[0]
+			imax := new(big.Int)
+			imax.Lsh(big.NewInt(1), uint(bitSize-1))
+			imax.Sub(imax, big.NewInt(1))
+			imin := new(big.Int)
+			imin.Neg(imax)
+			imin.Sub(imin, big.NewInt(1))
+			if !(inum.Cmp(imin) < 0 || inum.Cmp(imax) > 0) {
+				opChain.fail(AssertionFailure{
+					Type:   AssertNotInRange,
+					Actual: &AssertionValue{n.value},
+					Expected: &AssertionValue{AssertionRange{
+						Min: intBoundary{imin, -1, bitSize - 1},
+						Max: intBoundary{imax, +1, bitSize - 1},
+					}},
+					Errors: []error{
+						fmt.Errorf(
+							"expected: number doesn't fit %d-bit signed integer",
+							bitSize),
+					},
+				})
+				return n
+			}
+		}
+	}
+
+	return n
+}
+
+// IsUint succeeds if number is an unsigned integer of the specified bit
+// width as an optional argument.
+//
+// Bits argument defines maximum allowed bitness for the given number.
+// If bits is omitted, boundary check is omitted too.
+//
+// Example:
+//
+//	number := NewNumber(t, 1000000)
+//	number.IsUint()   // success
+//	number.IsUint(32) // success
+//	number.IsUint(16) // failure
+//
+//	number := NewNumber(t, -1000000)
+//	number.IsUint()   // failure
+//	number.IsUint(32) // failure
+//	number.IsUint(16) // failure
+//
+//	number := NewNumber(t, 0.5)
+//	number.IsUint()   // failure
+func (n *Number) IsUint(bits ...int) *Number {
+	opChain := n.chain.enter("IsUint()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if len(bits) > 1 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected multiple bits arguments"),
+			},
+		})
+		return n
+	}
+
+	if len(bits) == 1 && bits[0] <= 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected non-positive bits argument"),
+			},
+		})
+		return n
+	}
+
+	if math.IsNaN(n.value) {
+		opChain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{n.value},
+			Errors: []error{
+				errors.New("expected: number is unsigned integer"),
+			},
+		})
+		return n
+	}
+
+	inum, acc := big.NewFloat(n.value).Int(nil)
+	if !(acc == big.Exact) {
+		opChain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{n.value},
+			Errors: []error{
+				errors.New("expected: number is unsigned integer"),
+			},
+		})
+		return n
+	}
+
+	imin := big.NewInt(0)
+	if inum.Cmp(imin) < 0 {
+		opChain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{n.value},
+			Errors: []error{
+				errors.New("expected: number is unsigned integer"),
+			},
+		})
+		return n
+	}
+
+	if len(bits) > 0 {
+		bitSize := bits[0]
+		imax := new(big.Int)
+		imax.Lsh(big.NewInt(1), uint(bitSize))
+		imax.Sub(imax, big.NewInt(1))
+		if inum.Cmp(imax) > 0 {
+			opChain.fail(AssertionFailure{
+				Type:   AssertInRange,
+				Actual: &AssertionValue{n.value},
+				Expected: &AssertionValue{AssertionRange{
+					Min: intBoundary{imin, 0, 0},
+					Max: intBoundary{imax, +1, bitSize},
+				}},
+				Errors: []error{
+					fmt.Errorf("expected: number fits %d-bit unsigned integer", bitSize),
+				},
+			})
+			return n
+		}
+	}
+
+	return n
+}
+
+// NotUint succeeds if number is not an unsigned integer of the specified bit
+// width as an optional argument.
+//
+// Bits argument defines maximum allowed bitness for the given number.
+// If bits is omitted, boundary check is omitted too.
+//
+// Example:
+//
+//	number := NewNumber(t, 1000000)
+//	number.NotUint()   // failure
+//	number.NotUint(32) // failure
+//	number.NotUint(16) // success
+//
+//	number := NewNumber(t, -1000000)
+//	number.NotUint()   // success
+//	number.NotUint(32) // success
+//	number.NotUint(16) // success
+//
+//	number := NewNumber(t, 0.5)
+//	number.NotUint()   // success
+func (n *Number) NotUint(bits ...int) *Number {
+	opChain := n.chain.enter("NotUint()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if len(bits) > 1 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected multiple bits arguments"),
+			},
+		})
+		return n
+	}
+
+	if len(bits) == 1 && bits[0] <= 0 {
+		opChain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected non-positive bits argument"),
+			},
+		})
+		return n
+	}
+
+	if !math.IsNaN(n.value) {
+		inum, acc := big.NewFloat(n.value).Int(nil)
+		if acc == big.Exact {
+			imin := big.NewInt(0)
+			if inum.Cmp(imin) >= 0 {
+				if len(bits) == 0 {
+					opChain.fail(AssertionFailure{
+						Type:   AssertValid,
+						Actual: &AssertionValue{n.value},
+						Errors: []error{
+							errors.New("expected: number is not unsigned integer"),
+						},
+					})
+					return n
+				}
+
+				bitSize := bits[0]
+				imax := new(big.Int)
+				imax.Lsh(big.NewInt(1), uint(bitSize))
+				imax.Sub(imax, big.NewInt(1))
+				if inum.Cmp(imax) <= 0 {
+					opChain.fail(AssertionFailure{
+						Type:   AssertNotInRange,
+						Actual: &AssertionValue{n.value},
+						Expected: &AssertionValue{AssertionRange{
+							Min: intBoundary{imin, 0, 0},
+							Max: intBoundary{imax, +1, bitSize},
+						}},
+						Errors: []error{
+							fmt.Errorf(
+								"expected: number doesn't fit %d-bit unsigned integer",
+								bitSize),
+						},
+					})
+					return n
+				}
+			}
+		}
+	}
+
+	return n
+}
+
+// IsFinite succeeds if number is neither ±Inf nor NaN.
+//
+// Example:
+//
+//	number := NewNumber(t, 1234.5)
+//	number.IsFinite() // success
+//
+//	number := NewNumber(t, math.NaN())
+//	number.IsFinite() // failure
+//
+//	number := NewNumber(t, math.Inf(+1))
+//	number.IsFinite() // failure
+func (n *Number) IsFinite() *Number {
+	opChain := n.chain.enter("IsFinite()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if math.IsInf(n.value, 0) || math.IsNaN(n.value) {
+		opChain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{n.value},
+			Errors: []error{
+				errors.New("expected: number is neither ±Inf nor NaN"),
+			},
+		})
+		return n
+	}
+
+	return n
+}
+
+// NotFinite succeeds if number is either ±Inf or NaN.
+//
+// Example:
+//
+//	number := NewNumber(t, 1234.5)
+//	number.NotFinite() // failure
+//
+//	number := NewNumber(t, math.NaN())
+//	number.NotFinite() // success
+//
+//	number := NewNumber(t, math.Inf(+1))
+//	number.NotFinite() // success
+func (n *Number) NotFinite() *Number {
+	opChain := n.chain.enter("NotFinite()")
+	defer opChain.leave()
+
+	if opChain.failed() {
+		return n
+	}
+
+	if !(math.IsInf(n.value, 0) || math.IsNaN(n.value)) {
+		opChain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{n.value},
+			Errors: []error{
+				errors.New("expected: number is either ±Inf or NaN"),
+			},
+		})
+		return n
+	}
+
+	return n
+}
+
+type intBoundary struct {
+	val  *big.Int
+	sign int
+	bits int
+}
+
+func (b intBoundary) String() string {
+	if b.sign > 0 {
+		return fmt.Sprintf("+2^%d-1 (+%s)", b.bits, b.val)
+	} else if b.sign < 0 {
+		return fmt.Sprintf("-2^%d   (%s)", b.bits, b.val)
+	}
+	return fmt.Sprintf("%s", b.val)
+}


Test patch:
diff --git a/number_test.go b/number_test.go
index b47164d..95161df 100644
--- a/number_test.go
+++ b/number_test.go
@@ -33,6 +33,12 @@ func TestNumber_FailedChain(t *testing.T) {
 	value.Ge(0)
 	value.Lt(0)
 	value.Le(0)
+	value.IsInt()
+	value.NotInt()
+	value.IsUint()
+	value.NotUint()
+	value.IsFinite()
+	value.NotFinite()
 }
 
 func TestNumber_Constructors(t *testing.T) {
@@ -584,3 +590,366 @@ func TestNumber_ConvertLesser(t *testing.T) {
 	value.chain.assertFailed(t)
 	value.chain.clearFailed()
 }
+
+func TestNumber_IsInt(t *testing.T) {
+	t.Run("invalid argument", func(t *testing.T) {
+		reporter := newMockReporter(t)
+
+		NewNumber(reporter, 1234).IsInt(16, 32).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).NotInt(16, 32).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).IsInt(0).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).NotInt(0).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).IsInt(-16).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).NotInt(-16).
+			chain.assertFailed(t)
+	})
+
+	t.Run("values", func(t *testing.T) {
+		tests := []struct {
+			name    string
+			value   float64
+			isInt16 bool
+			isInt32 bool
+			isInt   bool
+		}{
+			{
+				name:    "0",
+				value:   0,
+				isInt16: true,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "1",
+				value:   1,
+				isInt16: true,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "0.5",
+				value:   0.5,
+				isInt16: false,
+				isInt32: false,
+				isInt:   false,
+			},
+			{
+				name:    "NaN",
+				value:   math.NaN(),
+				isInt16: false,
+				isInt32: false,
+				isInt:   false,
+			},
+			{
+				name:    "-Inf",
+				value:   math.Inf(-1),
+				isInt16: false,
+				isInt32: false,
+				isInt:   false,
+			},
+			{
+				name:    "+Inf",
+				value:   math.Inf(+1),
+				isInt16: false,
+				isInt32: false,
+				isInt:   false,
+			},
+			{
+				name:    "MinInt16-1",
+				value:   math.MinInt16 - 1,
+				isInt16: false,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "MinInt16",
+				value:   math.MinInt16,
+				isInt16: true,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "MaxInt16",
+				value:   math.MaxInt16,
+				isInt16: true,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "MaxInt16+1",
+				value:   math.MaxInt16 + 1,
+				isInt16: false,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "MinInt32-1",
+				value:   math.MinInt32 - 1,
+				isInt16: false,
+				isInt32: false,
+				isInt:   true,
+			},
+			{
+				name:    "MinInt32",
+				value:   math.MinInt32,
+				isInt16: false,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "MaxInt32",
+				value:   math.MaxInt32,
+				isInt16: false,
+				isInt32: true,
+				isInt:   true,
+			},
+			{
+				name:    "MaxInt32+1",
+				value:   math.MaxInt32 + 1,
+				isInt16: false,
+				isInt32: false,
+				isInt:   true,
+			},
+		}
+
+		for _, tc := range tests {
+			t.Run(tc.name, func(t *testing.T) {
+				reporter := newMockReporter(t)
+
+				if tc.isInt {
+					NewNumber(reporter, tc.value).IsInt().chain.assertNotFailed(t)
+					NewNumber(reporter, tc.value).NotInt().chain.assertFailed(t)
+				} else {
+					NewNumber(reporter, tc.value).IsInt().chain.assertFailed(t)
+					NewNumber(reporter, tc.value).NotInt().chain.assertNotFailed(t)
+				}
+
+				if tc.isInt32 {
+					NewNumber(reporter, tc.value).IsInt(32).chain.assertNotFailed(t)
+					NewNumber(reporter, tc.value).NotInt(32).chain.assertFailed(t)
+				} else {
+					NewNumber(reporter, tc.value).IsInt(32).chain.assertFailed(t)
+					NewNumber(reporter, tc.value).NotInt(32).chain.assertNotFailed(t)
+				}
+
+				if tc.isInt16 {
+					NewNumber(reporter, tc.value).IsInt(16).chain.assertNotFailed(t)
+					NewNumber(reporter, tc.value).NotInt(16).chain.assertFailed(t)
+				} else {
+					NewNumber(reporter, tc.value).IsInt(16).chain.assertFailed(t)
+					NewNumber(reporter, tc.value).NotInt(16).chain.assertNotFailed(t)
+				}
+			})
+		}
+	})
+}
+
+func TestNumber_IsUint(t *testing.T) {
+	t.Run("invalid argument", func(t *testing.T) {
+		reporter := newMockReporter(t)
+
+		NewNumber(reporter, 1234).IsUint(16, 32).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).NotUint(16, 32).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).IsUint(0).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).NotUint(0).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).IsUint(-16).
+			chain.assertFailed(t)
+
+		NewNumber(reporter, 1234).NotUint(-16).
+			chain.assertFailed(t)
+	})
+
+	t.Run("values", func(t *testing.T) {
+		tests := []struct {
+			name     string
+			value    float64
+			isUint16 bool
+			isUint32 bool
+			isUint   bool
+		}{
+			{
+				name:     "0",
+				value:    0,
+				isUint16: true,
+				isUint32: true,
+				isUint:   true,
+			},
+			{
+				name:     "1",
+				value:    1,
+				isUint16: true,
+				isUint32: true,
+				isUint:   true,
+			},
+			{
+				name:     "-1",
+				value:    -1,
+				isUint16: false,
+				isUint32: false,
+				isUint:   false,
+			},
+			{
+				name:     "0.5",
+				value:    0.5,
+				isUint16: false,
+				isUint32: false,
+				isUint:   false,
+			},
+			{
+				name:     "NaN",
+				value:    math.NaN(),
+				isUint16: false,
+				isUint32: false,
+				isUint:   false,
+			},
+			{
+				name:     "-Inf",
+				value:    math.Inf(-1),
+				isUint16: false,
+				isUint32: false,
+				isUint:   false,
+			},
+			{
+				name:     "+Inf",
+				value:    math.Inf(+1),
+				isUint16: false,
+				isUint32: false,
+				isUint:   false,
+			},
+			{
+				name:     "MaxUint16",
+				value:    math.MaxUint16,
+				isUint16: true,
+				isUint32: true,
+				isUint:   true,
+			},
+			{
+				name:     "MaxUint16+1",
+				value:    math.MaxUint16 + 1,
+				isUint16: false,
+				isUint32: true,
+				isUint:   true,
+			},
+			{
+				name:     "MaxUint32",
+				value:    math.MaxUint32,
+				isUint16: false,
+				isUint32: true,
+				isUint:   true,
+			},
+			{
+				name:     "MaxUint32+1",
+				value:    math.MaxUint32 + 1,
+				isUint16: false,
+				isUint32: false,
+				isUint:   true,
+			},
+		}
+
+		for _, tc := range tests {
+			t.Run(tc.name, func(t *testing.T) {
+				reporter := newMockReporter(t)
+
+				if tc.isUint {
+					NewNumber(reporter, tc.value).IsUint().chain.assertNotFailed(t)
+					NewNumber(reporter, tc.value).NotUint().chain.assertFailed(t)
+				} else {
+					NewNumber(reporter, tc.value).IsUint().chain.assertFailed(t)
+					NewNumber(reporter, tc.value).NotUint().chain.assertNotFailed(t)
+				}
+
+				if tc.isUint32 {
+					NewNumber(reporter, tc.value).IsUint(32).chain.assertNotFailed(t)
+					NewNumber(reporter, tc.value).NotUint(32).chain.assertFailed(t)
+				} else {
+					NewNumber(reporter, tc.value).IsUint(32).chain.assertFailed(t)
+					NewNumber(reporter, tc.value).NotUint(32).chain.assertNotFailed(t)
+				}
+
+				if tc.isUint16 {
+					NewNumber(reporter, tc.value).IsUint(16).chain.assertNotFailed(t)
+					NewNumber(reporter, tc.value).NotUint(16).chain.assertFailed(t)
+				} else {
+					NewNumber(reporter, tc.value).IsUint(16).chain.assertFailed(t)
+					NewNumber(reporter, tc.value).NotUint(16).chain.assertNotFailed(t)
+				}
+			})
+		}
+	})
+}
+
+func TestNumber_IsFinite(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    float64
+		isFinite bool
+	}{
+		{
+			name:     "0",
+			value:    0,
+			isFinite: true,
+		},
+		{
+			name:     "1",
+			value:    1,
+			isFinite: true,
+		},
+		{
+			name:     "-1",
+			value:    -1,
+			isFinite: true,
+		},
+		{
+			name:     "0.5",
+			value:    0.5,
+			isFinite: true,
+		},
+		{
+			name:     "NaN",
+			value:    math.NaN(),
+			isFinite: false,
+		},
+		{
+			name:     "-Inf",
+			value:    math.Inf(-1),
+			isFinite: false,
+		},
+		{
+			name:     "+Inf",
+			value:    math.Inf(+1),
+			isFinite: false,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			reporter := newMockReporter(t)
+
+			if tc.isFinite {
+				NewNumber(reporter, tc.value).IsFinite().chain.assertNotFailed(t)
+				NewNumber(reporter, tc.value).NotFinite().chain.assertFailed(t)
+			} else {
+				NewNumber(reporter, tc.value).IsFinite().chain.assertFailed(t)
+				NewNumber(reporter, tc.value).NotFinite().chain.assertNotFailed(t)
+			}
+		})
+	}
+}
diff --git a/string_test.go b/string_test.go
index c3623f2..2599d98 100644
--- a/string_test.go
+++ b/string_test.go
@@ -180,27 +180,33 @@ func TestString_Length(t *testing.T) {
 }
 
 func TestString_IsEmpty(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	value1 := NewString(reporter, "")
-
-	value1.IsEmpty()
-	value1.chain.assertNotFailed(t)
-	value1.chain.clearFailed()
+	cases := map[string]struct {
+		str       string
+		wantEmpty bool
+	}{
+		"empty_string":     {str: "", wantEmpty: true},
+		"populated_string": {str: "a", wantEmpty: false},
+	}
 
-	value1.NotEmpty()
-	value1.chain.assertFailed(t)
-	value1.chain.clearFailed()
+	for name, value := range cases {
+		t.Run(name, func(t *testing.T) {
+			reporter := newMockReporter(t)
 
-	value2 := NewString(reporter, "a")
+			if value.wantEmpty {
+				NewString(reporter, value.str).IsEmpty().
+					chain.assertNotFailed(t)
 
-	value2.IsEmpty()
-	value2.chain.assertFailed(t)
-	value2.chain.clearFailed()
+				NewString(reporter, value.str).NotEmpty().
+					chain.assertFailed(t)
+			} else {
+				NewString(reporter, value.str).IsEmpty().
+					chain.assertFailed(t)
 
-	value2.NotEmpty()
-	value2.chain.assertNotFailed(t)
-	value2.chain.clearFailed()
+				NewString(reporter, value.str).NotEmpty().
+					chain.assertNotFailed(t)
+			}
+		})
+	}
 }
 
 func TestString_IsEqual(t *testing.T) {
@@ -475,164 +481,142 @@ func TestString_MatchInvalid(t *testing.T) {
 }
 
 func TestString_IsAscii(t *testing.T) {
-	reporter := newMockReporter(t)
+	cases := []struct {
+		str       string
+		wantASCII bool
+	}{
+		{"Ascii", true},
+		{string(rune(127)), true},
+		{"Ascii is アスキー", false},
+		{"アスキー", false},
+		{string(rune(128)), false},
+	}
 
-	value1 := NewString(reporter, "Ascii")
-	value1.IsASCII()
-	value1.chain.assertNotFailed(t)
-	value1.chain.clearFailed()
-
-	value2 := NewString(reporter, "Ascii is アスキー")
-	value2.IsASCII()
-	value2.chain.assertFailed(t)
-	value2.chain.clearFailed()
-
-	value3 := NewString(reporter, "アスキー")
-	value3.IsASCII()
-	value3.chain.assertFailed(t)
-	value3.chain.clearFailed()
-
-	value4 := NewString(reporter, string(rune(127)))
-	value4.IsASCII()
-	value4.chain.assertNotFailed(t)
-	value4.chain.clearFailed()
-
-	value5 := NewString(reporter, string(rune(128)))
-	value5.IsASCII()
-	value5.chain.assertFailed(t)
-	value5.chain.clearFailed()
-}
+	for _, value := range cases {
+		t.Run(value.str, func(t *testing.T) {
+			reporter := newMockReporter(t)
 
-func TestString_NotAscii(t *testing.T) {
-	reporter := newMockReporter(t)
+			if value.wantASCII {
+				NewString(reporter, value.str).IsASCII().
+					chain.assertNotFailed(t)
+
+				NewString(reporter, value.str).NotASCII().
+					chain.assertFailed(t)
+			} else {
+				NewString(reporter, value.str).IsASCII().
+					chain.assertFailed(t)
 
-	value1 := NewString(reporter, "Ascii")
-	value1.NotASCII()
-	value1.chain.assertFailed(t)
-	value1.chain.clearFailed()
-
-	value2 := NewString(reporter, "Ascii is アスキー")
-	value2.NotASCII()
-	value2.chain.assertNotFailed(t)
-	value2.chain.clearFailed()
-
-	value3 := NewString(reporter, "アスキー")
-	value3.NotASCII()
-	value3.chain.assertNotFailed(t)
-	value3.chain.clearFailed()
-
-	value4 := NewString(reporter, string(rune(127)))
-	value4.NotASCII()
-	value4.chain.assertFailed(t)
-	value4.chain.clearFailed()
-
-	value5 := NewString(reporter, string(rune(128)))
-	value5.NotASCII()
-	value5.chain.assertNotFailed(t)
-	value5.chain.clearFailed()
+				NewString(reporter, value.str).NotASCII().
+					chain.assertNotFailed(t)
+			}
+		})
+	}
 }
 
 func TestString_AsNumber(t *testing.T) {
-	reporter := newMockReporter(t)
-
-	t.Run("default_base", func(t *testing.T) {
-		value1 := NewString(reporter, "1234567")
-		num1 := value1.AsNumber()
-		value1.chain.assertNotFailed(t)
-		num1.chain.assertNotFailed(t)
-		assert.Equal(t, float64(1234567), num1.Raw())
-
-		value2 := NewString(reporter, "11.22")
-		num2 := value2.AsNumber()
-		value2.chain.assertNotFailed(t)
-		num2.chain.assertNotFailed(t)
-		assert.Equal(t, float64(11.22), num2.Raw())
-
-		value3 := NewString(reporter, "a1")
-		num3 := value3.AsNumber()
-		value3.chain.assertFailed(t)
-		num3.chain.assertFailed(t)
-		assert.Equal(t, float64(0), num3.Raw())
-	})
-
-	t.Run("base10", func(t *testing.T) {
-		value1 := NewString(reporter, "100")
-		num1 := value1.AsNumber(10)
-		value1.chain.assertNotFailed(t)
-		num1.chain.assertNotFailed(t)
-		assert.Equal(t, float64(100), num1.Raw())
-
-		value2 := NewString(reporter, "11.22")
-		num2 := value2.AsNumber(10)
-		value2.chain.assertNotFailed(t)
-		num2.chain.assertNotFailed(t)
-		assert.Equal(t, float64(11.22), num2.Raw())
-	})
-
-	t.Run("base16", func(t *testing.T) {
-		value1 := NewString(reporter, "100")
-		num1 := value1.AsNumber(16)
-		value1.chain.assertNotFailed(t)
-		num1.chain.assertNotFailed(t)
-		assert.Equal(t, float64(0x100), num1.Raw())
-
-		value2 := NewString(reporter, "11.22")
-		num2 := value2.AsNumber(16)
-		value2.chain.assertFailed(t)
-		num2.chain.assertFailed(t)
-		assert.Equal(t, float64(0), num2.Raw())
-
-		value3 := NewString(reporter, "4000000000000000")
-		num3 := value3.AsNumber(16)
-		value3.chain.assertNotFailed(t)
-		num3.chain.assertNotFailed(t)
-		assert.Equal(t, float64(0x4000000000000000), num3.Raw())
-	})
-
-	t.Run("float_precision", func(t *testing.T) {
-		value1 := NewString(reporter, "4611686018427387905")
-		num1 := value1.AsNumber()
-		value1.chain.assertFailed(t)
-		num1.chain.assertFailed(t)
-		assert.Equal(t, float64(0), num1.Raw())
-
-		value2 := NewString(reporter, "4611686018427387905")
-		num2 := value2.AsNumber(10)
-		value2.chain.assertFailed(t)
-		num2.chain.assertFailed(t)
-		assert.Equal(t, float64(0), num2.Raw())
-
-		value3 := NewString(reporter, "8000000000000001")
-		num3 := value3.AsNumber(16)
-		value3.chain.assertFailed(t)
-		num3.chain.assertFailed(t)
-		assert.Equal(t, float64(0), num3.Raw())
-
-		value4 := NewString(reporter, "-4000000000000001")
-		num4 := value4.AsNumber(16)
-		value4.chain.assertFailed(t)
-		num4.chain.assertFailed(t)
-		assert.Equal(t, float64(0), num4.Raw())
-	})
+	cases := map[string]struct {
+		str         string
+		base        []int
+		fail        bool
+		expectedNum float64
+	}{
+		"default_base_integer": {
+			str:         "1234567",
+			fail:        false,
+			expectedNum: float64(1234567),
+		},
+		"default_base_float": {
+			str:         "11.22",
+			fail:        false,
+			expectedNum: float64(11.22),
+		},
+		"default_base_bad": {
+			str:  "a1",
+			fail: true,
+		},
+		"base10_integer": {
+			str:         "100",
+			base:        []int{10},
+			fail:        false,
+			expectedNum: float64(100),
+		},
+		"base10_float": {
+			str:         "11.22",
+			base:        []int{10},
+			fail:        false,
+			expectedNum: float64(11.22),
+		},
+		"base16_integer": {
+			str:         "100",
+			base:        []int{16},
+			fail:        false,
+			expectedNum: float64(0x100),
+		},
+		"base16_float": {
+			str:  "11.22",
+			base: []int{16},
+			fail: true,
+		},
+		"base16_large_integer": {
+			str:         "4000000000000000",
+			base:        []int{16},
+			fail:        false,
+			expectedNum: float64(0x4000000000000000),
+		},
+		"default_float_precision_max": {
+			str:  "4611686018427387905",
+			fail: true,
+		},
+		"base10_float_precision_max": {
+			str:  "4611686018427387905",
+			base: []int{10},
+			fail: true,
+		},
+		"base16_float_precision_max": {
+			str:  "8000000000000001",
+			base: []int{16},
+			fail: true,
+		},
+		"base16_float_precision_min": {
+			str:  "-4000000000000001",
+			base: []int{16},
+			fail: true,
+		},
+		"multiple_base": {
+			str:  "100",
+			base: []int{10, 16},
+			fail: true,
+		},
+	}
 
-	t.Run("multiple_base", func(t *testing.T) {
-		value1 := NewString(reporter, "100")
-		num1 := value1.AsNumber(10, 16)
-		value1.chain.assertFailed(t)
-		num1.chain.assertFailed(t)
-		assert.Equal(t, float64(0), num1.Raw())
-	})
+	for name, value := range cases {
+		t.Run(name, func(t *testing.T) {
+			reporter := newMockReporter(t)
+
+			str := NewString(reporter, value.str)
+			num := str.AsNumber(value.base...)
+
+			if value.fail {
+				str.chain.assertFailed(t)
+				num.chain.assertFailed(t)
+				assert.Equal(t, float64(0), num.Raw())
+			} else {
+				str.chain.assertNotFailed(t)
+				num.chain.assertNotFailed(t)
+				assert.Equal(t, value.expectedNum, num.Raw())
+			}
+		})
+	}
 }
 
 func TestString_AsBoolean(t *testing.T) {
-	reporter := newMockReporter(t)
-
 	trueValues := []string{"true", "True"}
 	falseValues := []string{"false", "False"}
 	badValues := []string{"TRUE", "FALSE", "t", "f", "1", "0", "bad"}
 
 	for _, str := range trueValues {
 		t.Run(str, func(t *testing.T) {
+			reporter := newMockReporter(t)
 			value := NewString(reporter, str)
 
 			b := value.AsBoolean()
@@ -644,6 +628,7 @@ func TestString_AsBoolean(t *testing.T) {
 
 	for _, str := range falseValues {
 		t.Run(str, func(t *testing.T) {
+			reporter := newMockReporter(t)
 			value := NewString(reporter, str)
 
 			b := value.AsBoolean()
@@ -655,6 +640,7 @@ func TestString_AsBoolean(t *testing.T) {
 
 	for _, str := range badValues {
 		t.Run(str, func(t *testing.T) {
+			reporter := newMockReporter(t)
 			value := NewString(reporter, str)
 
 			b := value.AsBoolean()
@@ -664,26 +650,37 @@ func TestString_AsBoolean(t *testing.T) {
 }
 
 func TestString_AsDateTime(t *testing.T) {
-	reporter := newMockReporter(t)
+	t.Run("default_formats_RFC1123+GMT", func(t *testing.T) {
+		reporter := newMockReporter(t)
+		value := NewString(reporter, "Tue, 15 Nov 1994 08:12:31 GMT")
+
+		dt := value.AsDateTime()
+		value.chain.assertNotFailed(t)
+		dt.chain.assertNotFailed(t)
+
+		assert.True(t, time.Date(1994, 11, 15, 8, 12, 31, 0, time.UTC).Equal(dt.Raw()))
+	})
+
+	t.Run("RFC822", func(t *testing.T) {
+		reporter := newMockReporter(t)
+		value := NewString(reporter, "15 Nov 94 08:12 GMT")
+
+		dt := value.AsDateTime(time.RFC822)
+		value.chain.assertNotFailed(t)
+		dt.chain.assertNotFailed(t)
+
+		assert.True(t, time.Date(1994, 11, 15, 8, 12, 0, 0, time.UTC).Equal(dt.Raw()))
+	})
+
+	t.Run("bad_input", func(t *testing.T) {
+		reporter := newMockReporter(t)
+		value := NewString(reporter, "bad")
+
+		dt := value.AsDateTime()
+		value.chain.assertFailed(t)
+		dt.chain.assertFailed(t)
 
-	t.Run("basic", func(t *testing.T) {
-		value1 := NewString(reporter, "Tue, 15 Nov 1994 08:12:31 GMT")
-		dt1 := value1.AsDateTime()
-		value1.chain.assertNotFailed(t)
-		dt1.chain.assertNotFailed(t)
-		assert.True(t, time.Date(1994, 11, 15, 8, 12, 31, 0, time.UTC).Equal(dt1.Raw()))
-
-		value2 := NewString(reporter, "15 Nov 94 08:12 GMT")
-		dt2 := value2.AsDateTime(time.RFC822)
-		value2.chain.assertNotFailed(t)
-		dt2.chain.assertNotFailed(t)
-		assert.True(t, time.Date(1994, 11, 15, 8, 12, 0, 0, time.UTC).Equal(dt2.Raw()))
-
-		value3 := NewString(reporter, "bad")
-		dt3 := value3.AsDateTime()
-		value3.chain.assertFailed(t)
-		dt3.chain.assertFailed(t)
-		assert.True(t, time.Unix(0, 0).Equal(dt3.Raw()))
+		assert.True(t, time.Unix(0, 0).Equal(dt.Raw()))
 	})
 
 	formats := []string{
@@ -701,27 +698,47 @@ func TestString_AsDateTime(t *testing.T) {
 	}
 
 	for n, f := range formats {
-		t.Run(f, func(t *testing.T) {
+		t.Run("default_formats_"+f, func(t *testing.T) {
 			str := time.Now().Format(f)
 
-			value1 := NewString(reporter, str)
-			dt1 := value1.AsDateTime()
-			dt1.chain.assertNotFailed(t)
+			reporter := newMockReporter(t)
+			value := NewString(reporter, str)
 
-			value2 := NewString(reporter, str)
-			dt2 := value2.AsDateTime(formats...)
-			dt2.chain.assertNotFailed(t)
+			dt := value.AsDateTime()
+			dt.chain.assertNotFailed(t)
+		})
 
-			value3 := NewString(reporter, str)
-			dt3 := value3.AsDateTime(f)
-			dt3.chain.assertNotFailed(t)
+		t.Run("all_formats_"+f, func(t *testing.T) {
+			str := time.Now().Format(f)
 
-			if n != 0 {
-				value4 := NewString(reporter, str)
-				dt4 := value4.AsDateTime(formats[0])
-				dt4.chain.assertFailed(t)
-			}
+			reporter := newMockReporter(t)
+			value := NewString(reporter, str)
+
+			dt := value.AsDateTime(formats...)
+			dt.chain.assertNotFailed(t)
 		})
+
+		t.Run("same_format_"+f, func(t *testing.T) {
+			str := time.Now().Format(f)
+
+			reporter := newMockReporter(t)
+			value := NewString(reporter, str)
+
+			dt := value.AsDateTime(f)
+			dt.chain.assertNotFailed(t)
+		})
+
+		if n != 0 {
+			t.Run("different_format_"+f, func(t *testing.T) {
+				str := time.Now().Format(f)
+
+				reporter := newMockReporter(t)
+				value := NewString(reporter, str)
+
+				dt := value.AsDateTime(formats[0])
+				dt.chain.assertFailed(t)
+			})
+		}
 	}
 }