Explorar o código

improve validate testing with examples

also simplified structure of test cases and failure logging
Aneurin Barker Snook hai 10 meses
pai
achega
a9c1184d6b
Modificáronse 14 ficheiros con 438 adicións e 449 borrados
  1. 1 25
      README.md
  2. 25 22
      all_test.go
  3. 36 35
      chars_test.go
  4. 32 14
      email_test.go
  5. 16 42
      equal_test.go
  6. 31 31
      error_test.go
  7. 23 45
      in_test.go
  8. 2 2
      length.go
  9. 37 34
      length_test.go
  10. 138 131
      number_test.go
  11. 2 2
      size.go
  12. 53 30
      size_test.go
  13. 20 17
      url_test.go
  14. 22 19
      uuid_test.go

+ 1 - 25
README.md

@@ -4,31 +4,7 @@ A suite of straightforward validation functions. You put something in, you get b
 
 ## Error handling
 
-You can use `errors.Is()` to ascertain the type of errors thrown by validation functions. This may be helpful to control side effects, particularly if using multiple validators and returning early (similar to a strongly-typed try-catch). For example:
-
-## Example
-
-```go
-package main
-
-import (
-	"errors"
-	"fmt"
-
-	"github.com/annybs/go/validate"
-)
-
-func main() {
-	v := validate.Equal("a")
-	if err := v("b"); err != nil {
-		if errors.Is(err, validate.ErrNotEqual) {
-			fmt.Println("failed successfully")
-		} else {
-			fmt.Println("failed unsuccessfully")
-		}
-	}
-}
-```
+You can use `errors.Is()` to ascertain the type of errors returned by validation functions. This may be helpful to control side effects, particularly if you are using multiple validators or want to change the error message.
 
 ## License
 

+ 25 - 22
all_test.go

