should
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
[](https://golang.com/)
[](https://pkg.go.dev/github.com/Kairum-Labs/should)
[](https://codecov.io/gh/Kairum-Labs/should)
[](https://github.com/Kairum-Labs/should/actions/workflows/golangci.yml)
[](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.