Awesome Go

should

CategoryTesting
SubcategoryTesting Frameworks
Stars48

Testing library with zero dependencies, detailed struct diffs and human-readable error messages

About should

This large README is shown as a plain-text preview. Read the full README at the source

# Should - A Go Assertion Library

[![go](https://img.shields.io/badge/go-1.22+-blue)](https://golang.com/)
[![Go Reference](https://pkg.go.dev/badge/github.com/Kairum-Labs/should.svg)](https://pkg.go.dev/github.com/Kairum-Labs/should)
[![codecov](https://codecov.io/gh/Kairum-Labs/should/branch/main/graph/badge.svg)](https://codecov.io/gh/Kairum-Labs/should)
[![Lint](https://github.com/Kairum-Labs/should/actions/workflows/golangci.yml/badge.svg)](https://github.com/Kairum-Labs/should/actions/workflows/golangci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

<div align="center">
  <img src="assets/should.png" alt="should project logo, a Go assertions library" width="500">
</div>

`Should` is a lightweight and intuitive assertion library for Go, designed to make your tests more readable and expressive. It provides **exceptionally detailed error messages** to help you debug failures faster and understand exactly what went wrong.

## Features

- **Detailed Error Messages**: Get comprehensive, contextual error information for every assertion type.
- **Smart String Handling**: Automatic multiline formatting for long strings and truncation with context.
- **Numeric Comparisons**: Detailed difference calculations with helpful hints for numeric assertions.
- **Time Comparisons**: Compare times with options to ignore timezone and/or nanoseconds with clear diffs.
- **Empty/Non-Empty Checks**: Rich context about collection types, sizes, and content.
- **String Similarity**: When a string assertion fails, `Should` suggests similar strings from your collection to help you spot typos.
- **Type-Safe**: Uses Go generics for type safety while maintaining a clean API.

## Installation

**Requirements:** Go 1.22 or later

```bash
go get github.com/Kairum-Labs/should
```

## Quick Start

```go
package main

import (
	"testing"
	"github.com/Kairum-Labs/should"
)

func TestBasicAssertions(t *testing.T) {
	// Boolean assertions
	should.BeTrue(t, true)
	should.BeFalse(t, false)

	// Equality checks
	should.BeEqual(t, "hello", "hello")
	should.BeEqual(t, 42, 42)

	// Numeric comparisons
	should.BeGreaterThan(t, 10, 5)
	should.BeLessThan(t, 3, 7)
	should.BeLessOrEqualTo(t, 5, 10)

	// Range validation
	should.BeInRange(t, user.Age, 18, 65)
	should.BeInRange(t, testScore, 0, 100)
	should.BeInRange(t, response.StatusCode, 200, 299)

	// Time comparisons
	should.BeSameTime(t, t1, t2)
	should.BeSameTime(t, t1, t2, should.WithIgnoreTimezone())
	should.BeSameTime(t, t1, t2, should.WithTruncate(time.Second))

	// Numeric comparisons with custom messages
	should.BeGreaterThan(t, user.Age, 18, should.WithMessage("User must be adult"))
	should.BeGreaterOrEqualTo(t, score, 0, should.WithMessage("Score cannot be negative"))
	should.BeLessOrEqualTo(t, user.Age, 65, should.WithMessage("User must be under retirement age"))

	// Stop the test immediately if a critical assertion fails
	should.NotBeError(t, err, should.WithFailFast())

	// Empty/Non-empty checks
	should.BeEmpty(t, "")
	should.NotBeEmpty(t, []int{1, 2, 3})

	// String operations
	should.StartWith(t, "Hello, World!", "Hello")
	should.EndWith(t, "Hello, World!", "World!")
	should.ContainSubstring(t, "Hello, World!", "World")

	// Collection operations
	users := []string{"Alice", "Bob", "Charlie"}
	should.Contain(t, users, "Alice")
	should.NotContain(t, users, "David")
	should.Contain(t, userIDs, targetID, should.WithMessage("User ID must exist in the system"))

	// Sort check
	should.BeSorted(t, []int{1, 2, 3, 4, 5})
	should.BeSorted(t, []string{"apple", "banana", "cherry"})
	should.BeSorted(t, scores, should.WithMessage("Test scores must be in ascending order"))
}
```

## Detailed Error Messages

### Empty/Non-Empty Assertions

`Should` provides rich context for empty and non-empty checks:

```go
// Short string
should.BeEmpty(t, "Hello World!")
// Output:
// Expected value to be empty, but it was not:
//         Type    : string
//         Length  : 12 characters
//         Content : "Hello World!"

// Long string (automatically formatted)
longText := "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
should.BeEmpty(t, longText)
// Output:
// Expected value to be empty, but it was not:
// Length: 516 characters, 9 lines
// 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
// 2.  Sed do eiusmod tempor incididunt ut labore et dolore ma
// 3. gna aliqua. Ut enim ad minim veniam, quis nostrud exerci
// 4. tation ullamco laboris nisi ut aliquip ex ea commodo con
// 5. sequat. Duis aute irure dolor in reprehenderit in volupt
//
// Last lines:
// 7. xcepteur sint occaecat cupidatat non proident, sunt in c
// 8. ulpa qui officia deserunt mollit anim id est laborum. Vi
// 9. vamus sagittis lacus vel augue laoreet rutrum faucibus d

// Large slice (shows truncated content)
largeSlice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
should.BeEmpty(t, largeSlice)
// Output:
// Expected value to be empty, but it was not:
//         Type    : []int
//         Length  : 15 elements
//         Content : [1, 2, 3, ...] (showing first 3 of 15)

// Empty slice
should.NotBeEmpty(t, []int{})
// Output:
// Expected value to be not empty, but it was empty:
//         Type    : []int
//         Length  : 0 elements
```

### Numeric Comparisons

Get detailed information about numeric comparison failures:

```go
// Basic comparison with custom message
should.BeGreaterThan(t, 5, 10, should.WithMessage("Score validation failed"))
// Output:
// Score validation failed
// Expected value to be greater than threshold:
//         Value     : 5
//         Threshold : 10
//         Difference: -5 (value is 5 smaller)
//         Hint      : Value should be larger than threshold

// Equal values
should.BeGreaterThan(t, 42, 42)
// Output:
// Expected value to be greater than threshold:
//         Value     : 42
//         Threshold : 42
//         Difference: 0 (values are equal)
//         Hint      : Value should be larger than threshold

// Float precision
should.BeLessThan(t, 3.14, 2.71)
// Output:
// Expected value to be less than threshold:
//         Value     : 3.14
//         Threshold : 2.71
//         Difference: +0.43000000000000016 (value is 0.43000000000000016 greater)
//         Hint      : Value should be smaller than threshold

// Large numbers
should.BeLessThan(t, 1000000, 999999)
// Output:
// Expected value to be less than threshold:
//         Value     : 1000000
//         Threshold : 999999
//         Difference: +1 (value is 1 greater)
//         Hint      : Value should be smaller than threshold

// Less than or equal (fails when value is greater)
should.BeLessOrEqualTo(t, 15, 10)
// Output:
// Expected value to be less than or equal to threshold:
//         Value     : 15
//         Threshold : 10
//         Difference: +5 (value is 5 greater)
//         Hint      : Value should be smaller than or equal to threshold

// Fails because 3.142 is not within ±0.001 of 3.140.
should.BeWithin(t, 3.142, 3.14, 0.001)
// Expected 3.142000 to be within ±0.001000 of 3.140000
// Difference: 0.002000 (100.00% greater than tolerance)

// Range validation (fails when value is below or above the range)
should.BeInRange(t, 16, 18, 65)
// Output:
// Expected value to be in range [18, 65], but it was below:
//         Value    : 16
//         Range    : [18, 65]
//         Distance : 2 below minimum (16 < 18)
//         Hint     : Value should be >= 18

// Range validation (fails when value is above the range)
should.BeInRange(t, 105, 0, 100)
// Output:
// Expected value to be in range [0, 100], but it was above:
//         Value    : 105
//         Range    : [0, 100]
//         Distance : 5 above maximum (105 > 100)
//         Hint     : Value should be <= 100

// Range validation with custom message
should.BeInRange(t, 150, 0, 100, should.WithMessage("Battery level must be valid percentage"))
// Output:
// Battery level must be valid percentage
// Expected value to be in range [0, 100], but it was above:
//         Value    : 150
//         Range    : [0, 100]
//         Distance : 50 above maximum (150 > 100)
//         Hint     : Value should be <= 100
```

### Struct and Object Comparisons

When comparing complex objects, `Should` shows exactly what differs:

```go
type Person struct {
    Name string
    Age  int
}

p1 := Person{Name: "John", Age: 30}
p2 := Person{Name: "Jane", Age: 25}
should.BeEqual(t, p1, p2)

// Output:
// Differences found:
// Not equal:
// expected: {Name: "Jane", Age: 25}
// actual  : {Name: "John", Age: 30}
// Field differences:
//   └─ Name: "Jane" ≠ "John"
//   └─ Age: 25 ≠ 30

// Ensure values are NOT equal
p3 := Person{Name: "John", Age: 30}
should.NotBeEqual(t, p1, p3)
// Output when values are equal:
// Expected values to be different, but they are equal
```

### Length and Type Assertions

Get clear feedback on length and type mismatches.

```go
// Incorrect length
should.HaveLength(t, []string{"apple", "banana"}, 3)
// Output:
// Expected collection to have specific length:
// Type          : []string
// Expected Length: 3
// Actual Length : 2
// Difference    : -1 (1 element missing)

// Incorrect type
type Dog struct{ Name string }
type Cat struct{ Name string }
var d Dog
should.BeOfType(t, Cat{Name: "Whiskers"}, d)
// Output:
// Expected value to be of specific type:
// Expected Type: should_test.Dog
// Actual Type  : should_test.Cat
// Difference   : Different concrete types
```

### String Similarity Detection

When checking for strings in slices, `Should` helps you find typos:

```go
users := []string{"user-one", "user_two", "UserThree", "user-3", "userThree"}
should.Contain(t, users, "user3")

// Output includes helpful suggestions:
// Expected collection to contain element:
//         Collection: [user-one, user_two, UserThree, user-3, userThree]
//         Missing   : user3
//
//         Found similar: user-3 (at index 3) - 1 extra character
```

### Numeric Context Information

When checking for numeric in slices, `Should` shows where the value would fit:

```go
numbers := []int{10, 80, 20, 70, 30, 60, 40, 50, 0, 100, 90, 120, 110} // 13 elements, unsorted
should.Contain(t, numbers, 55)

// Output includes context information:
// Expected collection to contain element:
// Collection: [10, 80, 20, 70, 30, ..., 0, 100, 90, 120, 110] (showing first 5 and last 5 of 13 elements)
// Missing  : 55
//
// Element 55 would fit between 50 and 60 in sorted order
// └─ Sorted view: [..., 40, 50, 60, 70, ...]
```

### Set Membership Assertions

Check if a value is part of a set of allowed options.

```go
should.BeOneOf(t, "pending", []string{"active", "inactive", "suspended"})
// Output:
// Expected value to be one of the allowed options:
// Value   : "pending"
// Options : ["active", "inactive", "suspended"]
// Count   : 0 of 3 options matched
```

### String Prefix and Suffix Assertions

Check if strings start or end with specific substrings, with intelligent case handling:

```go
// Basic string prefix checking
should.StartWith(t, "Hello, World!", "Hello")

// Case-sensitive by default
should.StartWith(t, "Hello, World!", "hello")
// Output:
// Expected string to start with 'hello', but it starts with 'Hello'
// Expected : 'hello'
// Actual   : 'Hello, World!'
//             ^^^^^
//           (actual prefix)
// Note: Case mismatch detected (use should.WithIgnoreCase() if intended)

// Case-insensitive option
should.StartWith(t, "Hello, World!", "hello", should.WithIgnoreCase())

// Long strings: only the beginning is shown (where the prefix appears)
actual := strings.Repeat("a", 200) + "_suffix"
should.StartWith(t, actual, "different")
// Output:
// Expected string to start with 'different', but it starts with 'aaaaaaaaa'
// Expected : 'different'
// Actual   : 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… (truncated)'
//             ^^^^^^^^^
//           (actual prefix)

// Negative counterpart — assert string does NOT start with a prefix
should.NotStartWith(t, "Hello, World!", "temp_")

// Fails when the prefix matches:
should.NotStartWith(t, "temp_user_data.txt", "temp_")
// Output:
// Expected string to NOT start with 'temp_', but it does:
// Prefix   : 'temp_'
// Actual   : 'temp_user_data.txt'
//             ^^^^^
//           (matching prefix)

// With custom message
should.NotStartWith(t, username, "admin_",
	should.WithMessage("Username cannot start with admin prefix"))
// Output:
// Username cannot start with admin prefix
// Expected string to NOT start with 'admin_', but it does:
// Prefix   : 'admin_'
// Actual   : 'admin_user123'
//             ^^^^^^
//           (matching prefix)

// Case-insensitive negative check
should.NotStartWith(t, "TEMP_file.txt", "temp_", should.WithIgnoreCase())

// String suffix checking
should.EndWith(t, "Hello, World!", "World!")

// Case-sensitive by default
should.EndWith(t, "Hello, World!", "Hello")
// Output:
// Expected string to end with 'Hello', but it ends with 'orld!'
// Expected : 'Hello'
// Actual   : 'Hello, World!'
//                     ^^^^^
//                     (actual suffix)

// Case mismatch detection
should.EndWith(t, "Hello, World", "world")
// Output:
// Expected string to end with 'world', but it ends with 'World'
// Expected : 'world'
// Actual   : 'Hello, World'
//                    ^^^^^
//                    (actual suffix)
// Note: Case mismatch detected (use should.WithIgnoreCase() if intended)

// Case-insensitive option
should.EndWith(t, "Hello, World!", "world!", should.WithIgnoreCase())

// Long strings: only the tail is shown (where the suffix appears)
actual = "prefix_" + strings.Repeat("a", 200)
should.EndWith(t, actual, "different")
// Output:
// Expected string to end with 'different', but it ends with 'aaaaaaaaa'
// Expected : 'different'
// Actual   : '(truncated) …aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
//                                                                         ^^^^^^^^^
//                                                                        (actual suffix)

// With custom messages
should.StartWith(t, filename, "temp_", should.WithMessage("Temporary files must have temp_ prefix"))
should.EndWith(t, filename, ".log", should.WithMessage("Log files must have .log extension"))

// Negated suffix checking
should.NotEndWith(t, "Hello, world!", "planet")  // passes
should.NotEndWith(t, "Hello, world!", "world!") // fails
// Output:
// Expected string to NOT end with 'world!', but it does
// Expected : 'world!'
// Actual   : 'Hello, world!'

// Case-insensitive negated suffix
should.NotEndWith(t, "Hello, WORLD", "world", should.WithIgnoreCase()) // fails

// With custom messages
should.NotEndWith(t, filename, ".tmp", should.WithMessage("Output files must not have .tmp extension"))
```

### String Substring Assertions

Check if strings contain specific substrings, with intelligent formatting for long strings:

```go
// Basic substring checking
should.ContainSubstring(t, "Hello, World!", "World")

// Case-sensitive by default
should.ContainSubstring(t, "Hello, World!", "world")
// Output:
// Expected string to contain "world", but found case difference
// Substring: "world"
// Found    : "World" at position 7
// Note: Case mismatch detected (use should.WithIgnoreCase() if intended)

// Case-insensitive option
should.ContainSubstring(t, "Hello, World!", "world", should.WithIgnoreCase())

// Typo detection for short substrings (≤20 characters)
should.ContainSubstring(t, "Hello, beautiful world!", "beatiful")
// Output:
// Expected string to contain "beatiful", but it was not found
// Substring   : "beatiful"
// Actual   : "Hello, beautiful world!"
//
// Similar substring found:
//   └─ 'beautiful' at position 7 - 1 character differs

// Similar match with transposition
should.ContainSubstring(t, "test testing tester", "tets")
// Output:
// Expected string to contain "tets", but it was not found
// Substring   : "tets"
// Actual   : "test testing tester"
//
// Similar substring found:
//   └─ 'test' at position 0 - 1 character differs

// Long strings with multiline formatting
longText := `This is a very long text that spans multiple lines
and contains various keywords and phrases that we might
want to search for in our test assertions.`

should.ContainSubstring(t, longText, "nonexistent")
// Output:
// Expected string to contain "nonexistent", but it was not found
// Substring   : "nonexistent"
// Actual   : (length: 149)
// This is a very long text that spans multiple lines
// and contains various keywords and phrases that we might
// want to search for in our test assertions.

// With custom messages
should.ContainSubstring(t, logContent, "ERROR", should.WithMessage("Log should contain error messages"))
should.ContainSubstring(t, apiResponse, "success", should.WithIgnoreCase(), should.WithMessage("API response should indicate success"))
```

**Note**: Typo detection using Levenshtein distance is automatically enabled for substrings up to 20 characters to maintain good performance. For longer substrings, only exact matching is performed.

**Known limitation**: Multiline previews of long strings are sliced on rune (code point) boundaries, so multi-byte characters are never corrupted in the output. They are not sliced on grapheme-cluster boundaries, so a combined sequence such as an emoji ZWJ sequence (for example, a family emoji) can still be split across two preview lines. This affects only the rendered error preview, never the assertion result.

### Duplicate Detection

Ensure collections contain no duplicate values with detailed reporting:

```go
// Check for duplicates in slices
should.NotContainDuplicates(t, []int{1, 2, 3, 4, 5}) // passes

should.NotContainDuplicates(t, []int{1, 2, 2, 3, 3, 3})
// Output:
// Expected no duplicates, but found 2 duplicate values:
// └─ 2 appears 2 times at indexes [1, 2]
// └─ 3 appears 3 times at indexes [3, 4, 5]
```

### Error Assertions

Handle error check with clear, informative messages:

```go
// Basic usage
should.BeError(t, err)
should.NotBeError(t, err)

// Check specific error types
var pathErr *os.PathError
should.BeErrorAs(t, err, &pathErr)

// Check specific error values
should.BeErrorIs(t, err, io.EOF)

// With custom messages
should.BeError(t, err, should.WithMessage("Expected operation to fail"))
should.NotBeError(t, err, should.WithMessage("Expected operation to pass"))
should.BeErrorAs(t, err, &pathErr, should.WithMessage("Expected path error"))
should.BeErrorIs(t, err, context.Canceled, should.WithMessage("Expected cancellation"))

// Outputs
// BeError - error required
should.BeError(t, err)
// Expected an error, but got nil

// NotBeError - no error
should.NotBeError(t, err)
// Expected no error, but got an error
// Error: "something went wrong"
// Type: *errors.errorString


// BeErrorAs - type not found
var pathErr *os.PathError
should.BeErrorAs(t, err, &pathErr)
// Expected error to be *os.PathError, but type not found in error chain
// Error: "invalid character 'i' looking for beginning of value"
// Types  : [*json.SyntaxError]

// BeErrorIs - value not found
should.BeErrorIs(t, err, io.EOF)
// Expected error to be "EOF", but not found in error chain
// Error: "connection refused"
// Types  : [*net.OpError, syscall.Errno]
```

### Time Assertions

Compare times with flexible options:

```go
// Basic usage
should.BeSameTime(t, actual, expected)

// With custom messages
should.BeSameTime(t, actual, expected, should.WithMessage("Expected times to match but they differ"))

// With options
should.BeSameTime(t, actual, expected,
    should.WithIgnoreTimezone(),
    should.WithTruncate(time.Second),
)

time1 := time.Date(2024, 1, 15, 14, 30, 0, 0, time.UTC)
time2 := time.Date(2024, 1, 15, 14, 30, 2, 500000000, time.UTC)

should.BeSameTime(t, time1, time2)
// Expected times to be the same, but difference is 2.5s
// Expected: 2024-01-15 14:30:00 UTC
// Actual  : 2024-01-15 14:30:02.5 UTC (2.5s later)

Frequently Asked Questions

What is should?

should is a Testing library for the Go programming language. Testing library with zero dependencies, detailed struct diffs and human-readable error messages

How do I install should?

Install should with the Go module system using `go get Kairum-Labs/should`. Check the repository for the current installation instructions.

What category does should belong to?

should is listed under Testing, specifically Testing Frameworks.

← Back to Testing Frameworks