@@ -2,40 +2,43 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
-func TestAll(t *testing.T) {
-	type TestCase[T any] struct {
-		Input T
-		F     func(T) error
-		Err   error
-	}
+func ExampleAll() {
+	testAll := All(MinLength(4), Chars("0123456789abcdef"))
+	fmt.Println(testAll("invalid input"))
+	// Output: contains disallowed characters
+}
 
-	f := All(
+func TestAll(t *testing.T) {
+	testAll := All(
 		MinLength(4),
 		MaxLength(8),
 		Chars("0123456789abcdef"),
 		In("abcd", "abcdef", "12345678"),
 	)
 
-	testCases := []TestCase[string]{
-		{Input: "abcd", F: f},
-		{Input: "abcdef", F: f},
-		{Input: "12345678", F: f},
-		{Input: "abc", F: f, Err: ErrMustBeLonger},
-		{Input: "abcdef012", F: f, Err: ErrMustBeShorter},
-		{Input: "abcdefgh", F: f, Err: ErrDisallowedChars},
-		{Input: "01abcd", F: f, Err: ErrValueNotAllowed},
-	}
+	testCases := map[string]error{
+		"abcd":     nil,
+		"abcdef":   nil,
+		"12345678": nil,
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q", n, tc.Input)
+		"abc":       ErrMustBeLonger.With(4),
+		"abcdef012": ErrMustBeShorter.With(8),
+		"abcdefgh":  ErrDisallowedChars,
+		"01abcd":    ErrValueNotAllowed,
+	}
 
-		err := tc.F(tc.Input)
+	for input, want := range testCases {
+		t.Run(input, func(t *testing.T) {
+			got := testAll(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
-		}
+			if !errors.Is(got, want) {
+				t.Error("got", got)
+				t.Error("want", want)
+			}
+		})
 	}
 }

+ 36 - 35
chars_test.go

@@ -2,59 +2,60 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
-func TestChars(t *testing.T) {
-	type TestCase struct {
-		Input string
-		C     string
-		Err   error
-	}
+func ExampleChars() {
+	testChars := Chars("0123456789abcdef")
+	fmt.Println(testChars("invalid input"))
+	// Output: contains disallowed characters
+}
 
-	hexRange := "0123456789abcdef"
+func ExampleExceptChars() {
+	testExceptChars := Chars("0123456789abcdef")
+	fmt.Println(testExceptChars("invalid input"))
+	// Output: contains disallowed characters
+}
 
-	testCases := []TestCase{
-		{Input: "abcd1234", C: hexRange},
-		{Input: "abcd 1234", C: hexRange, Err: ErrDisallowedChars},
-		{Input: "ghijklmno", C: hexRange, Err: ErrDisallowedChars},
+func TestChars(t *testing.T) {
+	testCases := map[string]map[string]error{
+		"0123456789abcdef": {"abcd1234": nil, "abcd 1234": ErrDisallowedChars, "ghijklmno": ErrDisallowedChars},
 	}
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q against %q", n, tc.Input, tc.C)
+	for setup, values := range testCases {
+		testChars := Chars(setup)
 
-		f := Chars(tc.C)
-		err := f(tc.Input)
+		for input, want := range values {
+			t.Run(input, func(t *testing.T) {
+				got := testChars(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+				if !errors.Is(got, want) {
+					t.Error("got", got)
+					t.Error("want", want)
+				}
+			})
 		}
 	}
 }
 
 func TestExceptChars(t *testing.T) {
-	type TestCase struct {
-		Input string
-		C     string
-		Err   error
-	}
-
-	hexRange := "0123456789abcdef"
-
-	testCases := []TestCase{
-		{Input: "abcd1234", C: hexRange, Err: ErrDisallowedChars},
-		{Input: "abcd 1234", C: hexRange, Err: ErrDisallowedChars},
-		{Input: "ghijklmno", C: hexRange},
+	testCases := map[string]map[string]error{
+		"0123456789abcdef": {"abcd1234": ErrDisallowedChars, "abcd 1234": ErrDisallowedChars, "ghijklmno": nil},
 	}
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q against %q", n, tc.Input, tc.C)
+	for setup, values := range testCases {
+		testExceptChars := ExceptChars(setup)
 
-		f := ExceptChars(tc.C)
-		err := f(tc.Input)
+		for input, want := range values {
+			t.Run(input, func(t *testing.T) {
+				got := testExceptChars(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+				if !errors.Is(got, want) {
+					t.Error("got", got)
+					t.Error("want", want)
+				}
+			})
 		}
 	}
 }

+ 32 - 14
email_test.go

@@ -2,27 +2,45 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
+func ExampleEmail() {
+	fmt.Println(Email("not an email"))
+	// Output: invalid email address
+}
+
+func FuzzEmail(f *testing.F) {
+	want := ErrInvalidEmail
+
+	f.Fuzz(func(t *testing.T, input string) {
+		got := Email(input)
+
+		if !errors.Is(got, want) {
+			t.Error("got", got)
+			t.Error("want", want)
+		}
+	})
+}
+
 func TestEmail(t *testing.T) {
-	type TestCase struct {
-		Input string
-		Err   error
-	}
+	testCases := map[string]error{
+		"test@example.com":                      nil,
+		"firstname.lastname@some-website.co.uk": nil,
 
-	testCases := []TestCase{
-		{Input: "test@example.com"},
-		{Input: "testexample.com", Err: ErrInvalidEmail},
+		"not an email":    ErrInvalidEmail,
+		"testexample.com": ErrInvalidEmail,
 	}
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q", n, tc.Input)
+	for input, want := range testCases {
+		t.Run(input, func(t *testing.T) {
+			got := Email(input)
 
-		err := Email(tc.Input)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
-		}
+			if !errors.Is(got, want) {
+				t.Error("got", got)
+				t.Error("want", want)
+			}
+		})
 	}
 }

+ 16 - 42
equal_test.go

@@ -2,55 +2,29 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
-func TestEqualInt(t *testing.T) {
-	type TestCase struct {
-		I   int
-		C   int
-		Err error
+func TestEqual(t *testing.T) {
+	testCases := map[string]map[string]error{
+		"abc": {"abc": nil, "def": ErrNotEqual.With("abc"), "xyz": ErrNotEqual.With("abc")},
+		"def": {"abc": ErrNotEqual.With("def"), "def": nil, "xyz": ErrNotEqual.With("def")},
+		"xyz": {"abc": ErrNotEqual.With("xyz"), "def": ErrNotEqual.With("xyz"), "xyz": nil},
 	}
 
-	testCases := []TestCase{
-		{I: 1, C: 1},
-		{I: 5 ^ 3, C: 5 ^ 3},
-		{I: 10, C: 15, Err: ErrNotEqual},
-	}
-
-	for i, tc := range testCases {
-		t.Logf("(%d) Testing %d against %d", i, tc.I, tc.C)
-
-		f := Equal(tc.C)
-		err := f(tc.I)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
-		}
-	}
-}
-
-func TestEqualStr(t *testing.T) {
-	type TestCase struct {
-		I   string
-		C   string
-		Err error
-	}
-
-	testCases := []TestCase{
-		{I: "abc", C: "abc"},
-		{I: "def ghi 123", C: "def ghi 123"},
-		{I: "jkl", C: "mno", Err: ErrNotEqual},
-	}
-
-	for i, tc := range testCases {
-		t.Logf("(%d) Testing %s against %s", i, tc.I, tc.C)
+	for setup, values := range testCases {
+		testEqual := Equal(setup)
 
-		f := Equal(tc.C)
-		err := f(tc.I)
+		for input, want := range values {
+			t.Run(fmt.Sprintf("%s/%s", setup, input), func(t *testing.T) {
+				got := testEqual(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+				if !errors.Is(got, want) {
+					t.Error("got", got)
+					t.Error("want", want)
+				}
+			})
 		}
 	}
 }

+ 31 - 31
error_test.go

@@ -2,48 +2,48 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
 func TestErrorIs(t *testing.T) {
 	type TestCase struct {
-		Err    error
-		Target error
-		Is     bool
+		A    error
+		B    error
+		Want bool
 	}
 
 	testCases := []TestCase{
-		// Is any validation error
-		{Err: Err, Target: Err, Is: true},
-		{Err: ErrDisallowedChars, Target: Err, Is: true},
-		{Err: ErrMustBeGreater, Target: Err, Is: true},
-
-		// Is specific validation error
-		{Err: ErrDisallowedChars, Target: ErrDisallowedChars, Is: true},
-		{Err: ErrMustBeGreater, Target: ErrMustBeGreater, Is: true},
-
-		// Is not specific validation error
-		{Err: Err, Target: ErrDisallowedChars},
-		{Err: Err, Target: ErrMustBeGreater},
-		{Err: ErrMustBeGreater, Target: ErrDisallowedChars},
-		{Err: ErrDisallowedChars, Target: ErrMustBeGreater},
-
-		// Is not any other error
-		{Err: ErrDisallowedChars, Target: errors.New("contains disallowed characters")},
-		{Err: ErrMustBeGreater, Target: errors.New("must be greater than %v")},
+		// Want any validation error
+		{A: Err, B: Err, Want: true},
+		{A: ErrDisallowedChars, B: Err, Want: true},
+		{A: ErrMustBeGreater, B: Err, Want: true},
+
+		// Want specific validation error
+		{A: ErrDisallowedChars, B: ErrDisallowedChars, Want: true},
+		{A: ErrMustBeGreater, B: ErrMustBeGreater, Want: true},
+
+		// Want not specific validation error
+		{A: Err, B: ErrDisallowedChars},
+		{A: Err, B: ErrMustBeGreater},
+		{A: ErrMustBeGreater, B: ErrDisallowedChars},
+		{A: ErrDisallowedChars, B: ErrMustBeGreater},
+
+		// Want not any other error
+		{A: ErrDisallowedChars, B: errors.New("contains disallowed characters")},
+		{A: ErrMustBeGreater, B: errors.New("must be greater than %v")},
 	}
 
-	for i, tc := range testCases {
-		t.Logf("(%d) Testing %v against %v", i, tc.Err, tc.Target)
+	for _, testCase := range testCases {
+		a, b, want := testCase.A, testCase.B, testCase.Want
 
-		if errors.Is(tc.Err, tc.Target) {
-			if !tc.Is {
-				t.Errorf("%v should not equal %v", tc.Err, tc.Target)
-			}
-		} else {
-			if tc.Is {
-				t.Errorf("%v should equal %v", tc.Err, tc.Target)
+		t.Run(fmt.Sprintf("%v/%v", a, b), func(t *testing.T) {
+			got := errors.Is(a, b)
+
+			if got != want {
+				t.Error("got", got)
+				t.Error("want", want)
 			}
-		}
+		})
 	}
 }

+ 23 - 45
in_test.go

@@ -2,59 +2,37 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
-func TestInInt(t *testing.T) {
-	type TestCase struct {
-		Input int
-		A     []int
-		Err   error
-	}
-
-	allow := []int{1, 23, 456}
-	testCases := []TestCase{
-		{Input: 1, A: allow},
-		{Input: 23, A: allow},
-		{Input: 456, A: allow},
-		{Input: 789, A: allow, Err: ErrValueNotAllowed},
-	}
-
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %d against %v", n, tc.Input, tc.A)
-
-		f := In(tc.A...)
-		err := f(tc.Input)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
-		}
-	}
+func ExampleIn() {
+	testIn := In("abc", "def", "xyz")
+	fmt.Println(testIn("123"))
+	// Output: not allowed
 }
 
-func TestInString(t *testing.T) {
-	type TestCase struct {
-		Input string
-		A     []string
-		Err   error
-	}
+func TestIn(t *testing.T) {
+	testIn := In("abc", "def", "xyz")
 
-	allow := []string{"abcd", "ef", "1234"}
-	testCases := []TestCase{
-		{Input: "abcd", A: allow},
-		{Input: "ef", A: allow},
-		{Input: "1234", A: allow},
-		{Input: "5678", A: allow, Err: ErrValueNotAllowed},
-	}
+	testCases := map[string]error{
+		"abc": nil,
+		"def": nil,
+		"xyz": nil,
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q against %v", n, tc.Input, tc.A)
+		"abcd": ErrValueNotAllowed,
+		"123":  ErrValueNotAllowed,
+		"":     ErrValueNotAllowed,
+	}
 
-		f := In(tc.A...)
-		err := f(tc.Input)
+	for input, want := range testCases {
+		t.Run(input, func(t *testing.T) {
+			got := testIn(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
-		}
+			if !errors.Is(got, want) {
+				t.Error("got", got)
+				t.Error("want", want)
+			}
+		})
 	}
 }

+ 2 - 2
length.go

@@ -2,8 +2,8 @@ package validate
 
 // Validation error.
 var (
-	ErrMustBeLonger  = NewError("must be at least %d characters")
-	ErrMustBeShorter = NewError("must be no more than %d characters")
+	ErrMustBeLonger  = NewError("must contain at least %d characters")
+	ErrMustBeShorter = NewError("must contain no more than %d characters")
 )
 
 // MaxLength validates the length of a string as being less than or equal to a given maximum.

+ 37 - 34
length_test.go

@@ -2,57 +2,60 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
-func TestMaxLength(t *testing.T) {
-	type TestCase struct {
-		Input string
-		L     int
-		Err   error
-	}
+func ExampleMaxLength() {
+	testMaxLength := MaxLength(8)
+	fmt.Println(testMaxLength("this string is too long"))
+	// Output: must contain no more than 8 characters
+}
+
+func ExampleMinLength() {
+	testMinLength := MinLength(8)
+	fmt.Println(testMinLength("2short"))
+	// Output: must contain at least 8 characters
+}
 
-	testCases := []TestCase{
-		{Input: "abcd", L: 8},
-		{Input: "abcdefgh", L: 8},
-		{Input: "abcd efg", L: 8},
-		{Input: "abcdefghi", L: 8, Err: ErrMustBeShorter},
+func TestMaxLength(t *testing.T) {
+	testCases := map[int]map[string]error{
+		8: {"abcd": nil, "abcdefgh": nil, "abcd efg": nil, "abcdefghi": ErrMustBeShorter.With(8)},
 	}
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q against maximum length of %d", n, tc.Input, tc.L)
+	for setup, values := range testCases {
+		testMaxLength := MaxLength(setup)
 
-		f := MaxLength(tc.L)
-		err := f(tc.Input)
+		for input, want := range values {
+			t.Run(fmt.Sprintf("%d/%s", setup, input), func(t *testing.T) {
+				got := testMaxLength(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+				if !errors.Is(got, want) {
+					t.Error("got", got)
+					t.Error("want", want)
+				}
+			})
 		}
 	}
 }
 
 func TestMinLength(t *testing.T) {
-	type TestCase struct {
-		Input string
-		L     int
-		Err   error
-	}
-
-	testCases := []TestCase{
-		{Input: "abcd", L: 8, Err: ErrMustBeLonger},
-		{Input: "abcdefgh", L: 8},
-		{Input: "abcd efg", L: 8},
-		{Input: "abcdefghi", L: 8},
+	testCases := map[int]map[string]error{
+		8: {"abcd": ErrMustBeLonger.With(8), "abcdefgh": nil, "abcd efg": nil, "abcdefghi": nil},
 	}
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q against minimum length of %d", n, tc.Input, tc.L)
+	for setup, values := range testCases {
+		testMinLength := MinLength(setup)
 
-		f := MinLength(tc.L)
-		err := f(tc.Input)
+		for input, want := range values {
+			t.Run(fmt.Sprintf("%d/%s", setup, input), func(t *testing.T) {
+				got := testMinLength(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+				if !errors.Is(got, want) {
+					t.Error("got", got)
+					t.Error("want", want)
+				}
+			})
 		}
 	}
 }

+ 138 - 131
number_test.go

@@ -2,167 +2,174 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
-func TestMax(t *testing.T) {
-	type TestCase struct {
-		Input int
-		N     int
-		Excl  bool
-		Err   error
-	}
-
-	testCases := []TestCase{
-		{Input: 10, N: 0, Err: ErrMustBeLessOrEqual},
-		{Input: 10, N: 10},
-		{Input: 10, N: 15},
-		{Input: 10, N: 10, Excl: true, Err: ErrMustBeLess},
-	}
-
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %d against maximum of %d", n, tc.Input, tc.N)
+func ExampleMax() {
+	testMax := Max(10, true)
+	fmt.Println(testMax(10))
+	// Output: must be less than 10
+}
 
-		f := Max(tc.N, tc.Excl)
-		err := f(tc.Input)
+func ExampleMin() {
+	testMin := Min(10, false)
+	fmt.Println(testMin(5))
+	// Output: must be greater than or equal to 10
+}
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+func TestMax(t *testing.T) {
+	testCases := map[int]map[bool]map[int]error{
+		10: {
+			true:  {0: nil, 1: nil, 2: nil, 10: ErrMustBeLess.With(10), 100: ErrMustBeLess.With(10)},
+			false: {0: nil, 1: nil, 2: nil, 10: nil, 100: ErrMustBeLessOrEqual.With(10)},
+		},
+	}
+
+	for setup, subSetup := range testCases {
+		for excl, values := range subSetup {
+			testMax := Max(setup, excl)
+
+			for input, want := range values {
+				t.Run(fmt.Sprintf("%d/%v/%d", setup, excl, input), func(t *testing.T) {
+					got := testMax(input)
+
+					if !errors.Is(got, want) {
+						t.Error("got", got)
+						t.Error("want", want)
+					}
+				})
+			}
 		}
 	}
 }
 
 func TestMaxFloat32(t *testing.T) {
-	type TestCase struct {
-		Input float32
-		N     float32
-		Excl  bool
-		Err   error
-	}
-
-	testCases := []TestCase{
-		{Input: 10, N: 0, Err: ErrMustBeLessOrEqual},
-		{Input: 10, N: 10},
-		{Input: 10, N: 15},
-		{Input: 10, N: 10, Excl: true, Err: ErrMustBeLess},
-	}
-
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %g against maximum of %g", n, tc.Input, tc.N)
-
-		f := MaxFloat32(tc.N, tc.Excl)
-		err := f(tc.Input)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+	testCases := map[float32]map[bool]map[float32]error{
+		10: {
+			true:  {0: nil, 1: nil, 2: nil, 10: ErrMustBeLess.With(10), 100: ErrMustBeLess.With(10)},
+			false: {0: nil, 1: nil, 2: nil, 10: nil, 100: ErrMustBeLessOrEqual.With(10)},
+		},
+	}
+
+	for setup, subSetup := range testCases {
+		for excl, values := range subSetup {
+			testMax := MaxFloat32(setup, excl)
+
+			for input, want := range values {
+				t.Run(fmt.Sprintf("%f/%v/%f", setup, excl, input), func(t *testing.T) {
+					got := testMax(input)
+
+					if !errors.Is(got, want) {
+						t.Error("got", got)
+						t.Error("want", want)
+					}
+				})
+			}
 		}
 	}
 }
 
 func TestMaxFloat64(t *testing.T) {
-	type TestCase struct {
-		Input float64
-		N     float64
-		Excl  bool
-		Err   error
-	}
-
-	testCases := []TestCase{
-		{Input: 10, N: 0, Err: ErrMustBeLessOrEqual},
-		{Input: 10, N: 10},
-		{Input: 10, N: 15},
-		{Input: 10, N: 10, Excl: true, Err: ErrMustBeLess},
-	}
-
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %g against maximum of %g", n, tc.Input, tc.N)
-
-		f := MaxFloat64(tc.N, tc.Excl)
-		err := f(tc.Input)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+	testCases := map[float64]map[bool]map[float64]error{
+		10: {
+			true:  {0: nil, 1: nil, 2: nil, 10: ErrMustBeLess.With(10), 100: ErrMustBeLess.With(10)},
+			false: {0: nil, 1: nil, 2: nil, 10: nil, 100: ErrMustBeLessOrEqual.With(10)},
+		},
+	}
+
+	for setup, subSetup := range testCases {
+		for excl, values := range subSetup {
+			testMax := MaxFloat64(setup, excl)
+
+			for input, want := range values {
+				t.Run(fmt.Sprintf("%f/%v/%f", setup, excl, input), func(t *testing.T) {
+					got := testMax(input)
+
+					if !errors.Is(got, want) {
+						t.Error("got", got)
+						t.Error("want", want)
+					}
+				})
+			}
 		}
 	}
 }
 
 func TestMin(t *testing.T) {
-	type TestCase struct {
-		Input int
-		N     int
-		Excl  bool
-		Err   error
-	}
-
-	testCases := []TestCase{
-		{Input: 10, N: 0},
-		{Input: 10, N: 10},
-		{Input: 10, N: 15, Err: ErrMustBeGreaterOrEqual},
-		{Input: 10, N: 10, Excl: true, Err: ErrMustBeGreater},
-	}
-
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %d against minimum of %d", n, tc.Input, tc.N)
-
-		f := Min(tc.N, tc.Excl)
-		err := f(tc.Input)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+	testCases := map[int]map[bool]map[int]error{
+		10: {
+			true:  {0: ErrMustBeGreater.With(10), 1: ErrMustBeGreater.With(10), 2: ErrMustBeGreater.With(10), 10: ErrMustBeGreater.With(10), 100: nil},
+			false: {0: ErrMustBeGreaterOrEqual.With(10), 1: ErrMustBeGreaterOrEqual.With(10), 2: ErrMustBeGreaterOrEqual.With(10), 10: nil, 100: nil},
+		},
+	}
+
+	for setup, subSetup := range testCases {
+		for excl, values := range subSetup {
+			testMin := Min(setup, excl)
+
+			for input, want := range values {
+				t.Run(fmt.Sprintf("%d/%v/%d", setup, excl, input), func(t *testing.T) {
+					got := testMin(input)
+
+					if !errors.Is(got, want) {
+						t.Error("got", got)
+						t.Error("want", want)
+					}
+				})
+			}
 		}
 	}
 }
 
 func TestMinFloat32(t *testing.T) {
-	type TestCase struct {
-		Input float32
-		N     float32
-		Excl  bool
-		Err   error
-	}
-
-	testCases := []TestCase{
-		{Input: 10, N: 0},
-		{Input: 10, N: 10},
-		{Input: 10, N: 15, Err: ErrMustBeGreaterOrEqual},
-		{Input: 10, N: 10, Excl: true, Err: ErrMustBeGreater},
-	}
-
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %g against minimum of %g", n, tc.Input, tc.N)
-
-		f := MinFloat32(tc.N, tc.Excl)
-		err := f(tc.Input)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+	testCases := map[float32]map[bool]map[float32]error{
+		10: {
+			true:  {0: ErrMustBeGreater.With(10), 1: ErrMustBeGreater.With(10), 2: ErrMustBeGreater.With(10), 10: ErrMustBeGreater.With(10), 100: nil},
+			false: {0: ErrMustBeGreaterOrEqual.With(10), 1: ErrMustBeGreaterOrEqual.With(10), 2: ErrMustBeGreaterOrEqual.With(10), 10: nil, 100: nil},
+		},
+	}
+
+	for setup, subSetup := range testCases {
+		for excl, values := range subSetup {
+			testMin := MinFloat32(setup, excl)
+
+			for input, want := range values {
+				t.Run(fmt.Sprintf("%f/%v/%f", setup, excl, input), func(t *testing.T) {
+					got := testMin(input)
+
+					if !errors.Is(got, want) {
+						t.Error("got", got)
+						t.Error("want", want)
+					}
+				})
+			}
 		}
 	}
 }
 
 func TestMinFloat64(t *testing.T) {
-	type TestCase struct {
-		Input float64
-		N     float64
-		Excl  bool
-		Err   error
-	}
-
-	testCases := []TestCase{
-		{Input: 10, N: 0},
-		{Input: 10, N: 10},
-		{Input: 10, N: 15, Err: ErrMustBeGreaterOrEqual},
-		{Input: 10, N: 10, Excl: true, Err: ErrMustBeGreater},
-	}
-
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %g against minimum of %g", n, tc.Input, tc.N)
-
-		f := MinFloat64(tc.N, tc.Excl)
-		err := f(tc.Input)
-
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+	testCases := map[float64]map[bool]map[float64]error{
+		10: {
+			true:  {0: ErrMustBeGreater.With(10), 1: ErrMustBeGreater.With(10), 2: ErrMustBeGreater.With(10), 10: ErrMustBeGreater.With(10), 100: nil},
+			false: {0: ErrMustBeGreaterOrEqual.With(10), 1: ErrMustBeGreaterOrEqual.With(10), 2: ErrMustBeGreaterOrEqual.With(10), 10: nil, 100: nil},
+		},
+	}
+
+	for setup, subSetup := range testCases {
+		for excl, values := range subSetup {
+			testMin := MinFloat64(setup, excl)
+
+			for input, want := range values {
+				t.Run(fmt.Sprintf("%f/%v/%f", setup, excl, input), func(t *testing.T) {
+					got := testMin(input)
+
+					if !errors.Is(got, want) {
+						t.Error("got", got)
+						t.Error("want", want)
+					}
+				})
+			}
 		}
 	}
 }

+ 2 - 2
size.go

@@ -10,7 +10,7 @@ var (
 func MaxSize[T any](l int) func([]T) error {
 	return func(value []T) error {
 		if len(value) > l {
-			return ErrMustHaveFewerItems
+			return ErrMustHaveFewerItems.With(l)
 		}
 		return nil
 	}
@@ -20,7 +20,7 @@ func MaxSize[T any](l int) func([]T) error {
 func MinSize[T any](l int) func([]T) error {
 	return func(value []T) error {
 		if len(value) < l {
-			return ErrMustHaveMoreItems
+			return ErrMustHaveMoreItems.With(l)
 		}
 		return nil
 	}

+ 53 - 30
size_test.go

@@ -2,55 +2,78 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
+func ExampleMaxSize() {
+	testMaxSize := MaxSize[string](3)
+	fmt.Println(testMaxSize([]string{"abc", "def", "ghi", "jkl"}))
+	// Output: must have no more than 3 items
+}
+
+func ExampleMinSize() {
+	testMaxSize := MinSize[string](3)
+	fmt.Println(testMaxSize([]string{"abc", "def"}))
+	// Output: must have at least 3 items
+}
+
 func TestMaxSize(t *testing.T) {
-	type TestCase struct {
-		Input []int
-		L     int
-		Err   error
+	testCases := map[int]map[int]error{
+		0:   {0: nil, 1: ErrMustHaveFewerItems.With(0), 2: ErrMustHaveFewerItems.With(0), 10: ErrMustHaveFewerItems.With(0)},
+		1:   {0: nil, 1: nil, 2: ErrMustHaveFewerItems.With(1), 10: ErrMustHaveFewerItems.With(1)},
+		2:   {0: nil, 1: nil, 2: nil, 10: ErrMustHaveFewerItems.With(2)},
+		10:  {0: nil, 1: nil, 2: nil, 10: nil},
+		100: {0: nil, 1: nil, 2: nil, 10: nil},
 	}
 
-	testCases := []TestCase{
-		{Input: []int{1, 2, 3, 4}, L: 8},
-		{Input: []int{1, 2, 3, 4, 5, 6, 7, 8}, L: 8},
-		{Input: []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, L: 8, Err: ErrMustHaveFewerItems},
-	}
+	for max, values := range testCases {
+		testMaxSize := MaxSize[int](max)
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q against maximum length of %d", n, tc.Input, tc.L)
+		for l, want := range values {
+			t.Run(fmt.Sprintf("%d/%d", max, l), func(t *testing.T) {
+				input := []int{}
+				for i := 0; i < l; i++ {
+					input = append(input, i)
+				}
 
-		f := MaxSize[int](tc.L)
-		err := f(tc.Input)
+				got := testMaxSize(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+				if !errors.Is(got, want) {
+					t.Error("got", got)
+					t.Error("want", want)
+				}
+			})
 		}
 	}
 }
 
 func TestMinSize(t *testing.T) {
-	type TestCase struct {
-		Input []int
-		L     int
-		Err   error
+	testCases := map[int]map[int]error{
+		0:   {0: nil, 1: nil, 2: nil, 10: nil},
+		1:   {0: ErrMustHaveMoreItems.With(1), 1: nil, 2: nil, 10: nil},
+		2:   {0: ErrMustHaveMoreItems.With(2), 1: ErrMustHaveMoreItems.With(2), 2: nil, 10: nil},
+		10:  {0: ErrMustHaveMoreItems.With(10), 1: ErrMustHaveMoreItems.With(10), 2: ErrMustHaveMoreItems.With(10), 10: nil},
+		100: {0: ErrMustHaveMoreItems.With(100), 1: ErrMustHaveMoreItems.With(100), 2: ErrMustHaveMoreItems.With(100), 10: ErrMustHaveMoreItems.With(100)},
 	}
 
-	testCases := []TestCase{
-		{Input: []int{1, 2, 3, 4}, L: 8, Err: ErrMustHaveMoreItems},
-		{Input: []int{1, 2, 3, 4, 5, 6, 7, 8}, L: 8},
-		{Input: []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, L: 8},
-	}
+	for min, values := range testCases {
+		testMinSize := MinSize[int](min)
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q against minimum length of %d", n, tc.Input, tc.L)
+		for l, want := range values {
+			t.Run(fmt.Sprintf("%d/%d", min, l), func(t *testing.T) {
+				input := []int{}
+				for i := 0; i < l; i++ {
+					input = append(input, i)
+				}
 
-		f := MinSize[int](tc.L)
-		err := f(tc.Input)
+				got := testMinSize(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
+				if !errors.Is(got, want) {
+					t.Error("got", got)
+					t.Error("want", want)
+				}
+			})
 		}
 	}
 }

+ 20 - 17
url_test.go

@@ -2,30 +2,33 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
+func ExampleURL() {
+	fmt.Println(URL("not a url"))
+	// Output: invalid URL
+}
+
 func TestURL(t *testing.T) {
-	type TestCase struct {
-		Input string
-		Err   error
-	}
+	testCases := map[string]error{
+		"http://example.com":                    nil,
+		"http://subdomain.example.com":          nil,
+		"http://www.example.com/some-page.html": nil,
 
-	testCases := []TestCase{
-		{Input: "http://example.com"},
-		{Input: "http://subdomain.example.com"},
-		{Input: "http://www.example.com/some-page.html"},
-		{Input: "subdomain.com", Err: ErrInvalidURL},
-		{Input: "not a url", Err: ErrInvalidURL},
+		"not a url":     ErrInvalidURL,
+		"subdomain.com": ErrInvalidURL,
 	}
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q", n, tc.Input)
-
-		err := URL(tc.Input)
+	for input, want := range testCases {
+		t.Run(input, func(t *testing.T) {
+			got := URL(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
-		}
+			if !errors.Is(got, want) {
+				t.Error("got", got)
+				t.Error("want", want)
+			}
+		})
 	}
 }

+ 22 - 19
uuid_test.go

@@ -2,32 +2,35 @@ package validate
 
 import (
 	"errors"
+	"fmt"
 	"testing"
 )
 
+func ExampleUUID() {
+	fmt.Println(UUID("not a uuid"))
+	// Output: invalid UUID
+}
+
 func TestUUID(t *testing.T) {
-	type TestCase struct {
-		Input string
-		Err   error
-	}
+	testCases := map[string]error{
+		"00000000-0000-0000-0000-000000000000": nil,
+		"01234567-89ab-cdef-0123-456789abcdef": nil,
+		"abcdef01-2345-6789-abcd-ef0123456789": nil,
 
-	testCases := []TestCase{
-		{Input: "00000000-0000-0000-0000-000000000000"},
-		{Input: "01234567-89ab-cdef-0123-456789abcdef"},
-		{Input: "abcdef01-2345-6789-abcd-ef0123456789"},
-		{Input: "Not a UUID", Err: ErrInvalidUUID},
-		{Input: "00000000-00-0000-0000-00000000000000", Err: ErrInvalidUUID},
-		{Input: "00000000000000000000000000000000", Err: ErrInvalidUUID},
-		{Input: "01234567-89ab-cdef-ghij-klmnopqrstuv", Err: ErrInvalidUUID},
+		"not a uuid":                           ErrInvalidUUID,
+		"00000000-00-0000-0000-00000000000000": ErrInvalidUUID,
+		"00000000000000000000000000000000":     ErrInvalidUUID,
+		"01234567-89ab-cdef-ghij-klmnopqrstuv": ErrInvalidUUID,
 	}
 
-	for n, tc := range testCases {
-		t.Logf("(%d) Testing %q", n, tc.Input)
-
-		err := UUID(tc.Input)
+	for input, want := range testCases {
+		t.Run(input, func(t *testing.T) {
+			got := UUID(input)
 
-		if !errors.Is(err, tc.Err) {
-			t.Errorf("Expected error %v, got %v", tc.Err, err)
-		}
+			if !errors.Is(got, want) {
+				t.Error("got", got)
+				t.Error("want", want)
+			}
+		})
 	}
 }