
@@ -15,7 +15,7 @@
# blugnu/test
-Provides a concise, fluent, type-safe API over the standard testing framework, simplifying common tests
+A concise, fluent, type-safe API over the standard testing framework, simplifying common tests
in an extensible fashion whilst maintaining compatibility with the standard testing package.
_Friends don't let friends write tests that are hard to read, hard to maintain, or that
@@ -41,7 +41,7 @@ func TestDoSomething(t *testing.T) {
}
```
-Do this instead:
+Do this:
```go
func TestDoSomething(t *testing.T) {
@@ -129,13 +129,14 @@ value from the test function as an argument:
}
```
-> :bulb: Calling `Parallel(t)` is equivalent to calling `With(t)` followed by `Parallel()` or `t.Parallel()`.
+> :bulb: to execute a test parallel, use `Parallel(t)` instead of `With(t)`; this is equivalent
+> to calling `With(t)` followed by `t.Parallel()`.
-There is no cleanup required after calling `With(t)`; the test frame is automatically cleaned up
-when the test completes.
+There is no cleanup required after calling `With(t)` or `Parallel(t)`; the test frame is
+automatically cleaned up when the test completes.
-If you use the `blugnu/test` package functions for running table-driven tests or explicit subtests
-the test frame stack is managed for you:
+If you use the `blugnu/test` package functions for running subtests, the test frame
+is managed for you:
```go
func TestDoSomething(t *testing.T) {
@@ -148,9 +149,9 @@ the test frame stack is managed for you:
}
```
-If a new test frame is created outside of the `test` package, then the `With(t)` function
-must be called again to push that test frame onto the stack. For example, if you choose to create
-a subtest using `*testing.T.Run()` and want to use the `blugnu/test` functions in that subtest:
+However, if a new test frame is created outside of the `test` package, then `With(t)` must be
+called to push that test frame onto the stack. For example, if you choose to create a subtest
+using `*testing.T.Run()` and want to use the `blugnu/test` functions in that subtest:
```go
func TestDoSomething(t *testing.T) {
@@ -173,15 +174,15 @@ for each test function.
### Writing a Test: Expect
-Almost all tests are written using `Expect` to create an expectation
-over some value (the _subject_). `Expect` returns an _expectation_ with
-methods for testing the subject.
+Almost all tests are written using `Expect` to create an expectation for some value
+(the _subject_). `Expect` returns an _expectation_ with methods for testing the subject.
Some expectation methods test the value directly, such as `IsEmpty()`, `IsNil()` and `IsNotNil()`:
```go
- err := DoSomething()
+ result, err := Sum(2, 2)
Expect(err).IsNil()
+ Expect(result).To(Equal(4))
```
### Using Matchers
@@ -322,7 +323,7 @@ is not possible.
- [Testing Slices](#testing-slices)
- [Testing Context](#testing-context)
-## [Test Runners](#test-runner-functions)
+## [Test Runners](#test-runners)
- [Subtests](#subtests)
- [Table-Driven Tests](#table-driven-tests)
@@ -337,7 +338,7 @@ In addition to performing common, basic tests, the `test` package also provides
| [Mocking Functions](#mocking-functions) | mock functions for testing |
| [Recording Console Output](#recording-console-output) | record output of a function that writes to `stdout` and/or `stderr` |
| [Test for an Expected Type](#test-for-an-expected-type) | test that a value is of an expected type |
-| [Testing Test Helpers](#testing-test-helpers) | test your own test helper functions |
+| [Testing a Test Helper](#testing-a-test-helper) | test your own test helper functions |
------
@@ -430,41 +431,84 @@ provided on expectations:
## Testing that an Error did not occur
-There are two ways to explicitly test that an error did not occur:
+There are three ways to test that an error did _not_ occur:
```go
Expect(err).DidNotOccur()
Expect(err).IsNil()
+ Expect(err).Is(nil) // useful in table-driven tests
```
-A third way to test that an error did not occur is to use the `Is()` method, passing `nil`
-as the expected error:
-
-```go
- Expect(err).Is(nil)
-```
-
-This is most useful when testing an error in a table driven test where each test case
+The last form is particularly useful in table-driven tests where each test case
may have an expected error or `nil`:
```go
- Expect(err).Is(tc.err) // tc.err may be nil or an expected error
+ Expect(err).Is(tc.err) // tc.err may be nil or some expected error
```
## Testing that an Error occurred (any error)
+There are two ways to test that some error _did_ occur without specifying
+a specific error or error type:
+
```go
Expect(err).DidOccur()
Expect(err).IsNotNil()
```
+> _It is generally **not** recommended to test errors in this way as the test_
+> _is vague and vulnerable to false positives; but it may be useful or even_
+> _necessary in some cases_.
+
## Testing that a Specific Error Occurred
+There are three ways to test that a specific error or an error of a specific type
+_did_ occur:
+
+```go
+ // an expected error value (errors.Is)
+ Expect(err).Is(someExpectedError)
+
+ // an expected error type (errors.As)
+ v, ok := Expect(err).To(BeError[E]())
+ v, ok := expect.Error[E](err)
+```
+
+The first form uses `errors.Is()` to determine whether the error matches the
+expected error.
+
+The other forms test for a specific error type, using `errors.As()` where the
+type parameter `E` is the expected error type. The tests will pass if the error
+is of type `E` (or wraps an error of that type), returning the matched error
+value of type `E` and a boolean indicating whether the test passed.
+
+If the test fails the indicator will be false and the matched value will be
+the zero value of `E`.
+
+## Expected vs Required Errors
+
+If an expected error is critical to a test, further test execution may be
+short-circuited by passing the `opt.IsRequired(true)` option:
+
+```go
+ Expect(err).IsNil(opt.Required())
+```
+
+Alternatively, the `Require()` function may be used to create the expectation:
+
+```go
+ Require(err).IsNil()
+```
+
+Finally, for the generic form of testing for an expected error type, the
+`require` package provides a short-circuit variant of the `Error[E]()` test:
+
```go
- Expect(err).Is(expectedError) // passes if `errors.Is(err, expectedError)` is true
+ v := require.Error[E](err)
```
-> _If `nil` is passed as the expected error, the test is equivalent to `IsNil()`_.
+Since the test will _only_ return if the test passes, the function only returns
+the matched error of type `E`. No boolean indicator is returned in this case.
# Testing for Panics
@@ -645,16 +689,16 @@ By contrast:
## Any-Matchers
-An any-matcher is a matcher that accepts `any` as the subject type, allowing it to be used
-with literally any value. Any-matchers are used with the `Should()` or `ShouldNot()` matching
-methods.
+An any-matcher is a matcher that accepts `any` as the subject type, allowing it
+to be used with literally any value. Any-matchers are used with the `Should()`
+or `ShouldNot()` matching methods.
> Any-matchers may also be used with the `To()` or `ToNot()` matching methods
-> if the formal type of the subject is `any`, but this is not recommended.
+> but only if the formal type of the subject is `any`; this is not recommended.
# Built-In Matchers
-A number of matchers are provided in the `test` package, including:
+A number of matchers are provided, including:
| Factory Function | Subject Type | Description |
@@ -678,7 +722,7 @@ A number of matchers are provided in the `test` package, including:
| `HaveContextValue(K,V)` | `context.Context` | Tests that the context contains the expected key and value |
-Matchers are used by passing the matcher to one of th expectation matching methods together
+Matchers are used by passing the matcher to one of the expectation matching methods together
with options to control the behaviour of the expectation or the matcher itself.
A matcher is typically constructed by a factory function accepting any arguments required
@@ -721,17 +765,17 @@ Options supported by matching methods (and therefore _all_ matchers) include:
| `opt.OnFailure(string)` | a string to use as the error report for the test failure; this overrides the default error report for the matcher |
| `opt.AsDeclaration(bool)` | a boolean to indicate whether values (other than strings) in test failure reports should be formatted as declarations (`%#v` rather than `%v`) |
| `opt.QuotedStrings(bool)` | a boolean to indicate whether string values should be quoted in failure reports; defaults to `true` |
-| `opt.IsRequired(bool)` | a boolean to indicate whether the expectation is required; defaults to `false` |
+| `opt.IsRequired(bool)` | a boolean to indicate whether the expectation is required, short-circuiting the current test on failure; defaults to `false` |
> `opt.OnFailure()` is a convenience function that returns an `opt.FailureReport` with a
> function that returns the specified string in the report.
>
-> `opt.FailureReport` and `opt.OnFailure()` are mutually exclusive; if both are specified, only the
-> first in the options list will be used.
+> If both `opt.FailureReport` and `opt.OnFailure()` are specified or multiple options
+> are provided of either type, only the first such option will be used.
-The `...any` argument to an `opt.FailureReport` function is used to pass any options supplied to the
-matcher, so that the error report can respect those options where appropriate.
+The `...any` argument to an `opt.FailureReport` function is used to pass any options
+supplied to the matcher, so that the error report can respect those options where appropriate.
> See the [Custom Failure Report Guide](.assets/readme/custom-failure-reports.md) for details.
@@ -862,7 +906,11 @@ expected by the matcher must be cast to that type:
------
-# Test Runner Functions
+# Test Runners
+
+- [Subtests](#subtests)
+- [Table-Driven Tests](#table-driven-tests)
+- [Flaky Tests](#flaky-tests)
The `testing.T` type is the standard test runner in Go. This type also
provides a way to run subtests using the `test.Run()` function, which
@@ -970,7 +1018,7 @@ There are two ways to skip/debug test cases:
```go
- type TestCase struct {
+ type testcase struct {
// fields...
skip bool // set true to skip this test case
debug bool // set true to debug this test case
@@ -978,27 +1026,27 @@ There are two ways to skip/debug test cases:
// skip a test case
Run(Testcases(
- ForEach(func(tc TestCase) {
+ ForEach(func(tc testcase) {
// test code here
}),
- Skip("first case", TestCase{...debug: true}), // this test case will be skipped; the `debug` field is overridden by the Skip() function
- Case("second case", TestCase{...}),
+ Skip("first case", testcase{...debug: true}), // this test case will be skipped; the `debug` field is overridden by the Skip() function
+ Case("second case", testcase{...}),
))
// debug a test case
Run(Testcases(
- ForEach(func(tc TestCase) {
+ ForEach(func(tc testcase) {
// test code here
}),
- Debug("first case", TestCase{... skip: true}), // ONLY this test case will be run; the 'skip' field is overridden by the Debug() function
- Case("second case", TestCase{...}),
+ Debug("first case", testcase{... skip: true}), // ONLY this test case will be run; the 'skip' field is overridden by the Debug() function
+ Case("second case", testcase{...}),
))
```
## Flaky Tests
-Flaky tests are tests that may fail intermittently, often due to timing issues
+Flaky tests are tests that may fail intermittently, due to timing issues
or other non-deterministic factors. The `test` package provides a way to run
flaky tests using the `FlakyTest()` runner.
@@ -1010,9 +1058,15 @@ maximum number of attempts and/or until a maximum elapsed time has passed
If the test passes before the maximum number of attempts or elapsed time is reached,
the test passes; any failed attempts are ignored.
-If the test fails on all attempts, the test fails, and the outcome of each attempt
+If the test fails on all attempts, the test fails and the outcome of each attempt
is reported in the test output.
+## Helper Test Scenarios
+
+The `HelperTests()` runner is a specialised runner for testing a test helper
+or custom matcher. See the [Running Multiple Helper Test Scenarios](#running-multiple-helper-test-scenarios)
+section for details.
+
------
@@ -1137,7 +1191,7 @@ outcome as an argument to the `R.Expect()` method:
/* your test code here */
})
- result.Expect(TestPassed)
+ result.Expect(test.Passed)
```
## Testing Test Helper Output
@@ -1160,24 +1214,38 @@ method:
> :bulb: By testing the output, the test is implicitly expected to fail, so the
> `R.Expect()` method in this case will also test that the outcome is `TestFailed`.
-## Running Multiple Test Scenarios
+## Running Multiple Helper Test Scenarios
-A specialised version of `RunScenarios()` is provided to test a test helper or
-custom matcher: `RunTestScenarios()`. This accepts a slice of `TestScenario` values,
-where each scenario is a test case to be run against your test helper or matcher.
+The `HelperTests()` test runner is provided to run multiple test scenarios for testing
+a test helper or custom matcher. The runner accepts a slice of `HelperScenario`
+test cases:
+
+```go
+ Run(HelperTests([]HelperScenario{
+ {
+ Scenario: "test scenario 1",
+ Act: func() { /* test code */ },
+ Assert: func(r *R) { /* assertions */ },
+ },
+ {
+ Scenario: "test scenario 2",
+ Act: func() { /* test code */ },
+ Assert: func(r *R) { /* assertions */ },
+ },
+ }...))
+```
-`RunTestScenarios()` implements a test runner function for you, so all you need
-to do is provide a slice of scenarios, with each scenario consisting of:
+Each scenario consists of:
- `Scenario`: a name for the scenario (scenario); each scenario is run in a subtest
using this name;
-- `Act`: a function that contains the test code for the scenario; this function has
- the signature `func()`;
+- `Act`: a function that contains the test code for the scenario;
- `Assert`: a function that tests the test outcome; this function has the signature
`func(*R)` where `R` is the result of the test scenario.
-The `Assert` function is optional; if not provided the scenario is one where the
-test is expected to pass without any errors or failures.
+The `Assert` function is optional; if not provided the scenario represents a case
+where the test is expected to pass without any errors or failures and producing no
+test output.
### Debugging and Skipping Scenarios
@@ -1186,8 +1254,9 @@ subset, or specific test, or to ignore scenarios that are not yet implemented or
known to be failing with problems which you wish to ignore while focussing on
other scenarios.
-The `RunTestScenarios()` function and `TestScenario` type support this by
-providing a `Skip` and `Debug` field on each `TestScenario`.
+Similar to the table-driven test runners, this can be accomplished by
+skipping scenarios or marking them for debugging using the `Debug` and `Skip`
+fields of the `HelperScenario` struct.
> :warning: When setting either `Debug` or `Skip` to `true`, it is important
to remember to remove those settings when you are ready to move on to the next
@@ -1202,7 +1271,7 @@ focus of your testing.
},
{
Scenario: "test scenario 2",
- Skip: true, // <== this scenario won't run
+ Skip: true, // <== this scenario won't run
Act: func() { /* test code */ },
Assert: func(r *R) { /* assertions */ },
},
@@ -1213,14 +1282,14 @@ Setting `Skip` to `true` may be impractical if you have a large number of scenar
and want to run only a few of them. In this case, you can use the `Debug` field to
focus on a single scenario or a subset of scenarios.
-## Debugging Scenarios
+### Debugging Scenarios
> :bulb: Setting `Debug` does not invoke the debugger or subject a test scenario to
> any special treatment, beyond selectively running it. The name merely reflects that it
> most likely to be of use when debugging.
When `Debug` is set to `true` on any one or more scenarios, the test runner will run
-ONLY those scenarios, skipping all other scenarios:
+**ONLY** those scenarios, skipping all other scenarios:
```go
scenarios := []TestScenario{
@@ -1249,137 +1318,35 @@ ONLY those scenarios, skipping all other scenarios:
# Test for an Expected Type
-You can test that some value is of an expected type using the `ExpectType` function.
-
-This function returns the value as the expected type and `true` if the test passes;
-otherwise the zero-value of the expected type is returned, with `false`.
-
-A common pattern when this type of test is useful is to assert the type of some
-value and then perform additional tests on that value appropriate to the type:
+There are two ways to test that some value is of an expected type:
```go
-func TestDoSomething(t *testing.T) {
- With(t)
+ // using the BeOfType matcher
+ Expect(value).Should(BeOfType[ExpectedType]())
- // ACT
- result := DoSomething()
-
- // ASSERT
- if cust, ok := ExpectType[Customer](result); ok {
- // further assertions on cust (type: Customer) ...
- }
-}
+ // using the expect.Type function
+ v, ok := expect.Type[ExpectedType](value)
```
-This test can only be used to test that a value is of a type that can be expressed
-through the type parameter on the generic function.
+The `BeOfType[T]()` matcher is used with the `Should()` or `ShouldNot()` methods
+to test that the subject is of the expected type `T`. When testing using this
+matcher, the test will fail if the subject is not of the expected type.
-For example, the following test will fail as an invalid test:
+If you need to use the value as the expected type, you can use the
+`expect.Type[T](value)` function to test that the value is of the expected type.
+This will return the value as the expected type `T` and a boolean indicating whether
+the test passed.
-```go
- type Counter interface {
- Count() int
- }
- ExpectType[Counter](result) // INVALID TEST: cannot be used to test for interfaces
-```
-
-------
-
-# Testing Test Helpers
-
-If you write your own test helper functions you should of course test them. This
-is problematic when using `*testing.T` since when your test helper fails the test
-that is testing your helper will also fail.
-
-`blugnu/test` addresses this by providing a `TestHelper()` function that runs
-your test helper in a separate test runner, capturing the outcome and any report.
-This allows the test helper to fail without affecting the outcome of the test that
-is testing it, allowing that test to then assert the expected outcome.
-
-The `TestHelper()` function accepts a function that executes your test helper,
-and returns a `test.R` value that contains information about the outcome of the test.
-
-The `R` type provides methods to test the outcome of the test, primarily this will
-involve testing the helper (correctly) failed and produced an expected report. This
-is accomplished by calling the `R.Expect()` method with the expected test report;
-when an expected report is passed, the test is implicitly expected to fail, so the
-`R.Expect()` method will also test that the outcome is `TestFailed`:
-
-```go
-func TestMyTestHelper(t *testing.T) {
- With(t)
+If the test fails, the boolean will be `false` and the value will be the zero-value
+of the expected type, `T`.
- result := TestHelper(func() {
- MyTestHelper() // this is the test helper being tested
- })
-
- result.Expect(
- "expected failure message", // the expected failure message
- )
-}
-```
-
-To test that the test helper passed, you can call the `R.Expect()` method
-with the expected outcome:
+If the failure of the test should cause the current test to fail immediately, you
+can use the `require.Type[T](value)` function which will short-circuit the current
+test if the value is not of the expected type, returning the value as the expected
+type `T` if the test passes.
```go
-func TestMyTestHelper(t *testing.T) {
- With(t)
-
- result := TestHelper(func() {
- MyTestHelper() // this is the test helper being tested
- })
-
- result.Expect(TestPassed) // the test helper is expected to pass
-}
-```
-
-## Testing a Test Helper with Scenarios
-
-A test runner is provided to test a test helper in a variety of scenarios.
-The runner defines a `HelperScenario` type for each scenario, which
-contains the following fields:
-
-- `Scenario`: a name for the scenario (scenario); each scenario is run in a subtest
- using this name;
-- `Act`: a function that contains the test code for the scenario; this function has
- the signature `func()`;
-- `Assert`: a function that tests the test outcome; this function has the signature
- `func(*R)` where `R` is the result of the test scenario.
-- `Skip`: a boolean to indicate whether the scenario should be skipped; defaults to `false`
-- `Debug`: a boolean to indicate whether the scenario is a debug scenario; defaults to `false`
-
-> :bulb: If the `Debug` field is set to `true` on any scenario(s), the test runner
-> will run only those scenarios, and these will be run even if `Skip` is also true.
-
-For scenarios where the helper is expected to pass, the `Assert` function
-is optional; if the `Assert` function is nil, the test runner will assert
-that the test passed without any errors or failures.
-
-The `HelperTests()` test runner accepts a variadic list of `HelperScenario` values,
-where each scenario is a test case to be run against your test helper:
-
-```go
-func TestMyTestHelper(t *testing.T) {
- With(t)
-
- Run(HelperTests([]test.HelperScenario{
- {Scenario: "provided with an empty string",
- Act: func() {
- MyTestHelper("")
- },
- Assert: func(r *test.R) {
- r.ExpectInvalid("input string cannot be empty")
- },
- },
- {Scenario: "this is expected to pass",
- Act: func() {
- MyTestHelper("some input")
- },
- },
- }...))
-}
+ v := require.Type[ExpectedType](value) // short-circuits the current test if value is not of type ExpectedType
```
-> :bulb: In the example above, the scenarios are provided as a slice literal
-> using the `...` operator to expand the slice into a variadic argument list.
+Since the current test is halted on failure, an indicator boolean is not returned.
diff --git a/bool.go b/bool.go
deleted file mode 100644
index cb59784..0000000
--- a/bool.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package test
-
-import "github.com/blugnu/test/matchers/bools"
-
-// BeFalse returns a matcher that will fail if the matched value is not false.
-//
-// # Supported Options
-//
-// opt.FailureReport(...) // a function returning a custom failure report
-// // in the event that the test fails
-func BeFalse() bools.BooleanMatcher {
- return bools.BooleanMatcher{Expected: false}
-}
-
-// BeTrue returns a matcher that will fail if the matched value is not true.
-//
-// # Supported Options
-//
-// opt.FailureReport(...) // a function returning a custom failure report
-// // in the event that the test fails
-func BeTrue() bools.BooleanMatcher {
- return bools.BooleanMatcher{Expected: true}
-}
-
-// ExpectFalse fails a test if a specified bool is not false. An optional
-// name (string) may be specified to be included in the test report in the
-// event of failure.
-//
-// This test is a convenience for these equivalent alternatives:
-//
-// Expect(got).To(Equal(false))
-// Expect(got).To(BeFalse())
-//
-// # Supported Options
-//
-// string // a name for the value, for use in any test
-// // failure report
-//
-// opt.FailureReport(func) // a function returning a custom failure report
-// // in the event that the test fails
-func ExpectFalse[T ~bool](got T, opts ...any) {
- GetT().Helper()
- Expect(bool(got), opts...).To(BeFalse(), opts...)
-}
-
-// ExpectTrue fails a test if a specified bool is not true. An optional
-// name (string) may be specified to be included in the test report in the
-// event of failure.
-//
-// This test is a convenience for these equivalent alternatives:
-//
-// Expect(got).To(Equal(true))
-// Expect(got).To(BeTrue())
-//
-// # Supported Options
-//
-// string // a name for the value, for use in any test
-// // failure report
-//
-// opt.FailureReport(func) // a function returning a custom failure report
-// // in the event that the test fails
-func ExpectTrue[T ~bool](got T, opts ...any) {
- GetT().Helper()
- Expect(bool(got), opts...).To(BeTrue(), opts...)
-}
diff --git a/context_examples_test.go b/context_examples_test.go
deleted file mode 100644
index 4b6ccfe..0000000
--- a/context_examples_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package test //nolint: testpackage // examples for test package reference the test package in expected output
-
-import (
- "context"
-
- "github.com/blugnu/test/test"
-)
-
-func ExampleHaveContextKey() {
- test.Example()
-
- type key int
- ctx := context.WithValue(context.Background(), key(57), "varieties")
-
- // these tests will pass
- Expect(ctx).To(HaveContextKey(key(57)))
- Expect(ctx).ToNot(HaveContextKey(key(58)))
-
- // this test will fail
- Expect(ctx).To(HaveContextKey(key(58)))
-
- // Output:
- // expected key: test.key(58)
- // key not present in context
-}
-
-func ExampleHaveContextValue() {
- // this is needed to make the example work; this would be usually
- // be `With(t)` where `t` is the *testing.T
- test.Example()
-
- type key int
- ctx := context.WithValue(context.Background(), key(57), "varieties")
-
- // these tests will pass
- Expect(ctx).To(HaveContextValue(key(57), "varieties"))
- Expect(ctx).ToNot(HaveContextValue(key(56), "varieties"))
- Expect(ctx).ToNot(HaveContextValue(key(57), "flavours"))
-
- // this test will fail
- Expect(ctx).To(HaveContextValue(key(57), "flavours"))
-
- // Output:
- // context value: test.key(57)
- // expected: "flavours"
- // got : "varieties"
-}
diff --git a/context_test.go b/context_test.go
deleted file mode 100644
index e0ca785..0000000
--- a/context_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package test_test
-
-import (
- "context"
- "testing"
-
- . "github.com/blugnu/test"
- "github.com/blugnu/test/opt"
-)
-
-func TestContext(t *testing.T) {
- With(t)
-
- type key string
- ctx := context.WithValue(context.Background(), key("key"), "value")
-
- Run(
- Test("ContextKey", func() {
- Run(HelperTests([]HelperScenario{
- {Scenario: "expected key present",
- Act: func() {
- Expect(ctx).To(HaveContextKey(key("key")))
- },
- },
- {Scenario: "expected key not present",
- Act: func() {
- Expect(ctx).To(HaveContextKey(key("other-key")))
- },
- Assert: func(result *R) {
- result.Expect(TestFailed, opt.IgnoreReport(true))
- },
- },
- }...))
- }))
-
- Run(Test("ContextValue", func() {
- Run(HelperTests([]HelperScenario{
- {Scenario: "expected value present",
- Act: func() {
- Expect(ctx).To(HaveContextValue(key("key"), "value"))
- },
- },
- {Scenario: "expected value not present",
- Act: func() {
- Expect(ctx).To(HaveContextValue(key("other-key"), "value"))
- },
- Assert: func(result *R) {
- result.Expect(TestFailed, opt.IgnoreReport(true))
- },
- },
- {Scenario: "expected value present but different",
- Act: func() {
- Expect(ctx).To(HaveContextValue(key("key"), "other value"))
- },
- Assert: func(result *R) {
- result.Expect(TestFailed, opt.IgnoreReport(true))
- },
- },
- }...))
- }),
- )
-}
diff --git a/emptiness_test.go b/emptiness_test.go
deleted file mode 100644
index c0a89be..0000000
--- a/emptiness_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package test_test
-
-import (
- "testing"
-
- . "github.com/blugnu/test"
-)
-
-func TestEmptiness(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "empty slice is empty",
- Act: func() {
- Expect([]int{}).Should(BeEmpty())
- },
- },
- {Scenario: "nil slice is empty",
- Act: func() {
- var nilSlice []int
- Expect(nilSlice).Should(BeEmptyOrNil())
- },
- },
- {Scenario: "nil slice has len 0",
- Act: func() {
- var nilSlice []int
- Expect(nilSlice).Should(HaveLen(0))
- },
- },
- }...))
-}
diff --git a/error.go b/error.go
deleted file mode 100644
index b7320d2..0000000
--- a/error.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package test
-
-import (
- "fmt"
-
- "github.com/blugnu/test/opt"
-)
-
-// Error explicitly and unconditionally fails the current test
-// with the given message.
-//
-// This should not be confused with the `test.Error` function
-// used to report an error condition in a test helper (from the
-// blugnu/test/test package).
-func Error(msg string) {
- T().Helper()
- Expect(false).To(BeTrue(), opt.OnFailure(msg))
-}
-
-// Errorf explicitly and unconditionally fails the current test
-// with the formatted message.
-//
-// This should not be confused with the `test.Error` function
-// used to report an error condition in a test helper (from the
-// blugnu/test/test package).
-func Errorf(s string, args ...any) {
- T().Helper()
- Expect(false).To(BeTrue(), opt.OnFailure(fmt.Sprintf(s, args...)))
-}
diff --git a/error_test.go b/error_test.go
deleted file mode 100644
index 4ac8fa3..0000000
--- a/error_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package test_test
-
-import (
- "testing"
-
- . "github.com/blugnu/test"
-)
-
-func TestError(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "Error fails the test with the given message",
- Act: func() {
- Error("test error message")
- },
- Assert: func(result *R) {
- result.Expect("test error message")
- },
- },
- {Scenario: "Errorf fails the test with a formatted message",
- Act: func() {
- Errorf("test error message %d", 42)
- },
- Assert: func(result *R) {
- result.Expect("test error message 42")
- },
- },
- }...))
-}
diff --git a/errors.go b/errors.go
index 7285d50..b19bb06 100644
--- a/errors.go
+++ b/errors.go
@@ -4,22 +4,56 @@ import (
"errors"
)
+// general errors
var (
- // general errors
- ErrInvalidArgument = errors.New("invalid argument")
+ // ErrInvalidArgument indicates that an argument provided to a function or method is invalid.
+ ErrInvalidArgument = errors.New("invalid argument")
+
+ // ErrInvalidOperation indicates that an attempted operation is invalid in the current context.
ErrInvalidOperation = errors.New("invalid operation")
+)
- // mock and fake errors
+// mock and fake errors
+var (
+ // ErrExpectationsNotMet indicates that the expectations set on a mock or fake
+ // were not met.
ErrExpectationsNotMet = errors.New("expectations not met")
- ErrExpectedArgs = errors.New("arguments were expected but not recorded")
- ErrNoResultForArgs = errors.New("no result for arguments")
- ErrUnexpectedArgs = errors.New("the arguments recorded did not match those expected")
- ErrUnexpectedCall = errors.New("unexpected call")
- ErrResultNotUsed = errors.New("result not used")
-
- // recording errors
- ErrRecordingFailed = errors.New("recording failed")
- ErrRecordingStdout = errors.New("error recording stdout")
- ErrRecordingStderr = errors.New("error recording stderr")
- ErrRecordingUnableToRedirectLogger = errors.New("unable to redirect logger output")
+
+ // ErrExpectedArgs indicates that arguments were expected but not recorded.
+ ErrExpectedArgs = errors.New("arguments were expected but not recorded")
+
+ // ErrNoResultForArgs indicates that no result was found for the given arguments.
+ ErrNoResultForArgs = errors.New("no result for arguments")
+
+ // ErrUnexpectedArgs indicates that the arguments recorded did not match those expected.
+ ErrUnexpectedArgs = errors.New("the arguments recorded did not match those expected")
+
+ // ErrUnexpectedCall indicates that a call was made that was not expected.
+ ErrUnexpectedCall = errors.New("unexpected call")
+
+ // ErrResultNotUsed indicates that a result was provided but not used.
+ ErrResultNotUsed = errors.New("result not used")
+)
+
+// recording errors
+var (
+ // ErrRecordingFailed indicates that an error occurred while trying to
+ // record output.
+ ErrRecordingFailed = errors.New("recording failed")
+
+ // ErrRecordingStdout indicates that an error occurred while trying to
+ // record stdout.
+ ErrRecordingStdout = errors.New("error recording stdout")
+
+ // ErrRecordingStderr indicates that an error occurred while trying to
+ // record stderr.
+ ErrRecordingStderr = errors.New("error recording stderr")
+
+ // ErrFailedToRedirectLogger indicates that an error occurred while trying
+ // to redirect logger output.
+ ErrFailedToRedirectLogger = errors.New("failed to redirect logger output")
+
+ // Deprecated: this error is deprecated and will be removed in a future version;
+ // use [ErrFailedToRedirectLogger] instead.
+ ErrRecordingUnableToRedirectLogger = ErrFailedToRedirectLogger
)
diff --git a/expect/bool.go b/expect/bool.go
new file mode 100644
index 0000000..4597482
--- /dev/null
+++ b/expect/bool.go
@@ -0,0 +1,45 @@
+package expect
+
+import "github.com/blugnu/test"
+
+// False fails a test if a specified bool is not false. An optional
+// name (string) may be specified to be included in the test report in the
+// event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Expect(got).To(Equal(false))
+// Expect(got).To(BeFalse())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func False[T ~bool](got T, opts ...any) {
+ test.T().Helper()
+ test.Expect(bool(got), opts...).To(test.BeFalse(), opts...)
+}
+
+// True fails a test if a specified bool is not true. An optional
+// name (string) may be specified to be included in the test report in the
+// event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Expect(got).To(Equal(true))
+// Expect(got).To(BeTrue())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func True[T ~bool](got T, opts ...any) {
+ test.T().Helper()
+ test.Expect(bool(got), opts...).To(test.BeTrue(), opts...)
+}
diff --git a/expect/bool_test.go b/expect/bool_test.go
new file mode 100644
index 0000000..23d8e21
--- /dev/null
+++ b/expect/bool_test.go
@@ -0,0 +1,36 @@
+package expect_test
+
+import (
+ "github.com/blugnu/test/expect"
+ "github.com/blugnu/test/test"
+)
+
+func ExampleFalse() {
+ test.Example()
+
+ var a = 1
+
+ expect.False(a == 1) // will fail
+ expect.False(a-1+1 == 1) // will also fail
+ expect.False(a == 2) // will pass
+
+ // Output:
+ // expected false
+ //
+ // expected false
+}
+
+func ExampleTrue() {
+ test.Example()
+
+ var a = 1
+
+ expect.True(a == 1) // will pass
+ expect.True(a == 2) // will fail
+ expect.True(a == 3) // will also fail
+
+ // Output:
+ // expected true
+ //
+ // expected true
+}
diff --git a/expect/error.go b/expect/error.go
new file mode 100644
index 0000000..4fd250e
--- /dev/null
+++ b/expect/error.go
@@ -0,0 +1,55 @@
+package expect
+
+import (
+ "errors"
+
+ "github.com/blugnu/test/internal"
+ "github.com/blugnu/test/opt"
+
+ "github.com/blugnu/test"
+)
+
+// Error tests that an error is of an expected type E, using [errors.As].
+// An ok indicator is returned indicating whether the test passed (true)
+// or failed (false). If the test passes, the error is returned as a
+// value of type E, otherwise the zero value of E is returned and should
+// be ignored.
+//
+// If a test has no use for the returned E value, consider using the
+// [BeError] matcher with [Expect].
+//
+// If the test can only continue when the error is of the expected type,
+// consider using the [require.Error] function to halt test execution
+// when the error is not of the expected type.
+//
+// Expect(err).Should(BeError[E]()) // if the E value is not needed
+//
+// e := require.Error[E](err) // if the E value is necessary to the
+// // continuation of the test
+func Error[E error](got error, opts ...any) (E, bool) {
+ test.T().Helper()
+
+ var target E
+ if got != nil && errors.As(got, &target) {
+ return target, true
+ }
+
+ if _, ok := opt.Get[opt.FailureReport](opts); !ok {
+ opts = append(opts,
+ opt.FailureReport(func(...any) []string {
+ expectedType := internal.TypeName[E]()
+ if got == nil {
+ return []string{
+ "expected error: " + expectedType,
+ "got : nil",
+ }
+ }
+ return []string{"expected error: " + expectedType}
+ }),
+ )
+ }
+
+ test.Fail(opts...)
+
+ return target, false
+}
diff --git a/expect/error_test.go b/expect/error_test.go
new file mode 100644
index 0000000..62a202b
--- /dev/null
+++ b/expect/error_test.go
@@ -0,0 +1,72 @@
+package expect_test
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/expect"
+)
+
+type MyError struct {
+ msg string
+}
+
+func (e MyError) Error() string {
+ return e.msg
+}
+
+func TestError(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "error is of expected type",
+ Act: func() {
+ err := fmt.Errorf("an error occurred: %w", MyError{"invalid argument"})
+ e, ok := expect.Error[MyError](err)
+
+ Expect(ok).To(BeTrue())
+ Expect(e).To(Equal(MyError{"invalid argument"}))
+ },
+ },
+
+ {Scenario: "error is not of expected type",
+ Act: func() {
+ err := errors.New("some other error")
+ e, ok := expect.Error[MyError](err)
+ Expect(ok).To(BeFalse())
+ Expect(e).To(Equal(MyError{})) // zero value of MyError
+
+ Error("subsequent failures should appear in report")
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected error: expect_test.MyError",
+ )
+ result.Expect(
+ "subsequent failures should appear in report",
+ )
+ },
+ },
+
+ {Scenario: "nil should not be identified as any error",
+ Act: func() {
+ e, ok := expect.Error[error](error(nil))
+ Expect(ok).To(BeFalse())
+ Expect(e).IsNil()
+
+ Error("subsequent failures should appear in report")
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected error: error",
+ "got : nil",
+ )
+ result.Expect(
+ "subsequent failures should appear in report",
+ )
+ },
+ },
+ }...))
+}
diff --git a/expect/nil.go b/expect/nil.go
new file mode 100644
index 0000000..09d5cae
--- /dev/null
+++ b/expect/nil.go
@@ -0,0 +1,45 @@
+package expect
+
+import "github.com/blugnu/test"
+
+// Nil fails a test if a specified value is not nil. An optional
+// name (string) may be specified to be included in the test report in the
+// event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Expect(got).Is(nil)
+// Expect(got).Should(BeNil())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func Nil(got any, opts ...any) {
+ test.T().Helper()
+ test.Expect(got, opts...).Should(test.BeNil(), opts...)
+}
+
+// NotNil fails a test if a specified value is nil. An optional
+// name (string) may be specified to be included in the test report in the
+// event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Expect(got).IsNot(nil)
+// Expect(got).ShouldNot(BeNil())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func NotNil(got any, opts ...any) {
+ test.T().Helper()
+ test.Expect(got, opts...).ShouldNot(test.BeNil(), opts...)
+}
diff --git a/expect/nil_test.go b/expect/nil_test.go
new file mode 100644
index 0000000..f88a398
--- /dev/null
+++ b/expect/nil_test.go
@@ -0,0 +1,39 @@
+package expect_test
+
+import (
+ "errors"
+
+ "github.com/blugnu/test/expect"
+ "github.com/blugnu/test/test"
+)
+
+func ExampleNil() {
+ test.Example()
+
+ err := errors.New("an error")
+ expect.Nil(nil)
+ expect.Nil(err) // this test will fail
+
+ // this test will fail as invalid (int is not nilable)
+ expect.Nil(42)
+
+ // Output:
+ // expected nil, got error: an error
+ // <== INVALID TEST
+ // nilness.Matcher: values of type 'int' are not nilable
+}
+
+func ExampleNotNil() {
+ test.Example()
+
+ err := errors.New("an error")
+ expect.NotNil(nil) // this test will fail
+ expect.NotNil(err) // this test is valid and will pass (error is nilable)
+
+ // this test is valid and will also pass. Non-nilable types are
+ // inherently "not nil"; the test is unnecessary, but not invalid
+ expect.NotNil(42)
+
+ // Output:
+ // expected not nil
+}
diff --git a/expect/pkgdoc.go b/expect/pkgdoc.go
new file mode 100644
index 0000000..1f73ce2
--- /dev/null
+++ b/expect/pkgdoc.go
@@ -0,0 +1,6 @@
+// Package expect provides conclusory type assertion functions that operate on a
+// subject value. If the assertion fails, the test is failed though further expectations
+// in the test will be evaluated.
+package expect
+
+// this file is provided for package documentation purposes and is intentionally left blank
diff --git a/expect/type.go b/expect/type.go
new file mode 100644
index 0000000..02be3f7
--- /dev/null
+++ b/expect/type.go
@@ -0,0 +1,51 @@
+package expect
+
+import (
+ "github.com/blugnu/test"
+ "github.com/blugnu/test/internal"
+ "github.com/blugnu/test/opt"
+)
+
+// Type tests that a value is of an expected type T. If the test passes,
+// the value is returned as that type, with true. If the test fails the zero
+// value of the specified type is returned, with false.
+//
+// If a test has no use for the returned T value, consider using the
+// [BeOfType] matcher with [Expect].
+//
+// If the test can only continue when the value is of the expected type,
+// consider using the [require.Type] function to halt test execution if
+// when the value is not of the expected type.
+//
+// Expect(err).Should(BeOfType[T]()) // if the T value is not needed
+//
+// v := require.Type[T](got) // if the T value is necessary to the
+// // continuation of the test
+func Type[T any](got any, opts ...any) (T, bool) {
+ test.T().Helper()
+
+ result, ok := got.(T)
+ if ok {
+ return result, true
+ }
+
+ expectedType := internal.TypeName[T]()
+ gotType := internal.TypeName(got)
+ if got == nil {
+ gotType = "nil"
+ }
+
+ opts = append(opts,
+ opt.FailureReport(func(...any) []string {
+ return []string{
+ "expected type: " + expectedType,
+ "got : " + gotType,
+ }
+ }),
+ )
+
+ test.Expect(gotType, opts...).To(test.Equal(expectedType), opts...)
+
+ var zero T
+ return zero, false
+}
diff --git a/expect/type_test.go b/expect/type_test.go
new file mode 100644
index 0000000..9613f1f
--- /dev/null
+++ b/expect/type_test.go
@@ -0,0 +1,128 @@
+package expect_test
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ . "github.com/blugnu/test"
+
+ "github.com/blugnu/test/expect"
+ "github.com/blugnu/test/test"
+)
+
+type Counter struct{}
+
+func (Counter) Count() int {
+ return 42
+}
+
+func TestExpectType(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "expecting int got int",
+ Act: func() {
+ result, ok := expect.Type[int](1)
+ Expect(result).To(Equal(1))
+ Expect(ok).To(BeTrue())
+ },
+ },
+ {Scenario: "expecting int got string",
+ Act: func() {
+ result, ok := expect.Type[int]("string")
+ Expect(result).To(Equal(0))
+ Expect(ok).To(BeFalse())
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected type: int",
+ "got : string",
+ )
+ },
+ },
+ {Scenario: "expecting named int got bool",
+ Act: func() {
+ result, ok := expect.Type[int](false, "named value")
+ Expect(result).To(Equal(0))
+ Expect(ok).To(BeFalse())
+ },
+ Assert: func(result *R) {
+ result.Expect([]string{
+ "named value:",
+ " expected type: int",
+ " got : bool",
+ })
+ },
+ },
+ {Scenario: "expecting error, got nil",
+ Act: func() {
+ result, ok := expect.Type[error](nil)
+ Expect(ok).To(BeFalse())
+ Expect(result).Should(BeNil())
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected type: error",
+ "got : nil",
+ )
+ },
+ },
+ {Scenario: "expecting error, got error",
+ Act: func() {
+ result, ok := expect.Type[error](errors.New("an error occurred"))
+ Expect(ok).To(BeTrue())
+ Expect(result.Error()).To(Equal("an error occurred"))
+ },
+ },
+ {Scenario: "expecting interface, got struct implementing interface",
+ Act: func() {
+ result, ok := expect.Type[interface{ Count() int }](Counter{})
+ Expect(ok).To(BeTrue())
+ Expect(result).IsNotNil()
+ },
+ },
+ {Scenario: "expecting interface, got struct not implementing interface",
+ Act: func() {
+ result, ok := expect.Type[error](Counter{})
+ Expect(ok).To(BeFalse())
+ Expect(result).IsNil()
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected type: error",
+ "got : expect_test.Counter",
+ )
+ },
+ },
+ }...))
+}
+
+func ExampleType() {
+ test.Example()
+
+ // expect.Type returns the value as the expected type and true if the
+ // value is of that type
+ var got any = 1 / 2.0
+
+ result, ok := expect.Type[float64](got)
+
+ fmt.Printf("ok is %v\n", ok)
+ fmt.Printf("result: type is: %T\n", result)
+ fmt.Printf("result: value is: %v\n", result)
+
+ // expect.Type returns the zero value of the expected type and false if the
+ // value is not of that type (the return values can be ignored if the
+ // test is only concerned with checking the type)
+ got = "1 / 2.0"
+
+ _, _ = expect.Type[float64](got)
+
+ //Output:
+ // ok is true
+ // result: type is: float64
+ // result: value is: 0.5
+ //
+ // expected type: float64
+ // got : string
+}
diff --git a/expect_didOccur.go b/expect_didOccur.go
deleted file mode 100644
index c90b0b5..0000000
--- a/expect_didOccur.go
+++ /dev/null
@@ -1,169 +0,0 @@
-package test
-
-import (
- "fmt"
- "runtime"
-
- "github.com/blugnu/test/matchers/panics"
- "github.com/blugnu/test/opt"
- "github.com/blugnu/test/test"
-)
-
-// DidOccur is used to check whether an expected panic or error occurred.
-//
-// # Testing for Panics
-//
-// Use the Panic(r) function to create an expectation that a value r will
-// be recovered from a panic. The call to DidOccur() must be deferred:
-//
-// defer Expect(Panic(r)).DidOccur(opts...)
-//
-// If the value r is an error the test will pass only if a panic occurs
-// and an error is recovered from the panic that satisfies errors.Is(r).
-//
-// If the expected recovered value is not an error, the test passes if
-// the recovered value is equal to the expected value, based on comparison
-// using reflect.DeepEqual or a comparison function.
-//
-// # Supported Options
-//
-// func(a, b any) bool // a function to compare the values, overriding
-// // the use of reflect.DeepEqual.
-//
-// # Testing for Errors
-//
-// To test for an error, use the error value as the expected value.
-// The test will pass if the error is not nil:
-//
-// Expect(err).DidOccur()
-//
-// This is equivalent to:
-//
-// Expect(err).IsNotNil()
-//
-// NOTE: this approach to testing for errors is not recommended since
-// the test will pass if any error occurred which may not be the error
-// that was expected. This may be acceptable in very simple cases but
-// it is usually better to test for a specific error using:
-//
-// Expect(err).Is(expectedError)
-func (e expectation[T]) DidOccur(opts ...any) {
- e.t.Helper()
-
- switch v := any(e.subject).(type) {
- case panics.Expected:
- match := &panics.MatchRecovered{R: recover()}
-
- if match.R != nil {
- const bufsize = 65536
- stk := make([]byte, bufsize)
- n := runtime.Stack(stk, false)
- match.Stack = stk[:n-1]
- }
-
- if !match.Match(v, opts...) {
- e.fail(match, opts...)
- }
-
- case error:
- if v != nil {
- return
- }
-
- case nil:
- e.err("expected error, got nil")
-
- default:
- test.Invalid("test.DidOccur: may only be used with Panic() or error values")
- }
-}
-
-// DidNotOccur is used to ensure that a panic or error did not occur.
-//
-// # Testing for Panics
-//
-// Use the Panic() function to create an expectation for a Panic with
-// an unspecified recovered value. The call to DidNotOccur() must be
-// deferred:
-//
-// defer Expect(Panic()).DidNotOccur(opts...)
-//
-// The test will pass only if the function scope terminates without
-// a panic having occurred.
-//
-// # Testing for Errors
-//
-// To test for an error, use the error value as the expected value.
-// The test will pass if the error is nil:
-//
-// Expect(err).DidNotOccur()
-//
-// This is equivalent to:
-//
-// Expect(err).IsNil()
-func (e expectation[T]) DidNotOccur(opts ...any) {
- e.t.Helper()
-
- switch expected := any(e.subject).(type) {
- case panics.Expected:
- // for a "DidNotOccur" test, things are more complicated:
-
- // first let's grab any recoverable value and create a
- // matcher which we'll use later...
- matcher := &panics.MatchRecovered{R: recover()}
-
- // first, using DidNotOccur with Panic(nil) is invalid since it
- // is likely to cause confusion
- if expected.R == opt.NoPanicExpected(true) {
- test.Invalid("DidNotOccur: may not be used with Panic(nil); did you mean NilPanic()?")
- }
-
- // if we expect Panic(x) did NOT occur, but Panic(y) DID occur,
- // then although the expectation was met, the UNexpected panic
- // should still be reported as a test failure.
- //
- // so we use the MatchesPanic matcher to determine whether
- // the recovered value matches the expected value...
- recoveredExpectedValue := matcher.Match(expected, opts...)
-
- // if the recovered value matches the expected value, then
- // the test has failed since this panic should not have occurred...
- if recoveredExpectedValue && expected.R != nil {
- // we add the ToNotMatch(true) option to indicate that the
- // expectation was that the panic should not have occurred
- e.fail(matcher, append(opts, opt.ToNotMatch(true))...)
- return
- }
-
- // but we're not done yet...
- //
- // the recovered value did not match the expected value, and if that
- // recovered value is not nil, then we have an unexpected panic to report...
- if matcher.R != nil {
- // the existing matcher has already been used to test the recovered
- // value against an expected value where-as we now need to report an
- // unexpected panic (i.e. expected nil)
- //
- // so we create a new panic matcher, matching against an expected R:nil
- // and use THAT to report the failure
- matcher := &panics.MatchRecovered{R: matcher.R}
- matcher.Match(panics.Expected{R: nil})
- e.fail(matcher, opts...)
- }
-
- case error:
- opts = append(opts, opt.FailureReport(func(opts ...any) []string {
- return []string{
- "expected:
",
- fmt.Sprintf("got : %T(%v)", expected, opt.ValueAsString(expected, opts...)),
- }
- }))
- Expect(expected).IsNil(opts...)
-
- case nil:
- return
-
- default:
- test.Invalid("test.DidNotOccur: may only be used with Panic() or error values")
- }
-}
diff --git a/expect_didOccur_test.go b/expect_didOccur_test.go
deleted file mode 100644
index 0017120..0000000
--- a/expect_didOccur_test.go
+++ /dev/null
@@ -1,136 +0,0 @@
-package test_test
-
-import (
- "errors"
- "testing"
-
- . "github.com/blugnu/test"
- "github.com/blugnu/test/opt"
-)
-
-func TestExpect_DidOccur(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- // panics
- {Scenario: "panic was expected and occurred",
- Act: func() {
- defer Expect(Panic(ErrInvalidArgument)).DidOccur()
- panic(ErrInvalidArgument)
- },
- },
- {Scenario: "panic was expected and did not occur",
- Act: func() {
- defer Expect(Panic(ErrInvalidArgument)).DidOccur()
- },
- Assert: func(result *R) {
- result.Expect(TestFailed, opt.IgnoreReport(true))
- },
- },
-
- // errors
- {Scenario: "error was expected and occurred",
- Act: func() { Expect(errors.New("error")).DidOccur() },
- },
- {Scenario: "error was expected and did not occur",
- Act: func() {
- var err error
- Expect(err).DidOccur()
- },
- Assert: func(result *R) {
- result.Expect("expected error, got nil")
- },
- },
-
- // unsupported types
- {Scenario: "not an error or panic",
- Act: func() {
- Expect(42).DidOccur()
- },
- Assert: func(result *R) {
- result.ExpectInvalid(
- "test.DidOccur: may only be used with Panic() or error values",
- )
- },
- },
- }...))
-}
-
-func TestExpect_DidNotOccur(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- // panics
- {Scenario: "panic(), no panic occurs",
- Act: func() {
- defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
- },
- },
- {Scenario: "panic(r), no panic occurs",
- Act: func() {
- defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
- },
- },
- {Scenario: "panic(nil)",
- Act: func() {
- defer Expect(Panic(nil)).DidNotOccur()
- },
- Assert: func(result *R) {
- result.ExpectInvalid(
- "DidNotOccur: may not be used with Panic(nil); did you mean NilPanic()?",
- )
- },
- },
- {Scenario: "panic was expected to not occur",
- Act: func() {
- defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
- panic(ErrInvalidArgument)
- },
- Assert: func(result *R) {
- result.Expect(
- "expected: panic with *errors.errorString(invalid argument): should not have occurred",
- )
- },
- },
- {Scenario: "panic was not expected and occurred",
- Act: func() {
- defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
- panic(ErrInvalidOperation)
- },
- Assert: func(result *R) {
- result.Expect(
- "unexpected panic:",
- " recovered: *errors.errorString(invalid operation)",
- )
- },
- },
-
- // errors
- {Scenario: "error was not expected and did not occur",
- Act: func() { var err error = nil; Expect(err).DidNotOccur() },
- },
- {Scenario: "error was not expected and occurred",
- Act: func() {
- Expect(errors.New("error")).DidNotOccur()
- },
- Assert: func(result *R) {
- result.Expect(
- "expected: ",
- `got : *errors.errorString(error)`,
- )
- },
- },
-
- // unsupported types
- {Scenario: "not an error or panic",
- Act: func() {
- Expect(42).DidNotOccur()
- },
- Assert: func(result *R) {
- result.ExpectInvalid(
- "test.DidNotOccur: may only be used with Panic() or error values",
- )
- },
- },
- }...))
-}
diff --git a/expect.go b/expectation.go
similarity index 85%
rename from expect.go
rename to expectation.go
index b15846b..79871c6 100644
--- a/expect.go
+++ b/expectation.go
@@ -4,52 +4,14 @@ import (
"errors"
"fmt"
"reflect"
+ "slices"
+ "strings"
"github.com/blugnu/test/matchers/matcher"
"github.com/blugnu/test/opt"
"github.com/blugnu/test/test"
)
-// AnyMatcher is the interface implemented by matchers that can test
-// any type of value. It is used to apply matchers to expectations
-// that are not type-specific.
-//
-// It is preferable to use the Matcher[T] interface for type-safe
-// expectations; AnyMatcher is provided for situations where the
-// compatible types for a matcher cannot be enforced at compile-time.
-//
-// When implementing an AnyMatcher, it is important to ensure that
-// the matcher fails a test if it is not used correctly, i.e. if the
-// matcher is not compatible with the type of the value being tested.
-//
-// An AnyMatcher must be used with the Expect().Should() matching
-// function; they may also be used with Expect(got).To() where the got
-// value is of type `any`, though this is not recommended.
-type AnyMatcher interface {
- Match(got any, opts ...any) bool
-}
-
-// Matcher[T] is the interface implemented by matchers that can test
-// a value of type T. It is used to apply matchers to expectations
-// that are type-specific and type-safe.
-//
-// Note that not all type-safe matchers implement a generic interface;
-// a matcher that implements Match(got X, opts ...any) bool, where X is
-// a formal, literal type (i.e. not generic) is also a type-safe matcher.
-//
-// Matcher[T] describes the general form of a type-safe matcher.
-//
-// Generic matchers are able to leverage the type system to ensure
-// that the matcher is used correctly with a variety of types, i.e. where
-// the type of the Expect() value satisfies the constraints of the matcher
-// type, T. The equals.Matcher[T comparable] uses this approach, for
-// example, to ensure that the value being tested is comparable
-// with the expected value (since the matcher uses the == operator for
-// equality testing).
-type Matcher[T any] interface {
- Match(got T, opts ...any) bool
-}
-
// Expect creates an expectation for the given value. The value
// may be of any type.
//
@@ -142,18 +104,15 @@ func (e *expectation[T]) err(msg any) {
errorFn(msg)
case []string:
- var rpt string
- var indent string
-
+ rpt := slices.Clone(msg)
if e.name != "" {
- rpt = "\n" + e.name + ":"
- indent = " "
+ for i := range rpt {
+ rpt[i] = " " + rpt[i]
+ }
+ rpt = append([]string{e.name + ":"}, rpt...)
}
- for _, s := range msg {
- rpt += "\n" + indent + s
- }
- errorFn(rpt)
+ errorFn("\n" + strings.Join(rpt, "\n"))
// errMsg returns a string or []string, so we can safely use a type
// switch here to handle both cases without a default case
@@ -309,6 +268,141 @@ func (e *expectation[T]) getTestFailureReporter(opts ...any) any {
return nil
}
+// Is tests the value of the expectation against some expected
+// value.
+//
+// The function behaves differently depending on the values and
+// types of the expected and actual values:
+//
+// - If both values are nil, the test passes;
+//
+// - If either value is nil and the other is not, the test fails;
+//
+// - If both values implement the error interface, the test passes
+// if the error being tested satisfies errors.Is(expected);
+//
+// - Otherwise, the values are compared using reflect.DeepEqual
+// or a comparison function supplied in the options;
+//
+// i.e. for non-nil, non-error values, an Is() test is equivalent to:
+//
+// Expect(got).To(DeepEqual(expected), opts...)
+//
+// and for error values, an Is() test is equivalent to:
+//
+// Expect(errors.Is(got, expected)).To(BeTrue(), opts...)
+//
+// # Supported Options
+//
+// func(a, b any) bool // a function to compare the values
+// // (overriding the use of reflect.DeepEqual)
+//
+// opt.FailureReport(func) // a function that returns a custom test
+// // failure report if the test fails.
+//
+// opt.OnFailure(string) // a string to output as the failure
+// // report if the test fails.
+func (e *expectation[T]) Is(expected T, opts ...any) {
+ e.t.Helper()
+
+ switch {
+ case any(expected) == nil:
+ e.IsNil(opts...)
+ return
+
+ case any(expected) != nil && any(e.subject) == nil:
+ if _, ok := opt.Get[opt.FailureReport](opts); !ok {
+ opts = append(opts, opt.OnFailure(fmt.Sprintf("expected %v, got nil", expected)))
+ }
+ Fail(opts...)
+
+ default:
+ experr, _ := any(expected).(error)
+ goterr, _ := any(e.subject).(error)
+ if experr != nil && goterr != nil {
+ if _, ok := opt.Get[opt.FailureReport](opts); !ok {
+ opts = append(opts, opt.FailureReport(func(...any) []string {
+ return []string{
+ fmt.Sprintf("expected error: %v", experr),
+ fmt.Sprintf("got : %v", goterr),
+ }
+ }))
+ }
+
+ FailIfNot(errors.Is(goterr, experr), opts...)
+ return
+ }
+
+ Expect(e.subject, e.name).To(DeepEqual(expected), opts...)
+ }
+}
+
+// IsNot tests the value of the expectation against some expected
+// value for a non-match.
+//
+// The test behaves differently depending on the value and type of
+// the expected and actual values:
+//
+// - IsNot(nil) is equivalent to [expectation.IsNotNil];
+//
+// - If either value is [nil] and the other is not, the test passes;
+//
+// - If both values implement the [error] interface, the test passes
+// if the error being tested does not satisfy [errors.Is];
+//
+// - Otherwise, the values are compared using [reflect.DeepEqual],
+// or a comparison function supplied in the options;
+//
+// i.e. for non-nil, non-error values, an IsNot() test is equivalent to:
+//
+// Expect(got).ToNot(DeepEqual(expected), opts...)
+//
+// and for error values, an IsNot() test is equivalent to:
+//
+// Expect(errors.Is(got, expected)).To(BeFalse(), opts...)
+//
+// # Supported Options
+//
+// func(a, b any) bool // a function to compare the values
+// // (overriding the use of reflect.DeepEqual)
+//
+// opt.FailureReport(func) // a function that returns a custom test
+// // failure report if the test fails.
+//
+// opt.OnFailure(string) // a string to output as the failure
+// // report if the test fails.
+func (e *expectation[T]) IsNot(expected T, opts ...any) {
+ e.t.Helper()
+
+ switch {
+ case (any(expected) == nil) != (any(e.subject) == nil):
+ return
+
+ case any(expected) == nil:
+ e.IsNotNil(opts...)
+ return
+
+ default:
+ experr, _ := any(expected).(error)
+ goterr, _ := any(e.subject).(error)
+ if experr != nil && goterr != nil {
+ if _, ok := opt.Get[opt.FailureReport](opts); !ok {
+ opts = append(opts, opt.FailureReport(func(...any) []string {
+ return []string{
+ fmt.Sprintf("expected error that is not: %v", experr),
+ fmt.Sprintf("got : %v", goterr),
+ }
+ }))
+ }
+
+ FailIf(errors.Is(goterr, experr), opts...)
+ return
+ }
+
+ Expect(e.subject, e.name).ToNot(DeepEqual(expected), opts...)
+ }
+}
+
// Should applies a matcher to the expectation. If the matcher
// does not match the value, the test fails.
//
@@ -458,67 +552,3 @@ func (e *expectation[T]) ToNot(matcher matcher.ForType[T], opts ...any) {
e.fail(matcher, opts...)
}
}
-
-// Is tests the value of the expectation against some expected
-// value.
-//
-// The function behaves differently depending on the values and
-// types of the expected and actual values:
-//
-// - If both values are nil, the test passes;
-//
-// - If either value is nil and the other is not, the test fails;
-//
-// - If both values implement the error interface, the test passes
-// if the error being tested satisfies errors.Is(expected);
-//
-// - Otherwise, the values are compared using reflect.DeepEqual
-// or a comparison function supplied in the options;
-//
-// i.e. for non-nil, non-error values, an Is() test is equivalent to:
-//
-// Expect(got).To(DeepEqual(expected), opts...)
-//
-// and for error values, an Is() test is equivalent to:
-//
-// Expect(errors.Is(got, expected)).To(BeTrue(), opts...)
-//
-// # Supported Options
-//
-// func(a, b any) bool // a function to compare the values
-// // (overriding the use of reflect.DeepEqual)
-//
-// opt.FailureReport(func) // a function that returns a custom test
-// // failure report if the test fails.
-//
-// opt.OnFailure(string) // a string to output as the failure
-// // report if the test fails.
-func (e *expectation[T]) Is(expected T, opts ...any) {
- e.t.Helper()
-
- switch {
- case any(expected) == nil:
- e.IsNil()
- return
-
- case any(expected) != nil && any(e.subject) == nil:
- e.err(fmt.Sprintf("expected %v, got nil", expected))
-
- default:
- experr, _ := any(expected).(error)
- goterr, _ := any(e.subject).(error)
- if experr != nil && goterr != nil {
- ExpectTrue(
- errors.Is(goterr, experr),
- opt.FailureReport(func(...any) []string {
- return []string{
- fmt.Sprintf("expected error: %v", experr),
- fmt.Sprintf("got : %v", goterr),
- }
- }),
- )
- return
- }
- Expect(e.subject, e.name).To(DeepEqual(expected), opts...)
- }
-}
diff --git a/expect_test.go b/expectation_test.go
similarity index 67%
rename from expect_test.go
rename to expectation_test.go
index 2aecadf..35092e5 100644
--- a/expect_test.go
+++ b/expectation_test.go
@@ -290,6 +290,133 @@ func TestExpect_TestFailureReporting(t *testing.T) {
}...))
}
+func TestExpect_DidOccur(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ // panics
+ {Scenario: "panic was expected and occurred",
+ Act: func() {
+ defer Expect(Panic(ErrInvalidArgument)).DidOccur()
+ panic(ErrInvalidArgument)
+ },
+ },
+ {Scenario: "panic was expected and did not occur",
+ Act: func() {
+ defer Expect(Panic(ErrInvalidArgument)).DidOccur()
+ },
+ Assert: func(result *R) {
+ result.Expect(test.Failed, opt.IgnoreReport(true))
+ },
+ },
+
+ // errors
+ {Scenario: "error was expected and occurred",
+ Act: func() { Expect(errors.New("error")).DidOccur() },
+ },
+ {Scenario: "error was expected and did not occur",
+ Act: func() {
+ var err error
+ Expect(err).DidOccur()
+ },
+ Assert: func(result *R) {
+ result.Expect("expected error, got nil")
+ },
+ },
+
+ // unsupported types
+ {Scenario: "not an error or panic",
+ Act: func() {
+ Expect(42).DidOccur()
+ },
+ Assert: func(result *R) {
+ result.ExpectInvalid(
+ "test.DidOccur: may only be used with Panic() or error values",
+ )
+ },
+ },
+ }...))
+}
+
+func TestExpect_DidNotOccur(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ // panics
+ {Scenario: "panic(), no panic occurs",
+ Act: func() {
+ defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
+ },
+ },
+ {Scenario: "panic(r), no panic occurs",
+ Act: func() {
+ defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
+ },
+ },
+ {Scenario: "panic(nil)",
+ Act: func() {
+ defer Expect(Panic(nil)).DidNotOccur()
+ },
+ Assert: func(result *R) {
+ result.ExpectInvalid(
+ "DidNotOccur: may not be used with Panic(nil); did you mean NilPanic()?",
+ )
+ },
+ },
+ {Scenario: "panic was expected to not occur",
+ Act: func() {
+ defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
+ panic(ErrInvalidArgument)
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected: panic with *errors.errorString(invalid argument): should not have occurred",
+ )
+ },
+ },
+ {Scenario: "panic was not expected and occurred",
+ Act: func() {
+ defer Expect(Panic(ErrInvalidArgument)).DidNotOccur()
+ panic(ErrInvalidOperation)
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "unexpected panic:",
+ " recovered: *errors.errorString(invalid operation)",
+ )
+ },
+ },
+
+ // errors
+ {Scenario: "error was not expected and did not occur",
+ Act: func() { var err error = nil; Expect(err).DidNotOccur() },
+ },
+ {Scenario: "error was not expected and occurred",
+ Act: func() {
+ Expect(errors.New("error")).DidNotOccur()
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected: ",
+ `got : *errors.errorString(error)`,
+ )
+ },
+ },
+
+ // unsupported types
+ {Scenario: "not an error or panic",
+ Act: func() {
+ Expect(42).DidNotOccur()
+ },
+ Assert: func(result *R) {
+ result.ExpectInvalid(
+ "test.DidNotOccur: may only be used with Panic() or error values",
+ )
+ },
+ },
+ }...))
+}
+
func TestExpect_Is(t *testing.T) {
With(t)
@@ -367,6 +494,79 @@ func TestExpect_Is(t *testing.T) {
}...))
}
+func TestExpect_IsNot(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {
+ Scenario: "expecting not nil and got nil",
+ Act: func() { var a any; Expect(a).IsNot(nil) },
+ Assert: func(result *R) {
+ result.Expect("expected not nil")
+ },
+ },
+ {
+ Scenario: "expecting not nil and got not nil",
+ Act: func() { var a any = 1; Expect(a).IsNot(nil) },
+ },
+ {
+ Scenario: "expecting not nil when got is of not nilable type",
+ Act: func() { var a any = 1; Expect(a).IsNot(nil) },
+ },
+ {
+ Scenario: "expected and got are unequal ints",
+ Act: func() { var a int; Expect(a).IsNot(1) },
+ },
+ {
+ Scenario: "expecting not nil error and got nil",
+ Act: func() { var err error; Expect(err).IsNot(nil) },
+ Assert: func(result *R) {
+ result.Expect("expected not nil")
+ },
+ },
+ {
+ Scenario: "sentinel is not sentinel",
+ Act: func() {
+ sent := errors.New("sentinel")
+ Expect(sent).IsNot(sent)
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected error that is not: sentinel",
+ )
+ },
+ },
+ {
+ Scenario: "sentinel is not other sentinel",
+ Act: func() {
+ senta := errors.New("sentinel-a")
+ sentb := errors.New("sentinel-b")
+ Expect(senta).IsNot(sentb)
+ },
+ },
+ {
+ Scenario: "non-nil error is not nil",
+ Act: func() {
+ err := errors.New("error")
+ Expect(err).IsNot(nil)
+ },
+ },
+ {
+ Scenario: "struct is not equal struct",
+ Act: func() { Expect(struct{ a int }{a: 1}).IsNot(struct{ a int }{a: 1}) },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected to not equal: struct { a int }{a:1}",
+ )
+ },
+ },
+ {
+ Scenario: "struct is not unequal struct",
+ Act: func() { Expect(struct{ a int }{a: 1}).IsNot(struct{ a int }{a: 2}) },
+ },
+ }...))
+}
+
func TestExpect_Should(t *testing.T) {
With(t)
@@ -385,7 +585,7 @@ func TestExpect_Should(t *testing.T) {
Expect([]int{1}).Should(BeEmpty())
},
Assert: func(result *R) {
- result.Expect(TestFailed, opt.IgnoreReport(true)) // testing the behaviour of Should(); matcher report is not significant
+ result.Expect(test.Failed, opt.IgnoreReport(true)) // testing the behaviour of Should(); matcher report is not significant
},
},
}...))
@@ -409,7 +609,7 @@ func TestExpect_ShouldNot(t *testing.T) {
Expect([]int{}).ShouldNot(BeEmpty())
},
Assert: func(result *R) {
- result.Expect(TestFailed, opt.IgnoreReport(true)) // testing the behaviour of ToNot(); matcher report is not significant
+ result.Expect(test.Failed, opt.IgnoreReport(true)) // testing the behaviour of ToNot(); matcher report is not significant
},
},
}...))
@@ -442,7 +642,7 @@ func TestExpect_ToNot(t *testing.T) {
Expect(true).ToNot(Equal(true))
},
Assert: func(result *R) {
- result.Expect(TestFailed, opt.IgnoreReport(true)) // testing the behaviour of ToNot(), not the output of the matcher used
+ result.Expect(test.Failed, opt.IgnoreReport(true)) // testing the behaviour of ToNot(), not the output of the matcher used
},
},
}...))
@@ -459,6 +659,10 @@ func TestRequire(t *testing.T) {
result.Expect("expected false, got true")
}
+// ============================================================================
+// MARK: examples
+// ============================================================================
+
func ExampleRequire() {
test.Example()
diff --git a/fail.go b/fail.go
new file mode 100644
index 0000000..49c1707
--- /dev/null
+++ b/fail.go
@@ -0,0 +1,71 @@
+package test
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/blugnu/test/opt"
+)
+
+// Error explicitly and unconditionally fails the current test
+// with the given message.
+//
+// This should not be confused with the `test.Error` function
+// used to report an error condition in a test helper (from the
+// blugnu/test/test package).
+func Error(msg string) {
+ T().Helper()
+ Fail(opt.OnFailure(msg))
+}
+
+// Errorf explicitly and unconditionally fails the current test
+// with the formatted message.
+//
+// This should not be confused with the `test.Error` function
+// used to report an error condition in a test helper (from the
+// blugnu/test/test package).
+func Errorf(s string, args ...any) {
+ T().Helper()
+ Fail(opt.OnFailure(fmt.Sprintf(s, args...)))
+}
+
+// Fail explicitly and unconditionally fails the current test with the given
+// options.
+//
+// If the supplied options do not contain an [opt.FailureReport], the test is
+// failed with a "test failed" message.
+func Fail(opts ...any) {
+ T().Helper()
+ FailIf(true, opts...)
+}
+
+// FailIf fails the current test with the given options only if a specified
+// condition is true.
+func FailIf(cond bool, opts ...any) {
+ if !cond {
+ return
+ }
+
+ T().Helper()
+
+ if _, ok := opt.Get[opt.FailureReport](opts); !ok {
+ var msg string = "test failed"
+
+ if s, ok := opt.Get[[]string](opts); ok {
+ msg = strings.Join(s, "\n")
+ } else if s, ok := opt.Get[string](opts); ok {
+ msg = s
+ }
+
+ opts = append([]any{opt.OnFailure(msg)}, opts...)
+ }
+
+ Expect(cond).To(BeFalse(), opts...)
+}
+
+// FailIfNot fails the current test with the given options only if a specified
+// condition is not true.
+func FailIfNot(cond bool, opts ...any) {
+ T().Helper()
+ FailIf(!cond, opts...)
+}
diff --git a/fail_test.go b/fail_test.go
new file mode 100644
index 0000000..add45cb
--- /dev/null
+++ b/fail_test.go
@@ -0,0 +1,91 @@
+package test_test
+
+import (
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/opt"
+)
+
+func TestError(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "Error fails the test with the given message",
+ Act: func() {
+ Error("test error message")
+ },
+ Assert: func(result *R) {
+ result.Expect("test error message")
+ },
+ },
+ {Scenario: "Errorf fails the test with a formatted message",
+ Act: func() {
+ Errorf("test error message %d", 42)
+ },
+ Assert: func(result *R) {
+ result.Expect("test error message 42")
+ },
+ },
+ }...))
+}
+
+func TestFail(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "string message",
+ Act: func() {
+ Fail("it failed")
+ },
+ Assert: func(result *R) {
+ result.Expect("it failed")
+ },
+ },
+ {Scenario: "multi-line failure report",
+ Act: func() {
+ Fail([]string{"it failed", "on multiple lines"})
+ },
+ Assert: func(result *R) {
+ result.Expect([]string{
+ "it failed",
+ "on multiple lines",
+ })
+ },
+ },
+ {Scenario: "no failure report",
+ Act: func() {
+ Fail()
+ },
+ Assert: func(result *R) {
+ result.Expect("test failed")
+ },
+ },
+ {Scenario: "fail if condition is true",
+ Act: func() {
+ FailIf(true, "condition was true")
+ },
+ Assert: func(result *R) {
+ result.Expect("condition was true")
+ },
+ },
+ {Scenario: "fail if condition is not true",
+ Act: func() {
+ FailIfNot(false, "condition was false")
+ },
+ Assert: func(result *R) {
+ result.Expect("condition was false")
+ },
+ },
+
+ {Scenario: "applies opt.Required",
+ Act: func() {
+ Fail(opt.Required(), "failed and is required")
+ Fail("this should not be evaluated")
+ },
+ Assert: func(result *R) {
+ result.Expect("failed and is required")
+ },
+ },
+ }...))
+}
diff --git a/internal/testcase/runner.go b/internal/testcase/runner.go
index cd81dd2..e6fadfc 100644
--- a/internal/testcase/runner.go
+++ b/internal/testcase/runner.go
@@ -103,8 +103,7 @@ func (tcr *Runner[T]) AddCase(name string, tc T, flags ...Flags) {
// Run runs the test cases in the runner. Each test case is run as a subtest
// in the current test frame. The test case name is used to identify the test
-// case in the test output, and any Before/After scaffolding functions are
-// called with the test case data.
+// case in the test output.
func (tcr Runner[T]) Run() {
t := tcr.TestingT
t.Helper()
diff --git a/internal/testcase/runner_test.go b/internal/testcase/runner_test.go
index 06437c2..08a9f10 100644
--- a/internal/testcase/runner_test.go
+++ b/internal/testcase/runner_test.go
@@ -6,6 +6,7 @@ import (
. "github.com/blugnu/test"
"github.com/blugnu/test/internal/testcase"
+ "github.com/blugnu/test/test"
)
func TestNewRunner(t *testing.T) {
@@ -133,6 +134,6 @@ func TestRunner_Run(t *testing.T) {
Expect(executed).To(Equal(2))
})
- result.Expect(TestPassed)
+ result.Expect(test.Passed)
}))
}
diff --git a/internal/testframe/errors.go b/internal/testframe/errors.go
index 0e846e1..0f1f2b9 100644
--- a/internal/testframe/errors.go
+++ b/internal/testframe/errors.go
@@ -5,6 +5,7 @@ import "errors"
var (
ErrEmptyStack = errors.New("empty stack for this goroutine")
ErrNoStack = errors.New("error determining goroutine id: unable to obtain stack information for goroutine")
+ ErrNoCleanupFunction = errors.New("no cleanup function")
ErrNoTestFrame = errors.New("no test frame; did you forget to call With(t)?")
ErrUnexpectedStackFormat = errors.New("unable to determine goroutine id: unexpected stack format")
)
diff --git a/internal/testframe/is-parallel.go b/internal/testframe/is-parallel.go
new file mode 100644
index 0000000..eda49b7
--- /dev/null
+++ b/internal/testframe/is-parallel.go
@@ -0,0 +1,26 @@
+package testframe
+
+import (
+ "reflect"
+ "testing"
+)
+
+// IsParallel checks if the current test frame is running in parallel.
+//
+// The underlying type of the current test frame must be a *testing.T;
+// any other implementation will return false.
+//
+// For *testing.T values, reflection is used to examine the internal
+// state to determine if it is running in parallel.
+func IsParallel() bool {
+ t, ok := Peek[*testing.T]()
+ if !ok {
+ // tests cannot be parallel if the TestingT is not a *testing.T
+ return false
+ }
+
+ c := reflect.Indirect(reflect.ValueOf(t)).FieldByName("common")
+ ip := reflect.Indirect(c).FieldByName("isParallel")
+
+ return ip.Bool()
+}
diff --git a/internal/testframe/is-parallel_test.go b/internal/testframe/is-parallel_test.go
new file mode 100644
index 0000000..fe1fc96
--- /dev/null
+++ b/internal/testframe/is-parallel_test.go
@@ -0,0 +1,34 @@
+package testframe_test
+
+import (
+ "testing"
+
+ . "github.com/blugnu/test"
+
+ "github.com/blugnu/test/internal/testframe"
+ "github.com/blugnu/test/test"
+)
+
+func TestIsParallel(t *testing.T) {
+ With(t)
+
+ Run(Test("not parallel", func() {
+ Expect(testframe.IsParallel()).To(BeFalse())
+ T().Parallel()
+ Expect(testframe.IsParallel()).To(BeTrue())
+ }))
+
+ Run(Test("parallel", func() {
+ T().Parallel()
+ Expect(testframe.IsParallel()).To(BeTrue())
+ }))
+
+ Run(Test("example runner is always non-parallel", func() {
+ test.Example()
+ defer testframe.Pop()
+
+ T().Parallel() // NO-OP in example mode
+
+ Expect(testframe.IsParallel()).To(BeFalse())
+ }))
+}
diff --git a/internal/testframe/stack.go b/internal/testframe/stack.go
index 81bc88a..1f33b93 100644
--- a/internal/testframe/stack.go
+++ b/internal/testframe/stack.go
@@ -125,3 +125,18 @@ func Push(t any) {
stk = append(stk, testframe{T: t})
stacks.frames[id] = stk
}
+
+// PushWithCleanup adds a new test frame to the current goroutine's stack
+// and registers a cleanup function to remove it when the test frame completes.
+//
+// The function is safe to call concurrently and will not modify the stack
+// of other goroutines.
+func PushWithCleanup(t any) {
+ c, ok := t.(interface{ Cleanup(func()) })
+ if !ok {
+ panic(ErrNoCleanupFunction)
+ }
+
+ Push(t)
+ c.Cleanup(Pop)
+}
diff --git a/internal/testframe/stack_test.go b/internal/testframe/stack_test.go
index 648f5e2..95ddad9 100644
--- a/internal/testframe/stack_test.go
+++ b/internal/testframe/stack_test.go
@@ -188,3 +188,46 @@ func TestPush(t *testing.T) {
t.Errorf("expected 1 stack, got %d", len(stacks.frames))
}
}
+
+type mockT struct {
+ cleanupWasCalled bool
+}
+
+func (m *mockT) Cleanup(f func()) {
+ m.cleanupWasCalled = true
+ f()
+}
+
+func TestPushWithCleanup(t *testing.T) {
+ og := stacks.frames
+ defer func() {
+ stacks.frames = og
+ }()
+
+ t.Run("cleanup called", func(t *testing.T) {
+ mock := &mockT{}
+
+ // ensure we start with no stacks
+ stacks.frames = map[uintptr][]testframe{}
+
+ PushWithCleanup(mock)
+
+ if !mock.cleanupWasCalled {
+ t.Errorf("expected cleanup to be called")
+ }
+ })
+
+ t.Run("called with non-cleanup type", func(t *testing.T) {
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Error("expected panic, got nil")
+ } else if err, ok := r.(error); !ok || !errors.Is(err, ErrNoCleanupFunction) {
+ t.Errorf("expected panic with ErrNoCleanupFunction, got: %v", r)
+ }
+ }()
+
+ // ensure we start with no stacks
+ PushWithCleanup(42)
+ })
+}
diff --git a/internal/type-name.go b/internal/type-name.go
new file mode 100644
index 0000000..4c4a316
--- /dev/null
+++ b/internal/type-name.go
@@ -0,0 +1,48 @@
+package internal
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// TypeName returns the name of the type of the provided value.
+//
+// If the type cannot be determined (for example, if the value is nil),
+// the name of the type parameter T is returned instead.
+//
+// Note that for unnamed types (for example, inline interfaces), ""
+// is returned as the type name.
+func TypeName[T any](v ...T) string {
+ var n string
+ var t T
+
+ switch len(v) {
+ case 0:
+ n = fmt.Sprintf("%T", t)
+
+ default:
+ n = fmt.Sprintf("%T", v[0])
+ }
+
+ // if we could not determine the type of the expected value using the
+ // zero value of the type, we can use a dummy function and reflect
+ // the type of the first argument to that function.
+ //
+ // Q: why not just use the dummy func technique every time?
+ // A: because it is more expensive than using the zero value, and
+ // using the zero value provides a more precise (package-qualified)
+ // type name, when possible
+ if n == "" {
+ fn := func(T) { /* NO-OP */ }
+ n = reflect.TypeOf(fn).In(0).Name()
+
+ // call the function to ensure it is considered "covered" by tests
+ fn(t)
+ }
+
+ if n == "" {
+ n = ""
+ }
+
+ return n
+}
diff --git a/internal/type-name_test.go b/internal/type-name_test.go
new file mode 100644
index 0000000..f35dfa5
--- /dev/null
+++ b/internal/type-name_test.go
@@ -0,0 +1,79 @@
+package internal_test
+
+import (
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/internal"
+)
+
+func TestTypeName(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "basic type",
+ Act: func() {
+ Expect(internal.TypeName[int]()).To(Equal("int"))
+ },
+ },
+
+ {Scenario: "pointer to basic type",
+ Act: func() {
+ Expect(internal.TypeName[*int]()).To(Equal("*int"))
+ },
+ },
+
+ {Scenario: "struct type",
+ Act: func() {
+ type MyStruct struct{}
+ Expect(internal.TypeName[MyStruct]()).To(Equal("internal_test.MyStruct"))
+ },
+ },
+
+ {Scenario: "pointer to struct type",
+ Act: func() {
+ type MyStruct struct{}
+ Expect(internal.TypeName[*MyStruct]()).To(Equal("*internal_test.MyStruct"))
+ },
+ },
+
+ {Scenario: "type based on basic type",
+ Act: func() {
+ type MyInt int
+ Expect(internal.TypeName[MyInt]()).To(Equal("internal_test.MyInt"))
+ },
+ },
+
+ {Scenario: "pointer to type based on basic type",
+ Act: func() {
+ type MyInt int
+ Expect(internal.TypeName[*MyInt]()).To(Equal("*internal_test.MyInt"))
+ },
+ },
+
+ {Scenario: "interface type",
+ Act: func() {
+ Expect(internal.TypeName[error]()).To(Equal("error"))
+ },
+ },
+
+ {Scenario: "nil interface",
+ Act: func() {
+ var err error = nil
+ Expect(internal.TypeName(err)).To(Equal("error"))
+ },
+ },
+
+ {Scenario: "anonymous struct",
+ Act: func() {
+ Expect(internal.TypeName[struct{ foo int }]()).To(Equal("struct { foo int }"))
+ },
+ },
+
+ {Scenario: "anonymous interface",
+ Act: func() {
+ Expect(internal.TypeName[interface{ Foo() }]()).To(Equal(""))
+ },
+ },
+ }...))
+}
diff --git a/match-boolean.go b/match-boolean.go
new file mode 100644
index 0000000..8295072
--- /dev/null
+++ b/match-boolean.go
@@ -0,0 +1,25 @@
+package test
+
+import (
+ "github.com/blugnu/test/matchers/bools"
+)
+
+// BeFalse returns a matcher that will fail if the matched value is not false.
+//
+// # Supported Options
+//
+// opt.FailureReport(...) // a function returning a custom failure report
+// // in the event that the test fails
+func BeFalse() bools.BooleanMatcher {
+ return bools.BooleanMatcher{Expected: false}
+}
+
+// BeTrue returns a matcher that will fail if the matched value is not true.
+//
+// # Supported Options
+//
+// opt.FailureReport(...) // a function returning a custom failure report
+// // in the event that the test fails
+func BeTrue() bools.BooleanMatcher {
+ return bools.BooleanMatcher{Expected: true}
+}
diff --git a/bool_test.go b/match-boolean_test.go
similarity index 62%
rename from bool_test.go
rename to match-boolean_test.go
index aad5eef..f2ee934 100644
--- a/bool_test.go
+++ b/match-boolean_test.go
@@ -4,6 +4,7 @@ import (
"testing"
. "github.com/blugnu/test"
+ "github.com/blugnu/test/expect"
"github.com/blugnu/test/opt"
"github.com/blugnu/test/test"
)
@@ -13,11 +14,11 @@ func TestBooleans(t *testing.T) {
Run(
HelperTests([]HelperScenario{
- {Scenario: "ExpectFalse when false",
- Act: func() { ExpectFalse(false) },
+ {Scenario: "expect.False when false",
+ Act: func() { expect.False(false) },
},
- {Scenario: "ExpectTrue when true",
- Act: func() { ExpectTrue(true) },
+ {Scenario: "expect.True when true",
+ Act: func() { expect.True(true) },
},
{Scenario: "BeFalse when false",
Act: func() { Expect(false).To(BeFalse()) },
@@ -33,37 +34,30 @@ func TestBooleans(t *testing.T) {
},
// supported options
- {Scenario: "ExpectFalse with name",
- Act: func() { ExpectFalse(true, "this will fail") },
+ {Scenario: "expect.False with name",
+ Act: func() { expect.False(true, "this will fail") },
Assert: func(result *R) {
- // Expect(result).To(HaveFailedWithReport(
- // "this will fail:",
- // " expected false, got true",
- // ))
result.Expect(
"this will fail:",
- " expected false, got true",
+ " expected false",
)
},
},
- {Scenario: "ExpectFalse with custom failure report",
+ {Scenario: "expect.False with custom failure report",
Act: func() {
- ExpectFalse(true, opt.FailureReport(func(...any) []string {
+ expect.False(true, opt.FailureReport(func(...any) []string {
return []string{"custom failure report"}
}))
},
Assert: func(result *R) {
- // Expect(result).To(HaveFailedWithReport(
- // "custom failure report",
- // ))
result.Expect(
"custom failure report",
)
},
},
- {Scenario: "ExpectFalse with name and custom failure report",
+ {Scenario: "expect.False with name and custom failure report",
Act: func() {
- ExpectFalse(true, "this will fail", opt.FailureReport(func(...any) []string {
+ expect.False(true, "this will fail", opt.FailureReport(func(...any) []string {
return []string{"custom failure report"}
}))
},
@@ -74,18 +68,18 @@ func TestBooleans(t *testing.T) {
)
},
},
- {Scenario: "ExpectTrue with name",
- Act: func() { ExpectTrue(false, "this will fail") },
+ {Scenario: "expect.True with name",
+ Act: func() { expect.True(false, "this will fail") },
Assert: func(result *R) {
result.Expect(
"this will fail:",
- " expected true, got false",
+ " expected true",
)
},
},
- {Scenario: "ExpectTrue with custom failure report",
+ {Scenario: "expect.True with custom failure report",
Act: func() {
- ExpectTrue(false, opt.FailureReport(func(...any) []string {
+ expect.True(false, opt.FailureReport(func(...any) []string {
return []string{"custom failure report"}
}))
},
@@ -95,9 +89,9 @@ func TestBooleans(t *testing.T) {
)
},
},
- {Scenario: "ExpectTrue with name and custom failure report",
+ {Scenario: "expect.True with name and custom failure report",
Act: func() {
- ExpectTrue(false, "this will fail", opt.FailureReport(func(...any) []string {
+ expect.True(false, "this will fail", opt.FailureReport(func(...any) []string {
return []string{"custom failure report"}
}))
},
@@ -136,23 +130,9 @@ func TestBooleans(t *testing.T) {
)
}
-func ExampleExpectFalse() {
- test.Example()
-
- ExpectFalse(true)
-
- // Output:
- // expected false, got true
-}
-
-func ExampleExpectTrue() {
- test.Example()
-
- ExpectTrue(false)
-
- // Output:
- // expected true, got false
-}
+// =================================================================
+// MARK: examples
+// =================================================================
func ExampleBeFalse() {
test.Example()
@@ -160,7 +140,7 @@ func ExampleBeFalse() {
Expect(true).To(BeFalse())
// Output:
- // expected false, got true
+ // expected false
}
func ExampleBeTrue() {
@@ -169,5 +149,5 @@ func ExampleBeTrue() {
Expect(false).To(BeTrue())
// Output:
- // expected true, got false
+ // expected true
}
diff --git a/bytes.go b/match-bytes.go
similarity index 95%
rename from bytes.go
rename to match-bytes.go
index dfe391d..acae7cb 100644
--- a/bytes.go
+++ b/match-bytes.go
@@ -1,6 +1,8 @@
package test
-import "github.com/blugnu/test/matchers/bytes"
+import (
+ "github.com/blugnu/test/matchers/bytes"
+)
// EqualBytes returns a matcher that checks if a byte slice is equal to an
// expected byte slice.
diff --git a/bytes_test.go b/match-bytes_test.go
similarity index 86%
rename from bytes_test.go
rename to match-bytes_test.go
index 3515be2..7734ffa 100644
--- a/bytes_test.go
+++ b/match-bytes_test.go
@@ -8,7 +8,7 @@ import (
"github.com/blugnu/test/test"
)
-func TestSliceOfBytes(t *testing.T) {
+func TestEqualBytes(t *testing.T) {
With(t)
Run(
@@ -43,6 +43,10 @@ func TestSliceOfBytes(t *testing.T) {
)
}
+// =================================================================
+// MARK: examples
+// =================================================================
+
func ExampleEqualBytes() {
test.Example()
diff --git a/context.go b/match-context.go
similarity index 100%
rename from context.go
rename to match-context.go
diff --git a/match-context_test.go b/match-context_test.go
new file mode 100644
index 0000000..9a93cd7
--- /dev/null
+++ b/match-context_test.go
@@ -0,0 +1,107 @@
+package test_test
+
+import (
+ "context"
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/opt"
+ "github.com/blugnu/test/test"
+)
+
+func TestContext(t *testing.T) {
+ With(t)
+
+ type key string
+ ctx := context.WithValue(context.Background(), key("key"), "value")
+
+ Run(
+ Test("ContextKey", func() {
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "expected key present",
+ Act: func() {
+ Expect(ctx).To(HaveContextKey(key("key")))
+ },
+ },
+ {Scenario: "expected key not present",
+ Act: func() {
+ Expect(ctx).To(HaveContextKey(key("other-key")))
+ },
+ Assert: func(result *R) {
+ result.Expect(test.Failed, opt.IgnoreReport(true))
+ },
+ },
+ }...))
+ }))
+
+ Run(Test("ContextValue", func() {
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "expected value present",
+ Act: func() {
+ Expect(ctx).To(HaveContextValue(key("key"), "value"))
+ },
+ },
+ {Scenario: "expected value not present",
+ Act: func() {
+ Expect(ctx).To(HaveContextValue(key("other-key"), "value"))
+ },
+ Assert: func(result *R) {
+ result.Expect(test.Failed, opt.IgnoreReport(true))
+ },
+ },
+ {Scenario: "expected value present but different",
+ Act: func() {
+ Expect(ctx).To(HaveContextValue(key("key"), "other value"))
+ },
+ Assert: func(result *R) {
+ result.Expect(test.Failed, opt.IgnoreReport(true))
+ },
+ },
+ }...))
+ }),
+ )
+}
+
+// =================================================================
+// MARK: examples
+// =================================================================
+
+func ExampleHaveContextKey() {
+ test.Example()
+
+ type key int
+ ctx := context.WithValue(context.Background(), key(57), "varieties")
+
+ // these tests will pass
+ Expect(ctx).To(HaveContextKey(key(57)))
+ Expect(ctx).ToNot(HaveContextKey(key(58)))
+
+ // this test will fail
+ Expect(ctx).To(HaveContextKey(key(58)))
+
+ // Output:
+ // expected key: test_test.key(58)
+ // key not present in context
+}
+
+func ExampleHaveContextValue() {
+ // this is needed to make the example work; this would be usually
+ // be `With(t)` where `t` is the *testing.T
+ test.Example()
+
+ type key int
+ ctx := context.WithValue(context.Background(), key(57), "varieties")
+
+ // these tests will pass
+ Expect(ctx).To(HaveContextValue(key(57), "varieties"))
+ Expect(ctx).ToNot(HaveContextValue(key(56), "varieties"))
+ Expect(ctx).ToNot(HaveContextValue(key(57), "flavours"))
+
+ // this test will fail
+ Expect(ctx).To(HaveContextValue(key(57), "flavours"))
+
+ // Output:
+ // context value: test_test.key(57)
+ // expected: "flavours"
+ // got : "varieties"
+}
diff --git a/emptiness.go b/match-emptiness.go
similarity index 81%
rename from emptiness.go
rename to match-emptiness.go
index 6fec1e7..aa2538d 100644
--- a/emptiness.go
+++ b/match-emptiness.go
@@ -2,7 +2,6 @@ package test
import (
"github.com/blugnu/test/matchers/emptiness"
- "github.com/blugnu/test/matchers/length"
)
// BeEmpty returns a matcher that checks if the value is empty.
@@ -74,24 +73,3 @@ func BeEmpty() *emptiness.Matcher {
func BeEmptyOrNil() *emptiness.Matcher {
return &emptiness.Matcher{TreatNilAsEmpty: true}
}
-
-// HaveLen returns a matcher that checks if the value has len() equal to n.
-//
-// The returned matcher is an `AnyMatcher` that may only be used with values
-// of a type that is compatible with the built-in len() function. That is:
-//
-// - string
-// - slice
-// - array
-// - channel
-// - map
-//
-// A nil value of any of these types is considered to have a length of 0.
-//
-// If the value is of any other type, the test fails as an invalid test,
-// with a message similar to:
-//
-// length.Matcher: requires a value that is a string, slice, channel, or map: got
-func HaveLen(n int) *length.Matcher {
- return &length.Matcher{Length: n}
-}
diff --git a/match-emptiness_test.go b/match-emptiness_test.go
new file mode 100644
index 0000000..7876dd3
--- /dev/null
+++ b/match-emptiness_test.go
@@ -0,0 +1,101 @@
+package test_test
+
+import (
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/test"
+)
+
+func TestBeEmpty(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "empty slice is empty",
+ Act: func() {
+ Expect([]int{}).Should(BeEmpty())
+ },
+ },
+ }...))
+}
+
+func TestBeEmptyOrNil(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "nil slice is empty",
+ Act: func() {
+ var nilSlice []int
+ Expect(nilSlice).Should(BeEmptyOrNil())
+ },
+ },
+ }...))
+}
+
+// =================================================================
+// MARK: examples
+// =================================================================
+
+func ExampleBeEmpty() {
+ test.Example()
+
+ // these tests all pass:
+ Expect([]int{}).Should(BeEmpty())
+ Expect("").Should(BeEmpty())
+ Expect(map[int]string{}).Should(BeEmpty())
+ Expect([0]int{}).Should(BeEmpty())
+
+ // these tests all fail:
+ Expect([]int{1}).Should(BeEmpty())
+
+ var ch chan int
+ Expect(ch, "nil channel").Should(BeEmpty())
+
+ var ns []int
+ Expect(ns, "nil slice").Should(BeEmpty())
+
+ var nm map[int]struct{}
+ Expect(nm, "nil map").Should(BeEmpty())
+
+ // Output:
+ // expected:
+ // got : len() == 1
+ //
+ // nil channel:
+ // expected:
+ // got : nil chan
+ //
+ // nil slice:
+ // expected:
+ // got : nil slice
+ //
+ // nil map:
+ // expected:
+ // got : nil map
+}
+
+func ExampleBeEmptyOrNil() {
+ test.Example()
+
+ // these tests all pass:
+ Expect([]int{}).Should(BeEmptyOrNil())
+ Expect("").Should(BeEmptyOrNil())
+ Expect(map[int]string{}).Should(BeEmptyOrNil())
+ Expect([0]int{}).Should(BeEmptyOrNil())
+
+ var ch chan int
+ Expect(ch, "nil channel").Should(BeEmptyOrNil())
+
+ var ns []int
+ Expect(ns, "nil slice").Should(BeEmptyOrNil())
+
+ var nm map[int]struct{}
+ Expect(nm, "nil map").Should(BeEmptyOrNil())
+
+ // this test will fail:
+ Expect([]int{1}).Should(BeEmptyOrNil())
+
+ // Output:
+ // expected:
+ // got : len() == 1
+}
diff --git a/equal.go b/match-equals.go
similarity index 98%
rename from equal.go
rename to match-equals.go
index 201611b..e03cabc 100644
--- a/equal.go
+++ b/match-equals.go
@@ -1,25 +1,21 @@
package test
-import "github.com/blugnu/test/matchers/equal"
+import (
+ "github.com/blugnu/test/matchers/equal"
+)
-// Equal returns a matcher that checks if a value of type T is equal to some
-// expected value of type T. The type T must be comparable.
-//
-// Equality is determined using ONE of the following, in order of preference:
+// DeepEqual returns a matcher that checks if a value of type T is equal
+// to some expected value of type T.
//
-// 1. any comparison function provided as an option
-// 2. a T.Equal(T) bool method, if it exists
-// 3. the == operator
+// Equality is always evaluated using reflect.DeepEqual. i.e. the matcher does
+// not support the use of a custom comparison function and will not use any
+// Equal(T) method implemented by type T.
//
-// If specified, a custom comparison function must take two arguments of type
-// T, returning a boolean indicating whether the values are considered equal.
+// To use a custom comparison function or a T.Equal(T) method, use the Equal()
+// matcher.
//
// # Supported Options
//
-// func(T, T) bool // a function to compare the values
-// // (overriding the use of the == operator or
-// // T.Equal(T) method, if it exists)
-//
// opt.QuotedString(bool) // determines whether string values in failure reports
// // are quoted (default is true); the option has no
// // effect on values that are not strings
@@ -30,22 +26,28 @@ import "github.com/blugnu/test/matchers/equal"
//
// opt.FailureReport(func) // a function returning a custom failure report
// // when the values are not equal
-func Equal[T comparable](want T) equal.Matcher[T] {
- return equal.Matcher[T]{Expected: want}
+func DeepEqual[T any](want T) equal.DeepMatcher[T] {
+ return equal.DeepMatcher[T]{Expected: want}
}
-// DeepEqual returns a matcher that checks if a value of type T is equal
-// to some expected value of type T.
+// Equal returns a matcher that checks if a value of type T is equal to some
+// expected value of type T. The type T must be comparable.
//
-// Equality is always evaluated using reflect.DeepEqual. i.e. the matcher does
-// not support the use of a custom comparison function and will not use any
-// Equal(T) method implemented by type T.
+// Equality is determined using ONE of the following, in order of preference:
//
-// To use a custom comparison function or a T.Equal(T) method, use the Equal()
-// matcher.
+// 1. any comparison function provided as an option
+// 2. a T.Equal(T) bool method, if it exists
+// 3. the == operator
+//
+// If specified, a custom comparison function must take two arguments of type
+// T, returning a boolean indicating whether the values are considered equal.
//
// # Supported Options
//
+// func(T, T) bool // a function to compare the values
+// // (overriding the use of the == operator or
+// // T.Equal(T) method, if it exists)
+//
// opt.QuotedString(bool) // determines whether string values in failure reports
// // are quoted (default is true); the option has no
// // effect on values that are not strings
@@ -56,6 +58,6 @@ func Equal[T comparable](want T) equal.Matcher[T] {
//
// opt.FailureReport(func) // a function returning a custom failure report
// // when the values are not equal
-func DeepEqual[T any](want T) equal.DeepMatcher[T] {
- return equal.DeepMatcher[T]{Expected: want}
+func Equal[T comparable](want T) equal.Matcher[T] {
+ return equal.Matcher[T]{Expected: want}
}
diff --git a/equal_test.go b/match-equals_test.go
similarity index 91%
rename from equal_test.go
rename to match-equals_test.go
index b29f8f4..dce793a 100644
--- a/equal_test.go
+++ b/match-equals_test.go
@@ -7,19 +7,6 @@ import (
"github.com/blugnu/test/test"
)
-func TestEqual(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "expected equal and was equal",
- Act: func() { Expect(1).To(Equal(1)) },
- },
- {Scenario: "expected to not be equal and was not equal",
- Act: func() { Expect(1).ToNot(Equal(2)) },
- },
- }...))
-}
-
func TestDeepEqual(t *testing.T) {
With(t)
@@ -37,6 +24,39 @@ func TestDeepEqual(t *testing.T) {
}...))
}
+func TestEqual(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "expected equal and was equal",
+ Act: func() { Expect(1).To(Equal(1)) },
+ },
+ {Scenario: "expected to not be equal and was not equal",
+ Act: func() { Expect(1).ToNot(Equal(2)) },
+ },
+ }...))
+}
+
+// =================================================================
+// MARK: examples
+// =================================================================
+
+func ExampleDeepEqual() {
+ test.Example()
+
+ Expect([]byte{1, 2, 3}).To(DeepEqual([]byte{1, 2, 4}))
+ Expect([]uint8{1, 1, 2, 3, 5}).To(DeepEqual([]uint8{1, 2, 4, 8, 16}))
+
+ // this will not compile because the types are not the same:
+ // Expect([]uint8{1, 1, 2, 3, 5}).To(DeepEqual([]int{1,1,2,3,5}))
+
+ // Output:
+ // expected [1 2 4], got [1 2 3]
+ //
+ // expected: [1 2 4 8 16]
+ // got : [1 1 2 3 5]
+}
+
func ExampleEqual() {
test.Example()
@@ -61,19 +81,3 @@ func ExampleEqual() {
// expected: "the lord of the rings"
// got : "the hobbit"
}
-
-func ExampleDeepEqual() {
- test.Example()
-
- Expect([]byte{1, 2, 3}).To(DeepEqual([]byte{1, 2, 4}))
- Expect([]uint8{1, 1, 2, 3, 5}).To(DeepEqual([]uint8{1, 2, 4, 8, 16}))
-
- // this will not compile because the types are not the same:
- // Expect([]uint8{1, 1, 2, 3, 5}).To(DeepEqual([]int{1,1,2,3,5}))
-
- // Output:
- // expected [1 2 4], got [1 2 3]
- //
- // expected: [1 2 4 8 16]
- // got : [1 1 2 3 5]
-}
diff --git a/match-errors-nil-and-panics.go b/match-errors-nil-and-panics.go
new file mode 100644
index 0000000..41f2936
--- /dev/null
+++ b/match-errors-nil-and-panics.go
@@ -0,0 +1,350 @@
+package test
+
+import (
+ "fmt"
+ "runtime"
+
+ "github.com/blugnu/test/matchers/nilness"
+ "github.com/blugnu/test/matchers/panics"
+ "github.com/blugnu/test/matchers/typecheck"
+ "github.com/blugnu/test/opt"
+ "github.com/blugnu/test/test"
+)
+
+// MARK: occurs
+
+// DidOccur is used to check whether an expected panic or error occurred.
+//
+// # Testing for Panics
+//
+// Use the Panic(r) function to create an expectation that a value r will
+// be recovered from a panic. The call to DidOccur() must be deferred:
+//
+// defer Expect(Panic(r)).DidOccur(opts...)
+//
+// If the value r is an error the test will pass only if a panic occurs
+// and an error is recovered from the panic that satisfies errors.Is(r).
+//
+// If the expected recovered value is not an error, the test passes if
+// the recovered value is equal to the expected value, based on comparison
+// using reflect.DeepEqual or a comparison function.
+//
+// # Supported Options
+//
+// func(a, b any) bool // a function to compare the values, overriding
+// // the use of reflect.DeepEqual.
+//
+// # Testing for Errors
+//
+// To test for an error, use the error value as the expected value.
+// The test will pass if the error is not nil:
+//
+// Expect(err).DidOccur()
+//
+// This is equivalent to:
+//
+// Expect(err).IsNotNil()
+//
+// NOTE: this approach to testing for errors is not recommended since
+// the test will pass if any error occurred which may or may not be an
+// expected error. This may be acceptable in very simple cases but
+// it is usually better to test for a specific error using one of the
+// other methods described below.:
+//
+// Expect(err).Is(expectedError)
+// Expect(err).To(BeError(expectedErr))
+// expect.Error[E](err)
+func (e expectation[T]) DidOccur(opts ...any) {
+ e.t.Helper()
+
+ switch v := any(e.subject).(type) {
+ case panics.Expected:
+ match := &panics.MatchRecovered{R: recover()}
+
+ if match.R != nil {
+ const bufsize = 65536
+ stk := make([]byte, bufsize)
+ n := runtime.Stack(stk, false)
+ match.Stack = stk[:n-1]
+ }
+
+ if !match.Match(v, opts...) {
+ e.fail(match, opts...)
+ }
+
+ case error:
+ if v != nil {
+ return
+ }
+
+ case nil:
+ e.err("expected error, got nil")
+
+ default:
+ test.Invalid("test.DidOccur: may only be used with Panic() or error values")
+ }
+}
+
+// DidNotOccur is used to ensure that a panic or error did not occur.
+//
+// # Testing for Panics
+//
+// Use the Panic() function to create an expectation for a Panic with
+// an unspecified recovered value. The call to DidNotOccur() must be
+// deferred:
+//
+// defer Expect(Panic()).DidNotOccur(opts...)
+//
+// The test will pass only if the function scope terminates without
+// a panic having occurred.
+//
+// # Testing for Errors
+//
+// To test for an error, use the error value as the expected value.
+// The test will pass if the error is nil:
+//
+// Expect(err).DidNotOccur()
+//
+// This is equivalent to:
+//
+// Expect(err).IsNil()
+func (e expectation[T]) DidNotOccur(opts ...any) {
+ e.t.Helper()
+
+ switch expected := any(e.subject).(type) {
+ case panics.Expected:
+ // for a "DidNotOccur" test, things are more complicated:
+
+ // first let's grab any recoverable value and create a
+ // matcher which we'll use later...
+ matcher := &panics.MatchRecovered{R: recover()}
+
+ // first, using DidNotOccur with Panic(nil) is invalid since it
+ // is likely to cause confusion
+ if expected.R == opt.NoPanicExpected(true) {
+ test.Invalid("DidNotOccur: may not be used with Panic(nil); did you mean NilPanic()?")
+ }
+
+ // if we expect Panic(x) did NOT occur, but Panic(y) DID occur,
+ // then although the expectation was met, the UNexpected panic
+ // should still be reported as a test failure.
+ //
+ // so we use the MatchesPanic matcher to determine whether
+ // the recovered value matches the expected value...
+ recoveredExpectedValue := matcher.Match(expected, opts...)
+
+ // if the recovered value matches the expected value, then
+ // the test has failed since this panic should not have occurred...
+ if recoveredExpectedValue && expected.R != nil {
+ // we add the ToNotMatch(true) option to indicate that the
+ // expectation was that the panic should not have occurred
+ e.fail(matcher, append(opts, opt.ToNotMatch(true))...)
+ return
+ }
+
+ // but we're not done yet...
+ //
+ // the recovered value did not match the expected value, and if that
+ // recovered value is not nil, then we have an unexpected panic to report...
+ if matcher.R != nil {
+ // the existing matcher has already been used to test the recovered
+ // value against an expected value where-as we now need to report an
+ // unexpected panic (i.e. expected nil)
+ //
+ // so we create a new panic matcher, matching against an expected R:nil
+ // and use THAT to report the failure
+ matcher := &panics.MatchRecovered{R: matcher.R}
+ matcher.Match(panics.Expected{R: nil})
+ e.fail(matcher, opts...)
+ }
+
+ case error:
+ opts = append(opts, opt.FailureReport(func(opts ...any) []string {
+ return []string{
+ "expected: ",
+ fmt.Sprintf("got : %T(%v)", expected, opt.ValueAsString(expected, opts...)),
+ }
+ }))
+ Expect(expected).IsNil(opts...)
+
+ case nil:
+ return
+
+ default:
+ test.Invalid("test.DidNotOccur: may only be used with Panic() or error values")
+ }
+}
+
+// MARK: error
+
+// BeError returns a matcher that checks if the value is an error that satisfies
+// [errors.As] for the type E. This test asserts the type of the error but does
+// not return the error as that type.
+//
+// # Alternatives
+//
+// To perform further tests on a value that passes this check using a strongly
+// typed value of type E, use:
+//
+// expect.Error[E](err) // returns an E and an indicator whether the test passed
+// // or failed, without halting test execution on failure
+//
+// require.Error[E](err) // returns an E when the test passes; halts current test
+// // execution when the test fails, avoiding the need
+// // for a returned indicator
+func BeError[E error]() typecheck.MatchError[E] {
+ return typecheck.MatchError[E]{}
+}
+
+// MARK: nil
+
+// IsNil checks that the value of the expectation is nil. If the
+// value is not nil, the test fails. If the value is nil, the test
+// passes.
+//
+// If the value being tested does not support a nil value the test
+// will fail and produce a report similar to:
+//
+// test.IsNil: values of type '' are not nilable
+//
+// # Supported Options
+//
+// opt.QuotedStrings(bool) // determines whether any non-nil string
+// // values are quoted in any test failure
+// // report. The default is false (string
+// // values are quoted).
+// //
+// // If the value is not a string type this
+// // option has no effect.
+//
+// opt.FailureReport(func) // a function that returns a custom test
+// // failure report if the test fails.
+//
+// opt.OnFailure(string) // a string to output as the failure
+// // report if the test fails.
+func (e expectation[T]) IsNil(opts ...any) {
+ e.t.Helper()
+ e.Should(BeNil(), opts...)
+}
+
+// IsNotNil checks that a specified value is not nil. If the value
+// is not nil, the test fails. If the value is nil, the test passes.
+//
+// NOTE: If the value being tested does not support a nil value the
+// test will pass. This is to allow for testing values that may be
+// nilable or non-nilable.
+//
+// # Supported Options
+//
+// opt.FailureReport(func) // a function that returns a custom test
+// // failure report if the test fails.
+//
+// opt.OnFailure(string) // a string to output as the failure
+// // report if the test fails.
+func (e expectation[T]) IsNotNil(opts ...any) {
+ e.t.Helper()
+ e.ShouldNot(BeNil(), opts...)
+}
+
+// BeNil returns a matcher that checks if the value is nil.
+//
+// The returned matcher is an `AnyMatcher` that may only be used
+// with the `Should()` method, or with `To()` where the subject
+// is of formal type any.
+//
+// If used in a To() or Should() test with a subject that is not
+// nilable, the test fails with a message similar to:
+//
+// test.BeNil: values of type '' are not nilable
+//
+// If used with ToNo() or ShouldNot() a non-nilable subject will
+// pass the test.
+//
+// # Supported Options
+//
+// opt.QuotedStrings(bool) // determines whether any non-nil string
+// // values are quoted in any test failure
+// // report. The default is false (string
+// // values are quoted).
+// //
+// // If the value is not a string type this
+// // option has no effect.
+//
+// opt.FailureReport(func) // a function that returns a custom test
+// // failure report if the test fails.
+//
+// opt.OnFailure(string) // a string to output as the failure
+// // report if the test fails.
+func BeNil() nilness.Matcher {
+ return nilness.Matcher{}
+}
+
+// MARK: panics
+
+// NilPanic returns an expectation that a panic will occur that recovers
+// a *runtime.PanicNilError.
+//
+// see: https://go.dev/blog/compat#expanded-godebug-support-in-go-121
+//
+// For more information, refer to the # The Panic(nil) Special Case
+// described in the [Panic] function documentation.
+func NilPanic() panics.Expected {
+ return panics.Expected{R: &runtime.PanicNilError{}}
+}
+
+// Panic returns an expectation subject that can be used to test whether a
+// panic has occurred, optionally identifying a value that should match the
+// value recovered from the expected panic.
+//
+// NOTE: At most ONE panic test should be expected per function. In addition,
+// extreme care should be exercised when combining panic tests with other
+// deferred recover() calls as these will also interfere with a panic test
+// (or vice versa).
+//
+// # Usage
+//
+// - If called with no arguments, any panic will satisfy the expectation,
+// regardless of the value recovered.
+//
+// - If called with a single argument, it will expect to recover a panic that
+// recovers that value (unless the argument is nil; see The Panic(nil)
+// Special Case, below)
+//
+// - If called with > 1 argument, the test will be failed as invalid.
+//
+// # The Panic(nil) Special Case
+//
+// Panic(nil) is a special case that is equivalent to "no panic expected".
+// This is motivated by table-driven tests to avoid having to write conditional
+// code to handle test cases where a panic is expected vs those where not.
+//
+// Treating Panic(nil) as "no panic expected" allows you to write:
+//
+// defer Expect(Panic(testcase.expectedPanic)).DidOccur()
+//
+// When testcase.expectedPanic is nil, this is equivalent to:
+//
+// defer Expect(Panic()).DidNotOccur()
+//
+// Should you need to test for an actual panic(nil), use:
+//
+// defer Expect(NilPanic()).DidOccur()
+//
+// Or, in a table-driven test, specify an expected recovery value of
+// &runtime.PanicNilError{}.
+func Panic(r ...any) panics.Expected {
+ switch len(r) {
+ case 0:
+ return panics.Expected{}
+ case 1:
+ if r[0] == nil {
+ return panics.Expected{R: opt.NoPanicExpected(true)}
+ }
+ return panics.Expected{R: r[0]}
+ }
+
+ T().Helper()
+ test.Invalid(fmt.Sprintf("Panic: expected at most one argument, got %d", len(r)))
+
+ return panics.Expected{}
+}
diff --git a/match-errors-nil-and-panics_test.go b/match-errors-nil-and-panics_test.go
new file mode 100644
index 0000000..d20a9c9
--- /dev/null
+++ b/match-errors-nil-and-panics_test.go
@@ -0,0 +1,218 @@
+package test_test
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "runtime"
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/matchers/panics"
+ "github.com/blugnu/test/opt"
+ "github.com/blugnu/test/test"
+)
+
+// MARK: errors
+
+type MyError struct {
+ msg string
+}
+
+func (e MyError) Error() string {
+ return e.msg
+}
+
+func TestBeError(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "error is of expected type",
+ Act: func() {
+ err := fmt.Errorf("an error occurred: %w", MyError{"invalid argument"})
+ Expect(err).To(BeError[MyError]())
+ },
+ },
+
+ {Scenario: "error is not of expected type",
+ Act: func() {
+ err := errors.New("some other error")
+ Expect(err).To(BeError[MyError]())
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected: test_test.MyError",
+ "got : *errors.errorString",
+ )
+ },
+ },
+
+ {Scenario: "nil is not error",
+ Act: func() { Expect(error(nil)).To(BeError[error]()) },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected: error",
+ "got : nil",
+ )
+ },
+ },
+ }...))
+}
+
+// MARK: nil
+
+func TestExpect_IsNil(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "nil",
+ Act: func() { var subject any; Expect(subject).IsNil() },
+ },
+ {Scenario: "nil error",
+ Act: func() { var err error; Expect(err).IsNil() },
+ },
+
+ {Scenario: "non-nil error",
+ Act: func() { Expect(errors.New("error")).IsNil() },
+ Assert: func(result *R) {
+ result.Expect("expected nil, got error: error")
+ },
+ },
+ {Scenario: "non-nilable subject",
+ Act: func() { Expect(0).IsNil() },
+ Assert: func(result *R) {
+ result.ExpectInvalid(
+ "nilness.Matcher: values of type 'int' are not nilable",
+ )
+ },
+ },
+ {Scenario: "with custom failure report",
+ Act: func() {
+ Expect(errors.New("not nil")).IsNil(opt.FailureReport(func(a ...any) []string {
+ return []string{"custom failure report"}
+ }))
+ },
+ Assert: func(result *R) {
+ result.Expect("custom failure report")
+ },
+ },
+ }...))
+}
+
+func TestExpect_IsNotNil(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "non-nil error",
+ Act: func() { Expect(errors.New("error")).IsNotNil() },
+ },
+ {Scenario: "non-nilable subject",
+ Act: func() { Expect(0).IsNotNil() },
+ },
+
+ {Scenario: "nil",
+ Act: func() { var subject any; Expect(subject).IsNotNil() },
+ Assert: func(result *R) {
+ result.Expect("expected not nil")
+ },
+ },
+ {Scenario: "with custom failure report",
+ Act: func() {
+ var subject any
+ Expect(subject).IsNotNil(opt.FailureReport(func(a ...any) []string {
+ return []string{"custom failure report"}
+ }))
+ },
+ Assert: func(result *R) {
+ result.Expect("custom failure report")
+ },
+ },
+ }...))
+}
+
+// MARK: panics
+
+func TestNilPanic(t *testing.T) {
+ With(t)
+
+ result := NilPanic()
+ Expect(result).To(Equal(panics.Expected{R: &runtime.PanicNilError{}}))
+}
+
+func TestPanic(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "with no args",
+ Act: func() {
+ result := Panic()
+ Expect(result).To(Equal(panics.Expected{}))
+ },
+ },
+ {Scenario: "with nil arg",
+ Act: func() {
+ result := Panic(nil)
+ Expect(result).To(Equal(panics.Expected{R: opt.NoPanicExpected(true)}))
+ },
+ },
+ {Scenario: "with non-nil arg",
+ Act: func() {
+ result := Panic("some string")
+ Expect(result).To(Equal(panics.Expected{R: "some string"}))
+ },
+ },
+ {Scenario: "with multiple args",
+ Act: func() {
+ result := Panic("one", "two")
+ Expect(result).To(Equal(panics.Expected{}))
+ },
+ Assert: func(result *R) {
+ result.ExpectInvalid("Panic: expected at most one argument, got 2")
+ },
+ },
+ }...))
+}
+
+// =================================================================
+// MARK: examples
+// =================================================================
+
+func ExampleBeError() {
+ test.Example()
+
+ err := fmt.Errorf("an error occurred: %w", MyError{"invalid argument"})
+
+ // this test will pass:
+ Expect(err).To(BeError[MyError]())
+ Expect(err).ToNot(BeError[*fs.PathError]())
+
+ // these tests will fail:
+ Expect(error(nil)).To(BeError[error]())
+ Expect(err).To(BeError[*fs.PathError]())
+
+ // Output:
+ // expected: error
+ // got : nil
+ //
+ // expected: *fs.PathError
+ // got : *fmt.wrapError
+}
+
+func ExamplePanic() {
+ test.Example()
+
+ // panic tests must be deferred so that the panic can be recovered;
+ //
+ // a stack trace is included by default but is disabled here to
+ // avoid breaking the example output
+ defer Expect(Panic("some string")).DidOccur(opt.NoStackTrace())
+
+ // cause a panic; note that the value is different to the value
+ // expected to be recovered, so the test will fail
+ panic("some other string")
+
+ // Output:
+ // unexpected panic:
+ // expected : string("some string")
+ // recovered: string("some other string")
+}
diff --git a/expectationsWereMet.go b/match-expectations-were-met.go
similarity index 100%
rename from expectationsWereMet.go
rename to match-expectations-were-met.go
diff --git a/expectationsWereMet_test.go b/match-expectations-were-met_test.go
similarity index 100%
rename from expectationsWereMet_test.go
rename to match-expectations-were-met_test.go
diff --git a/match-length.go b/match-length.go
new file mode 100644
index 0000000..7d30119
--- /dev/null
+++ b/match-length.go
@@ -0,0 +1,28 @@
+package test
+
+import (
+ "github.com/blugnu/test/matchers/length"
+)
+
+// HaveLen returns a matcher that checks if the value has len() equal to n.
+//
+// The returned matcher is an [AnyMatcher] that must be used with
+// [expectation.Should] or [expectation.ShouldNot] expectations. The matcher
+// supports values of any type that is compatible with the built-in len()
+// function. That is:
+//
+// - array
+// - channel
+// - map
+// - slice
+// - string
+//
+// A nil value of any of these types is considered to have a length of 0.
+//
+// If the value is of any other type, the test fails as invalid, with a
+// message similar to:
+//
+// `length.Matcher: requires a value that is a string, slice, channel, or map: got `
+func HaveLen(n int) *length.Matcher {
+ return &length.Matcher{Length: n}
+}
diff --git a/match-length_test.go b/match-length_test.go
new file mode 100644
index 0000000..9cd2b16
--- /dev/null
+++ b/match-length_test.go
@@ -0,0 +1,72 @@
+package test_test
+
+import (
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/test"
+)
+
+func TestHaveLen(t *testing.T) {
+ With(t)
+
+ // HaveLen returns a matcher from the matchers/length package which is
+ // thoroughly tested in that package; here we provide a simple test to
+ // verify that the HaveLen factory function is wired up correctly.
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "slice with items",
+ Act: func() {
+ sut := []int{1, 2}
+ Expect(sut).Should(HaveLen(2))
+ },
+ },
+ {Scenario: "nil slice",
+ Act: func() {
+ var sut []int
+ Expect(sut).Should(HaveLen(0))
+ },
+ },
+ }...))
+}
+
+// =================================================================
+// MARK: examples
+// =================================================================
+
+func ExampleHaveLen() {
+ test.Example()
+
+ // these tests all pass:
+ Expect([]int{}).Should(HaveLen(0))
+ Expect(map[int]string{}).Should(HaveLen(0))
+ Expect("").Should(HaveLen(0))
+ Expect([0]int{}).Should(HaveLen(0))
+
+ var ch chan int
+ Expect(ch, "nil channel").Should(HaveLen(0))
+
+ var ns []int
+ Expect(ns, "nil slice").Should(HaveLen(0))
+
+ var nm map[int]struct{}
+ Expect(nm, "nil map").Should(HaveLen(0))
+
+ // these tests all fail:
+ Expect([]int{1, 2, 3}, "slice").Should(HaveLen(2))
+ Expect(map[int]struct{}{1: {}, 2: {}, 3: {}}, "map").Should(HaveLen(2))
+ Expect("abc", "string").Should(HaveLen(2))
+
+ // Output:
+ // slice:
+ // expected: len() == 2
+ // got : len() == 3
+ //
+ // map:
+ // expected: len() == 2
+ // got : len() == 3
+ //
+ // string:
+ // expected: len() == 2
+ // got : len() == 3
+}
diff --git a/map.go b/match-maps.go
similarity index 100%
rename from map.go
rename to match-maps.go
diff --git a/map_test.go b/match-maps_test.go
similarity index 100%
rename from map_test.go
rename to match-maps_test.go
diff --git a/ordered.go b/match-ordered.go
similarity index 100%
rename from ordered.go
rename to match-ordered.go
diff --git a/ordered_test.go b/match-ordered_test.go
similarity index 100%
rename from ordered_test.go
rename to match-ordered_test.go
diff --git a/slices.go b/match-slices.go
similarity index 100%
rename from slices.go
rename to match-slices.go
diff --git a/slices_test.go b/match-slices_test.go
similarity index 100%
rename from slices_test.go
rename to match-slices_test.go
diff --git a/string.go b/match-string.go
similarity index 100%
rename from string.go
rename to match-string.go
diff --git a/string_test.go b/match-string_test.go
similarity index 100%
rename from string_test.go
rename to match-string_test.go
diff --git a/match-typecheck.go b/match-typecheck.go
new file mode 100644
index 0000000..490e300
--- /dev/null
+++ b/match-typecheck.go
@@ -0,0 +1,11 @@
+package test
+
+import "github.com/blugnu/test/matchers/typecheck"
+
+// BeOfType returns a matcher that checks if the value is of the expected type.
+//
+// To perform further tests on a value that passes this check using a strongly
+// typed value of the expected type, use expect.Type or require.Type instead.
+func BeOfType[T any]() typecheck.Matcher[T] {
+ return typecheck.Matcher[T]{}
+}
diff --git a/match-typecheck_test.go b/match-typecheck_test.go
new file mode 100644
index 0000000..257999e
--- /dev/null
+++ b/match-typecheck_test.go
@@ -0,0 +1,64 @@
+package test_test
+
+import (
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/test"
+)
+
+func TestShould_BeOfType(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "int is int",
+ Act: func() { Expect(42).Should(BeOfType[int]()) },
+ },
+ {Scenario: "int is not string",
+ Act: func() { Expect(42).Should(BeOfType[string]()) },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected type: string",
+ "got : int",
+ )
+ },
+ },
+ }...))
+}
+
+func TestShouldNot_BeOfType(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "int is not string",
+ Act: func() { Expect(42).ShouldNot(BeOfType[string]()) },
+ },
+ {Scenario: "int is string",
+ Act: func() { Expect(42).ShouldNot(BeOfType[int]()) },
+ Assert: func(result *R) {
+ result.Expect(
+ "should not be of type: int",
+ )
+ },
+ },
+ }...))
+}
+
+// =================================================================
+// MARK: examples
+// =================================================================
+
+func ExampleBeOfType() {
+ test.Example()
+
+ var i int = 42
+
+ Expect(i).Should(BeOfType[int]()) // passes
+ Expect(i).ShouldNot(BeOfType[string]()) // passes
+
+ Expect(i).Should(BeOfType[bool]()) // fails
+
+ // Output:
+ // expected type: bool
+ // got : int
+}
diff --git a/matchers/bools/bools_test.go b/matchers/bools/bools_test.go
index c71815f..b778f21 100644
--- a/matchers/bools/bools_test.go
+++ b/matchers/bools/bools_test.go
@@ -18,7 +18,7 @@ func TestBooleans(t *testing.T) {
Act: func() { Expect(true).To(BeFalse()) },
Assert: func(result *R) {
result.Expect(
- "expected false, got true",
+ "expected false",
)
},
},
@@ -42,7 +42,7 @@ func TestBooleans(t *testing.T) {
Act: func() { Expect(false).To(BeTrue()) },
Assert: func(result *R) {
result.Expect(
- "expected true, got false",
+ "expected true",
)
},
},
diff --git a/matchers/bools/equal.go b/matchers/bools/equal.go
index f7ee67f..fe1557f 100644
--- a/matchers/bools/equal.go
+++ b/matchers/bools/equal.go
@@ -6,7 +6,8 @@ import (
"github.com/blugnu/test/opt"
)
-// implements the Matcher interface for testing expected bool values.
+// BooleanMatcher implements the Matcher interface for testing expected
+// bool values.
type BooleanMatcher struct {
Expected bool
}
@@ -20,5 +21,5 @@ func (m BooleanMatcher) OnTestFailure(got bool, opts ...any) string {
if opt.IsSet(opts, opt.ToNotMatch(true)) {
return fmt.Sprintf("did not expect %v", m.Expected)
}
- return fmt.Sprintf("expected %v, got %v", m.Expected, got)
+ return fmt.Sprintf("expected %v", m.Expected)
}
diff --git a/matchers/nilness/matcher.go b/matchers/nilness/matcher.go
index 9a7b164..3956411 100644
--- a/matchers/nilness/matcher.go
+++ b/matchers/nilness/matcher.go
@@ -64,6 +64,9 @@ func (m Matcher) OnTestFailure(subject any, opts ...any) string {
expect := "expected nil"
if opt.IsSet(opts, opt.ToNotMatch(true)) {
expect = "expected not nil"
+ if subject == nil {
+ return expect
+ }
}
switch got := subject.(type) {
diff --git a/matchers/nilness/matcher_test.go b/matchers/nilness/matcher_test.go
index 655d9f2..de114e4 100644
--- a/matchers/nilness/matcher_test.go
+++ b/matchers/nilness/matcher_test.go
@@ -119,7 +119,7 @@ func TestShouldNot_BeNil(t *testing.T) {
{Scenario: "nil is not nil",
Act: func() { Expect((any)(nil)).ShouldNot(BeNil()) },
Assert: func(result *R) {
- result.Expect("expected not nil, got nil")
+ result.Expect("expected not nil")
},
},
{Scenario: "int is not nil",
diff --git a/matchers/panics/matchRecovered.go b/matchers/panics/match-recovered.go
similarity index 100%
rename from matchers/panics/matchRecovered.go
rename to matchers/panics/match-recovered.go
diff --git a/matchers/panics/matchRecovered_test.go b/matchers/panics/match-recovered_test.go
similarity index 98%
rename from matchers/panics/matchRecovered_test.go
rename to matchers/panics/match-recovered_test.go
index cd6e715..0c209c2 100644
--- a/matchers/panics/matchRecovered_test.go
+++ b/matchers/panics/match-recovered_test.go
@@ -6,6 +6,7 @@ import (
"testing"
. "github.com/blugnu/test"
+ "github.com/blugnu/test/test"
)
func TestPanic_DidOccur(t *testing.T) {
@@ -29,7 +30,7 @@ func TestPanic_DidOccur(t *testing.T) {
panic("panics with string")
},
Assert: func(result *R) {
- result.Expect(TestPanicked, "panics with string")
+ result.Expect(test.Panicked, "panics with string")
},
},
{Scenario: "panicked with unexpected error",
diff --git a/matchers/slices/appendToReport.go b/matchers/slices/append-to-report.go
similarity index 100%
rename from matchers/slices/appendToReport.go
rename to matchers/slices/append-to-report.go
diff --git a/matchers/slices/appendToReport_test.go b/matchers/slices/append-to-report_test.go
similarity index 100%
rename from matchers/slices/appendToReport_test.go
rename to matchers/slices/append-to-report_test.go
diff --git a/matchers/slices/containsItem.go b/matchers/slices/contains-item.go
similarity index 96%
rename from matchers/slices/containsItem.go
rename to matchers/slices/contains-item.go
index d6abfcd..9d87f60 100644
--- a/matchers/slices/containsItem.go
+++ b/matchers/slices/contains-item.go
@@ -8,7 +8,7 @@ import (
"github.com/blugnu/test/opt"
)
-// ContainsItemsMatcher is a matcher for []T that will match the []T
+// ContainsItemMatcher is a matcher for []T that will match the []T
// if it contains at least one element that is equal to the expected
// element.
type ContainsItemMatcher[T any] struct {
diff --git a/matchers/slices/containsItem_test.go b/matchers/slices/contains-item_test.go
similarity index 100%
rename from matchers/slices/containsItem_test.go
rename to matchers/slices/contains-item_test.go
diff --git a/matchers/slices/containsItems.go b/matchers/slices/contains-items.go
similarity index 100%
rename from matchers/slices/containsItems.go
rename to matchers/slices/contains-items.go
diff --git a/matchers/slices/containsItems_test.go b/matchers/slices/contains-items_test.go
similarity index 100%
rename from matchers/slices/containsItems_test.go
rename to matchers/slices/contains-items_test.go
diff --git a/matchers/slices/containsSlice.go b/matchers/slices/contains-slice.go
similarity index 95%
rename from matchers/slices/containsSlice.go
rename to matchers/slices/contains-slice.go
index 0c0eecb..76242fe 100644
--- a/matchers/slices/containsSlice.go
+++ b/matchers/slices/contains-slice.go
@@ -32,7 +32,7 @@ func (m ContainsSliceMatcher[T]) Match(got []T, opts ...any) bool {
return slice[T](got).containsSlice(m.Expected, cmp)
}
-// TestFailure returns a report of the failure for the matcher.
+// OnTestFailure returns a report of the failure for the matcher.
func (m ContainsSliceMatcher[T]) OnTestFailure(got []T, opts ...any) []string {
result := make([]string, 0, 2+len(got)+len(m.Expected))
cond := "containing slice"
diff --git a/matchers/slices/containsSlice_test.go b/matchers/slices/contains-slice_test.go
similarity index 100%
rename from matchers/slices/containsSlice_test.go
rename to matchers/slices/contains-slice_test.go
diff --git a/matchers/slices/equalsSlice.go b/matchers/slices/equals-slice.go
similarity index 96%
rename from matchers/slices/equalsSlice.go
rename to matchers/slices/equals-slice.go
index e5e1a51..aff1ce1 100644
--- a/matchers/slices/equalsSlice.go
+++ b/matchers/slices/equals-slice.go
@@ -44,7 +44,7 @@ func (m EqualMatcher[T]) Match(got []T, opts ...any) bool {
return slice[T](got).containsSlice(m.Expected, cmp)
}
-// TestFailure returns a report of the failure for the matcher.
+// OnTestFailure returns a report of the failure for the matcher.
func (m EqualMatcher[T]) OnTestFailure(got []T, opts ...any) []string {
result := make([]string, 0, 2+len(got)+len(m.Expected))
cond := "equal to"
diff --git a/matchers/slices/equalsSlice_test.go b/matchers/slices/equals-slice_test.go
similarity index 100%
rename from matchers/slices/equalsSlice_test.go
rename to matchers/slices/equals-slice_test.go
diff --git a/matchers/typecheck/match-error.go b/matchers/typecheck/match-error.go
new file mode 100644
index 0000000..887312c
--- /dev/null
+++ b/matchers/typecheck/match-error.go
@@ -0,0 +1,65 @@
+package typecheck
+
+import (
+ "errors"
+
+ "github.com/blugnu/test/internal"
+ "github.com/blugnu/test/opt"
+)
+
+type MatchError[E error] struct {
+ Target E
+}
+
+// Match checks that the value satisfies [errors.As] for an error of type
+// E. If the value is not of the expected type and does not wrap an error
+// of that type, the test fails.
+//
+// # Supported Options
+//
+// opt.QuotedStrings(bool) // determines whether any non-nil string
+// // values are quoted in any test failure
+// // report. The default is false (string
+// // values are quoted).
+// //
+// // If the value is not a string type this
+// // option has no effect.
+//
+// opt.FailureReport(func) // a function that returns a custom test
+// // failure report if the test fails.
+//
+// opt.OnFailure(string) // a string to output as the failure
+// // report if the test fails.
+func (m MatchError[E]) Match(got error, opts ...any) bool {
+ return errors.As(got, &m.Target)
+}
+
+// OnTestFailure returns a failure report indicating that the actual value
+// was not of the expected type.
+//
+// If the ToNotMatch option is set, the report indicates that the value
+// should not have been of the expected type.
+func (m MatchError[E]) OnTestFailure(got any, opts ...any) []string {
+ expectedType := internal.TypeName[E]()
+
+ if opt.IsSet(opts, opt.ToNotMatch(true)) {
+ return []string{"should not be of type: " + expectedType}
+ }
+
+ // if got is nil we include this in the report as no error at all
+ // vs an error of the expected type is a significant difference
+ if got == nil {
+ return []string{
+ "expected: " + expectedType,
+ "got : nil",
+ }
+ }
+
+ // the type of got is not reported when not-nil since the desired
+ // error of type E may be in an expected wrapped error; the type of
+ // got is not particularly helpful
+ return []string{
+ "expected: " + expectedType,
+ "got : " + internal.TypeName(got),
+ }
+}
diff --git a/matchers/typecheck/match-error_test.go b/matchers/typecheck/match-error_test.go
new file mode 100644
index 0000000..f3385eb
--- /dev/null
+++ b/matchers/typecheck/match-error_test.go
@@ -0,0 +1,82 @@
+package typecheck_test
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ . "github.com/blugnu/test"
+)
+
+type MyError struct {
+ msg string
+}
+
+func (e MyError) Error() string {
+ return e.msg
+}
+
+func TestTo_BeError(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "error is of expected type",
+ Act: func() {
+ err := fmt.Errorf("an error occurred: %w", MyError{"invalid argument"})
+ Expect(err).To(BeError[MyError]())
+ },
+ },
+
+ {Scenario: "error is not of expected type",
+ Act: func() {
+ err := errors.New("some other error")
+ Expect(err).To(BeError[MyError]())
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected: typecheck_test.MyError",
+ "got : *errors.errorString",
+ )
+ },
+ },
+
+ {Scenario: "nil is not error",
+ Act: func() { Expect(error(nil)).To(BeError[error]()) },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected: error",
+ "got : nil",
+ )
+ },
+ },
+ }...))
+}
+
+func TestToNot_BeError(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "error is not of the expected type",
+ Act: func() {
+ err := errors.New("some other error")
+ Expect(err).ToNot(BeError[MyError]())
+ },
+ },
+
+ {Scenario: "error is of the expected type",
+ Act: func() {
+ err := fmt.Errorf("an error occurred: %w", MyError{"invalid argument"})
+ Expect(err).ToNot(BeError[MyError]())
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "should not be of type: typecheck_test.MyError",
+ )
+ },
+ },
+
+ {Scenario: "nil is not of any error type",
+ Act: func() { Expect(error(nil)).ToNot(BeError[error]()) },
+ },
+ }...))
+}
diff --git a/matchers/typecheck/matcher.go b/matchers/typecheck/matcher.go
index 44b8048..8184b89 100644
--- a/matchers/typecheck/matcher.go
+++ b/matchers/typecheck/matcher.go
@@ -2,8 +2,8 @@ package typecheck
import (
"fmt"
- "reflect"
+ "github.com/blugnu/test/internal"
"github.com/blugnu/test/opt"
)
@@ -34,25 +34,7 @@ func (m Matcher[T]) Match(got any, opts ...any) bool {
}
func (m Matcher[T]) OnTestFailure(got any, opts ...any) []string {
- var (
- expected T
- expectedType = fmt.Sprintf("%T", expected)
- )
-
- // if we could not determine the type of the expected value using the
- // zero value of the type, we can use a dummy function and reflect
- // the type of the first argument to that function.
- //
- // Q: why not just use the dummy func technique every time?
- // A: because it is more expensive than using the zero value, and
- // using the zero value provides a more precise (package-qualified)
- // type name
- if expectedType == "" {
- fn := func(T) { /* NO-OP */ }
- fn(expected) // ensures that the dummy function is covered by tests
-
- expectedType = reflect.TypeOf(fn).In(0).Name()
- }
+ expectedType := internal.TypeName[T]()
if opt.IsSet(opts, opt.ToNotMatch(true)) {
return []string{"should not be of type: " + expectedType}
diff --git a/mockFn.go b/mock-fn.go
similarity index 99%
rename from mockFn.go
rename to mock-fn.go
index e9fe9bf..552c011 100644
--- a/mockFn.go
+++ b/mock-fn.go
@@ -293,7 +293,7 @@ func (mock *MockFn[A, R]) RecordCall(args ...A) (R, error) {
return result, err
}
-// ExpectedResults returns an error if any expectations were not met; otherwise nil.
+// ExpectationsWereMet returns an error if any expectations were not met; otherwise nil.
//
// This method is typically called at the end of a test to ensure that all expected
// calls were made.
diff --git a/mockFn_test.go b/mock-fn_test.go
similarity index 100%
rename from mockFn_test.go
rename to mock-fn_test.go
diff --git a/nilness.go b/nilness.go
deleted file mode 100644
index d2f2572..0000000
--- a/nilness.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package test
-
-import "github.com/blugnu/test/matchers/nilness"
-
-// IsNil checks that the value of the expectation is nil. If the
-// value is not nil, the test fails. If the value is nil, the test
-// passes.
-//
-// If the value being tested does not support a nil value the test
-// will fail and produce a report similar to:
-//
-// test.IsNil: values of type '' are not nilable
-//
-// # Supported Options
-//
-// opt.QuotedStrings(bool) // determines whether any non-nil string
-// // values are quoted in any test failure
-// // report. The default is false (string
-// // values are quoted).
-// //
-// // If the value is not a string type this
-// // option has no effect.
-//
-// opt.FailureReport(func) // a function that returns a custom test
-// // failure report if the test fails.
-//
-// opt.OnFailure(string) // a string to output as the failure
-// // report if the test fails.
-func (e expectation[T]) IsNil(opts ...any) {
- e.t.Helper()
- e.Should(BeNil(), opts...)
-}
-
-// IsNotNil checks that a specified value is not nil. If the value
-// is not nil, the test fails. If the value is nil, the test passes.
-//
-// NOTE: If the value being tested does not support a nil value the
-// test will pass. This is to allow for testing values that may be
-// nilable or non-nilable.
-//
-// # Supported Options
-//
-// opt.FailureReport(func) // a function that returns a custom test
-// // failure report if the test fails.
-//
-// opt.OnFailure(string) // a string to output as the failure
-// // report if the test fails.
-func (e expectation[T]) IsNotNil(opts ...any) {
- e.t.Helper()
- e.ShouldNot(BeNil(), opts...)
-}
-
-// BeNil returns a matcher that checks if the value is nil.
-//
-// The returned matcher is an `AnyMatcher` that may only be used
-// with the `Should()` method, or with `To()` where the subject
-// is of formal type any.
-//
-// If used in a To() or Should() test with a subject that is not
-// nilable, the test fails with a message similar to:
-//
-// test.BeNil: values of type '' are not nilable
-//
-// If used with ToNo() or ShouldNot() a non-nilable subject will
-// pass the test.
-//
-// # Supported Options
-//
-// opt.QuotedStrings(bool) // determines whether any non-nil string
-// // values are quoted in any test failure
-// // report. The default is false (string
-// // values are quoted).
-// //
-// // If the value is not a string type this
-// // option has no effect.
-//
-// opt.FailureReport(func) // a function that returns a custom test
-// // failure report if the test fails.
-//
-// opt.OnFailure(string) // a string to output as the failure
-// // report if the test fails.
-func BeNil() nilness.Matcher {
- return nilness.Matcher{}
-}
diff --git a/nilness_test.go b/nilness_test.go
deleted file mode 100644
index c57a4a3..0000000
--- a/nilness_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package test_test
-
-import (
- "errors"
- "testing"
-
- . "github.com/blugnu/test"
- "github.com/blugnu/test/opt"
-)
-
-func TestExpect_IsNil(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "nil",
- Act: func() { var subject any; Expect(subject).IsNil() },
- },
- {Scenario: "nil error",
- Act: func() { var err error; Expect(err).IsNil() },
- },
-
- {Scenario: "non-nil error",
- Act: func() { Expect(errors.New("error")).IsNil() },
- Assert: func(result *R) {
- result.Expect("expected nil, got error: error")
- },
- },
- {Scenario: "non-nilable subject",
- Act: func() { Expect(0).IsNil() },
- Assert: func(result *R) {
- result.ExpectInvalid(
- "nilness.Matcher: values of type 'int' are not nilable",
- )
- },
- },
- {Scenario: "with custom failure report",
- Act: func() {
- Expect(errors.New("not nil")).IsNil(opt.FailureReport(func(a ...any) []string {
- return []string{"custom failure report"}
- }))
- },
- Assert: func(result *R) {
- result.Expect("custom failure report")
- },
- },
- }...))
-}
-
-func TestExpect_IsNotNil(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "non-nil error",
- Act: func() { Expect(errors.New("error")).IsNotNil() },
- },
- {Scenario: "non-nilable subject",
- Act: func() { Expect(0).IsNotNil() },
- },
-
- {Scenario: "nil",
- Act: func() { var subject any; Expect(subject).IsNotNil() },
- Assert: func(result *R) {
- result.Expect("expected not nil, got nil")
- },
- },
- {Scenario: "with custom failure report",
- Act: func() {
- var subject any
- Expect(subject).IsNotNil(opt.FailureReport(func(a ...any) []string {
- return []string{"custom failure report"}
- }))
- },
- Assert: func(result *R) {
- result.Expect("custom failure report")
- },
- },
- }...))
-}
diff --git a/opt/anyAreSet.go b/opt/any-are-set.go
similarity index 100%
rename from opt/anyAreSet.go
rename to opt/any-are-set.go
diff --git a/opt/anyAreSet_test.go b/opt/any-are-set_test.go
similarity index 100%
rename from opt/anyAreSet_test.go
rename to opt/any-are-set_test.go
diff --git a/opt/failureReport.go b/opt/failure-report.go
similarity index 91%
rename from opt/failureReport.go
rename to opt/failure-report.go
index b76d406..f8fb4a6 100644
--- a/opt/failureReport.go
+++ b/opt/failure-report.go
@@ -12,7 +12,7 @@ package opt
// }))
type FailureReport func(...any) []string
-// TestFailure implements the TestFailure(...any) []string interface,
+// OnTestFailure implements the TestFailure(...any) []string interface,
// calling the function with the provided options.
func (f FailureReport) OnTestFailure(opts ...any) []string {
return f(opts...)
diff --git a/opt/failureReport_test.go b/opt/failure-report_test.go
similarity index 100%
rename from opt/failureReport_test.go
rename to opt/failure-report_test.go
diff --git a/opt/get.go b/opt/get.go
index 5fb458c..97afcb3 100644
--- a/opt/get.go
+++ b/opt/get.go
@@ -1,7 +1,7 @@
package opt
-// Get extracts an option of a given type from a variadic list of options.
-// Only the first value of type T is significant; any additional T values
+// Get extracts an option of a given type from a slice of options. Only the
+// first value of type T in the slice is significant; any additional T values
// are ignored.
//
// The function returns the option value and true if an option of the
@@ -14,5 +14,6 @@ func Get[T any](opts []any) (T, bool) {
}
}
- return *new(T), false
+ var zero T
+ return zero, false
}
diff --git a/opt/isSet.go b/opt/is-set.go
similarity index 100%
rename from opt/isSet.go
rename to opt/is-set.go
diff --git a/opt/isSet_test.go b/opt/is-set_test.go
similarity index 100%
rename from opt/isSet_test.go
rename to opt/is-set_test.go
diff --git a/opt/opts.go b/opt/opts.go
index 28130f7..f90911d 100644
--- a/opt/opts.go
+++ b/opt/opts.go
@@ -31,8 +31,8 @@ type IgnoreReport bool
// see also: Require()
type IsRequired bool
-// NoPanic is an internal option used as a sentinel recover value by the panic
-// testing mechanism to signal that a panic is NOT expected to occur
+// NoPanicExpected is an internal option used as a sentinel recover value by
+// the panic testing mechanism to signal that a panic is NOT expected to occur
type NoPanicExpected bool
// PrefixInlineWithFirstItem may be used to indicate that the first item
diff --git a/opt/opts_test.go b/opt/opts_test.go
index b264eab..d11488c 100644
--- a/opt/opts_test.go
+++ b/opt/opts_test.go
@@ -4,6 +4,7 @@ import (
"testing"
. "github.com/blugnu/test"
+ "github.com/blugnu/test/expect"
"github.com/blugnu/test/opt"
)
@@ -12,7 +13,7 @@ func TestAnyOrder(t *testing.T) {
result := opt.AnyOrder()
- if value, ok := ExpectType[opt.ExactOrder](result); ok {
+ if value, ok := expect.Type[opt.ExactOrder](result); ok {
Expect(value).To(Equal(opt.ExactOrder(false)))
}
}
@@ -22,7 +23,7 @@ func TestRequired(t *testing.T) {
result := opt.Required()
- if value, ok := ExpectType[opt.IsRequired](result); ok {
+ if value, ok := expect.Type[opt.IsRequired](result); ok {
Expect(value).To(Equal(opt.IsRequired(true)))
}
}
@@ -32,7 +33,7 @@ func TestNoStackTrace(t *testing.T) {
result := opt.NoStackTrace()
- if value, ok := ExpectType[opt.StackTrace](result); ok {
+ if value, ok := expect.Type[opt.StackTrace](result); ok {
Expect(value).To(Equal(opt.StackTrace(false)))
}
}
@@ -42,7 +43,7 @@ func TestUnquotedStrings(t *testing.T) {
result := opt.UnquotedStrings()
- if value, ok := ExpectType[opt.QuotedStrings](result); ok {
+ if value, ok := expect.Type[opt.QuotedStrings](result); ok {
Expect(value).To(Equal(opt.QuotedStrings(false)))
}
}
diff --git a/opt/valueAsString.go b/opt/value-as-string.go
similarity index 80%
rename from opt/valueAsString.go
rename to opt/value-as-string.go
index a292e64..42efff3 100644
--- a/opt/valueAsString.go
+++ b/opt/value-as-string.go
@@ -5,9 +5,10 @@ import (
"reflect"
)
-// FormatString formats a string according to the provided options.
+// ValueAsString converts a value to string representation, according to the
+// provided options.
//
-// This function should be used in TestFailure methods to format expected and
+// This function should be used in test failure methods to format expected and
// actual values consistently with other matcher failure reports.
//
// Supported options:
diff --git a/opt/valueAsString_test.go b/opt/value-as-string_test.go
similarity index 100%
rename from opt/valueAsString_test.go
rename to opt/value-as-string_test.go
diff --git a/panic.go b/panic.go
deleted file mode 100644
index 1f35177..0000000
--- a/panic.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package test
-
-import (
- "fmt"
- "runtime"
-
- "github.com/blugnu/test/matchers/panics"
- "github.com/blugnu/test/opt"
- "github.com/blugnu/test/test"
-)
-
-// Panic returns an expectation subject that can be used to test whether a
-// panic has occurred, optionally identifying a value that should match the
-// value recovered from the expected panic.
-//
-// NOTE: At most ONE panic test should be expected per function. In addition,
-// extreme care should be exercised when combining panic tests with other
-// deferred recover() calls as these will also interfere with a panic test
-// (or vice versa).
-//
-// # Usage
-//
-// - If called with no arguments, any panic will satisfy the expectation,
-// regardless of the value recovered.
-//
-// - If called with a single argument, it will expect to recover a panic that
-// recovers that value (unless the argument is nil; see The Panic(nil)
-// Special Case, below)
-//
-// - If called with > 1 argument, the test will be failed as invalid.
-//
-// # The Panic(nil) Special Case
-//
-// Panic(nil) is a special case that is equivalent to "no panic expected".
-// This is motivated by table-driven tests to avoid having to write conditional
-// code to handle test cases where a panic is expected vs those where not.
-//
-// Treating Panic(nil) as "no panic expected" allows you to write:
-//
-// defer Expect(Panic(testcase.expectedPanic)).DidOccur()
-//
-// When testcase.expectedPanic is nil, this is equivalent to:
-//
-// defer Expect(Panic()).DidNotOccur()
-//
-// Should you need to test for an actual panic(nil), use:
-//
-// defer Expect(NilPanic()).DidOccur()
-//
-// Or, in a table-driven test, specify an expected recovery value of
-// &runtime.PanicNilError{}.
-func Panic(r ...any) panics.Expected {
- switch len(r) {
- case 0:
- return panics.Expected{}
- case 1:
- if r[0] == nil {
- return panics.Expected{R: opt.NoPanicExpected(true)}
- }
- return panics.Expected{R: r[0]}
- }
-
- T().Helper()
- test.Invalid(fmt.Sprintf("Panic: expected at most one argument, got %d", len(r)))
-
- return panics.Expected{}
-}
-
-// NilPanic returns an expectation that a panic will occur that recovers
-// a *runtime.PanicNilError.
-//
-// Panic(nil) is syntactic sugar for "no panic expected", to simplify
-// table-drive tests where each test case may or may not expect a panic,
-// enabling the use of a single Expect() call.
-//
-// i.e. instead of writing:
-//
-// if testcase.expectedPanic == nil {
-// defer Expect(Panic()).DidNotOccur()
-// } else {
-// defer Expect(Panic(testcase.expectedPanic)).DidOccur()
-// }
-//
-// you can write:
-//
-// defer Expect(Panic(testcase.expectedPanic)).DidOccur()
-//
-// When testcase.expectedPanic is nil, this is equivalent to:
-//
-// defer Expect(Panic()).DidNotOccur()
-//
-// Without having to write conditional code to handle the different
-// expectations.
-//
-// # Testing for a nil panic
-//
-// In the unlikely event that you specifically need to test for a
-// panic(nil), you can use the NilPanic() function, which will
-// create an expectation for a panic that recovers a *runtime.PanicNilError.
-//
-// see: https://go.dev/blog/compat#expanded-godebug-support-in-go-121
-func NilPanic() panics.Expected {
- // This is a convenience function to create an expectation for a nil panic.
- // It is equivalent to Panic(nil).
- return panics.Expected{R: &runtime.PanicNilError{}}
-}
diff --git a/panic_test.go b/panic_test.go
deleted file mode 100644
index f81a7d8..0000000
--- a/panic_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package test_test
-
-import (
- "runtime"
- "testing"
-
- . "github.com/blugnu/test"
- "github.com/blugnu/test/matchers/panics"
- "github.com/blugnu/test/opt"
- "github.com/blugnu/test/test"
-)
-
-func TestNilPanic(t *testing.T) {
- With(t)
-
- result := NilPanic()
- Expect(result).To(Equal(panics.Expected{R: &runtime.PanicNilError{}}))
-}
-
-func TestPanic(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "with no args",
- Act: func() {
- result := Panic()
- Expect(result).To(Equal(panics.Expected{}))
- },
- },
- {Scenario: "with nil arg",
- Act: func() {
- result := Panic(nil)
- Expect(result).To(Equal(panics.Expected{R: opt.NoPanicExpected(true)}))
- },
- },
- {Scenario: "with non-nil arg",
- Act: func() {
- result := Panic("some string")
- Expect(result).To(Equal(panics.Expected{R: "some string"}))
- },
- },
- {Scenario: "with multiple args",
- Act: func() {
- result := Panic("one", "two")
- Expect(result).To(Equal(panics.Expected{}))
- },
- Assert: func(result *R) {
- result.ExpectInvalid("Panic: expected at most one argument, got 2")
- },
- },
- }...))
-}
-
-func ExamplePanic() {
- test.Example()
-
- // a stack trace is included by default, but is disabled for this
- // example to avoid breaking the example output
- defer Expect(Panic("some string")).DidOccur(opt.NoStackTrace())
-
- panic("some other string")
-
- // Output:
- // unexpected panic:
- // expected : string("some string")
- // recovered: string("some other string")
-}
diff --git a/parallel.go b/parallel.go
index 86c94d4..a865603 100644
--- a/parallel.go
+++ b/parallel.go
@@ -1,37 +1,14 @@
package test
import (
- "reflect"
- "testing"
-
"github.com/blugnu/test/internal/testframe"
"github.com/blugnu/test/test"
)
-// isParallel checks if the given TestingT is running in parallel.
-//
-// The underlying type of TestingT must be a *testing.T; any other
-// implementation will return false.
-//
-// For *testing.T values, reflection is used to examine the internal
-// state to determine if it is running in parallel.
-func isParallel(t TestingT) bool {
- tf := T()
- if _, ok := tf.(*testing.T); !ok {
- // tests cannot be parallel if the TestingT is not a *testing.T
- return false
- }
-
- c := reflect.Indirect(reflect.ValueOf(t)).FieldByName("common")
- ip := reflect.Indirect(c).FieldByName("isParallel")
-
- return ip.Bool()
-}
-
// IsParallel returns true if the current test is running in parallel or is a
// sub-test of a parallel test.
func IsParallel() bool {
- return isParallel(T())
+ return testframe.IsParallel()
}
// Parallel establishes a new test frame scheduled for parallel execution.
@@ -58,17 +35,17 @@ func IsParallel() bool {
// Parallel must not be called from a test that is already parallel or with
// a nil argument; in both cases the test will be failed as invalid.
func Parallel(t TestingT) {
- if t == nil {
- if t, ok := testframe.Peek[TestingT](); ok {
- t.Helper()
- }
- test.Invalid("Parallel() cannot be called with nil")
+ // mark t as a helper if possible
+ if t, ok := testframe.Peek[TestingT](); ok {
+ t.Helper()
}
- t.Helper()
+ switch {
+ case t == nil:
+ test.Invalid("Parallel() cannot be called with nil")
- if isParallel(t) {
- test.Invalid("Parallel() cannot be called from a parallel test")
+ case testframe.IsParallel():
+ test.Invalid("Parallel() must not be called from a parallel test")
}
With(t)
diff --git a/parallel_test.go b/parallel_test.go
index c7b8fb8..6968fc6 100644
--- a/parallel_test.go
+++ b/parallel_test.go
@@ -137,7 +137,7 @@ func TestParallelTests(t *testing.T) {
Parallel(T())
})
result.ExpectInvalid(
- "Parallel() cannot be called from a parallel test",
+ "Parallel() must not be called from a parallel test",
)
}))
diff --git a/pkgdocs.go b/pkgdocs.go
new file mode 100644
index 0000000..e71c522
--- /dev/null
+++ b/pkgdocs.go
@@ -0,0 +1,50 @@
+package test
+
+// AnyMatcher is the interface implemented by matchers that can test
+// any type of value. It is used to apply matchers to expectations
+// that are not type-specific.
+//
+// It is preferable to use the Matcher[T] interface for type-safe
+// expectations; AnyMatcher is provided for situations where the
+// compatible types for a matcher cannot be enforced at compile-time.
+//
+// When implementing an AnyMatcher, it is important to ensure that
+// the matcher fails a test if it is not used correctly, i.e. if the
+// matcher is not compatible with the type of the value being tested.
+//
+// An AnyMatcher must be used with the Expect().Should() matching
+// function; they may also be used with Expect(got).To() where the got
+// value is of type `any`, though this is not recommended.
+//
+// NOTE: This interface is not referenced by the test package; the
+// declaration is provided for documentation purposes only.
+type AnyMatcher interface {
+ Match(got any, opts ...any) bool
+}
+
+// Matcher is a generic interface that describes the method set of
+// a type-safe matcher for testing values of a specific type, T.
+//
+// A type-safe matcher is not necessarily generic. A matcher
+// that implements Match(got X, opts ...any) bool, where X is a formal,
+// literal type (i.e. not generic) is also a type-safe matcher.
+//
+// `Matcher[T]` describes the general form of a type-safe matcher.
+//
+// # Generic Matchers
+//
+// A type-safe matcher that implements support for a specific type is
+// limited to only testing values of that explicit type.
+//
+// A generic matcher uses type constraints to specify a set of
+// compatible types for the matcher, enabling that matcher to be used
+// with a variety of types.
+//
+// For example, the equals.Matcher[T comparable] may be used to test,
+// values of any type that is comparable (supports comparison using ==).
+//
+// NOTE: This interface is not referenced by the test package; the
+// declaration is provided for documentation purposes only.
+type Matcher[T any] interface {
+ Match(got T, opts ...any) bool
+}
diff --git a/require/bool.go b/require/bool.go
new file mode 100644
index 0000000..3211f59
--- /dev/null
+++ b/require/bool.go
@@ -0,0 +1,47 @@
+package require
+
+import "github.com/blugnu/test"
+
+// False fails a test if a specified bool is not false. An optional
+// name (string) may be specified to be included in the test report in the
+// event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Require(got).To(Equal(false))
+// Require(got).To(BeFalse())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func False[T ~bool](got T, opts ...any) {
+ test.T().Helper()
+ test.Require(bool(got), opts...).To(test.BeFalse(), opts...)
+}
+
+// True fails and halts execution of the current test if a specified bool
+// is not true. An optional name (string) may be specified to be included
+// in the test report in the event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Expect(got).To(Equal(true), opt.Required())
+// Expect(got).To(BeTrue(), opt.Required())
+// Require(got).To(Equal(true))
+// Require(got).To(BeTrue())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func True[T ~bool](got T, opts ...any) {
+ test.T().Helper()
+ test.Require(bool(got), opts...).To(test.BeTrue(), opts...)
+}
diff --git a/require/bool_test.go b/require/bool_test.go
new file mode 100644
index 0000000..bd63f74
--- /dev/null
+++ b/require/bool_test.go
@@ -0,0 +1,35 @@
+package require_test
+
+import (
+ "github.com/blugnu/test/expect"
+ "github.com/blugnu/test/require"
+ "github.com/blugnu/test/test"
+)
+
+func ExampleFalse() {
+ test.Example()
+
+ a := 1
+
+ // only one test failure is reported by these tests
+
+ require.False(a == 1) // will fail and stop the test here
+ expect.False(a+1-1 == 1) // this would also fail but is not evaluated
+
+ // Output:
+ // expected false
+}
+
+func ExampleTrue() {
+ test.Example()
+
+ a := 1
+
+ // only one test failure is reported by these tests
+
+ require.True(a == 0) // will fail and stop the test here
+ expect.True(a < 0) // this would also fail but is not evaluated
+
+ // Output:
+ // expected true
+}
diff --git a/require/error.go b/require/error.go
new file mode 100644
index 0000000..131009e
--- /dev/null
+++ b/require/error.go
@@ -0,0 +1,26 @@
+package require
+
+import (
+ "github.com/blugnu/test/expect"
+ "github.com/blugnu/test/opt"
+
+ "github.com/blugnu/test"
+)
+
+// Error[E] tests that an error satisfies [errors.As] for a specified
+// error type E. If the test passes, the value is returned as that type.
+// If the test fails, the test fails immediately without evaluating any
+// further expectations.
+//
+// If a test does not use the returned value, consider using the [BeError]
+// matcher instead, to avoid lint warnings about unused return values.
+//
+// If a failure to match the expected error type is not a fatal error for the
+// test, consider using [expect.Error] instead.
+func Error[E error](got error, opts ...any) E {
+ test.T().Helper()
+
+ target, _ := expect.Error[E](got, append(opts, opt.Required())...)
+
+ return target
+}
diff --git a/require/error_test.go b/require/error_test.go
new file mode 100644
index 0000000..96745a2
--- /dev/null
+++ b/require/error_test.go
@@ -0,0 +1,73 @@
+package require_test
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/require"
+)
+
+type MyError struct {
+ msg string
+}
+
+func (e MyError) Error() string {
+ return e.msg
+}
+
+func TestError(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "error is of expected type",
+ Act: func() {
+ // arrange
+ err := fmt.Errorf("an error occurred: %w", MyError{"invalid argument"})
+
+ // act
+ e := require.Error[MyError](err)
+
+ // assert
+ Expect(e).To(Equal(MyError{"invalid argument"}))
+ Error("subsequent failures should appear in report")
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "subsequent failures should appear in report",
+ )
+ },
+ },
+
+ {Scenario: "error is not of expected type",
+ Act: func() {
+ // arrange
+ err := errors.New("some other error")
+
+ // act
+ _ = require.Error[MyError](err)
+ Error("subsequent failures should not appear in report")
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected error: require_test.MyError",
+ )
+ },
+ },
+
+ {Scenario: "nil should not be identified as any error",
+ Act: func() {
+ // act
+ _ = require.Error[error](error(nil))
+ Error("subsequent failures should not appear in report")
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected error: error",
+ "got : nil",
+ )
+ },
+ },
+ }...))
+}
diff --git a/require/nil.go b/require/nil.go
new file mode 100644
index 0000000..122434d
--- /dev/null
+++ b/require/nil.go
@@ -0,0 +1,45 @@
+package require
+
+import "github.com/blugnu/test"
+
+// Nil fails and halts the current test if a specified value is not nil.
+// An optional name (string) may be specified to be included in the test
+// report in the event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Require(got).IsNil()
+// Require(got).Should(BeNil())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func Nil(got any, opts ...any) {
+ test.T().Helper()
+ test.Require(got, opts...).Should(test.BeNil(), opts...)
+}
+
+// NotNil fails and halts the current test if a specified value is nil.
+// An optional name (string) may be specified to be included in the test
+// report in the event of failure.
+//
+// This test is a convenience for these equivalent alternatives:
+//
+// Require(got).IsNot(nil)
+// Require(got).ShouldNot(BeNil())
+//
+// # Supported Options
+//
+// string // a name for the value, for use in any test
+// // failure report
+//
+// opt.FailureReport(func) // a function returning a custom failure report
+// // in the event that the test fails
+func NotNil(got any, opts ...any) {
+ test.T().Helper()
+ test.Require(got, opts...).ShouldNot(test.BeNil(), opts...)
+}
diff --git a/require/nil_test.go b/require/nil_test.go
new file mode 100644
index 0000000..ed0bebd
--- /dev/null
+++ b/require/nil_test.go
@@ -0,0 +1,32 @@
+package require_test
+
+import (
+ "errors"
+
+ "github.com/blugnu/test/require"
+ "github.com/blugnu/test/test"
+)
+
+func ExampleNil() {
+ test.Example()
+
+ err := errors.New("an error")
+ require.Nil(nil) // will pass
+ require.Nil(err) // will fail
+ require.Nil(42) // will not be reached
+
+ // Output:
+ // expected nil, got error: an error
+}
+
+func ExampleNotNil() {
+ test.Example()
+
+ err := errors.New("an error")
+ require.NotNil(err) // will pass
+ require.NotNil(nil) // will fail
+ require.NotNil(42) // will not be reached
+
+ // Output:
+ // expected not nil
+}
diff --git a/require/pkgdoc.go b/require/pkgdoc.go
new file mode 100644
index 0000000..c16a8d5
--- /dev/null
+++ b/require/pkgdoc.go
@@ -0,0 +1,6 @@
+// Package require provides conclusory type assertion functions that operate on a
+// subject value. If the assertion fails, the test is failed and halted immediately;
+// no further expectations in the test will be evaluated.
+package require
+
+// this file is provided for package documentation purposes and is intentionally left blank
diff --git a/require/type.go b/require/type.go
new file mode 100644
index 0000000..76e18a8
--- /dev/null
+++ b/require/type.go
@@ -0,0 +1,21 @@
+package require
+
+import (
+ "github.com/blugnu/test"
+ "github.com/blugnu/test/expect"
+ "github.com/blugnu/test/opt"
+)
+
+// Type tests that a value is of an expected type. If the test passes,
+// the value is returned as that type otherwise the test fails immediately
+// without evaluating any further expectations.
+//
+// If a test does not use the returned value, consider using the [BeOfType]
+// matcher instead, to avoid lint warnings about unused return values.
+func Type[T any](got any, opts ...any) T {
+ test.T().Helper()
+
+ z, _ := expect.Type[T](got, append(opts, opt.Required())...)
+
+ return z
+}
diff --git a/require/type_test.go b/require/type_test.go
new file mode 100644
index 0000000..4c632d6
--- /dev/null
+++ b/require/type_test.go
@@ -0,0 +1,72 @@
+package require_test
+
+import (
+ "fmt"
+ "testing"
+
+ . "github.com/blugnu/test"
+
+ "github.com/blugnu/test/require"
+ "github.com/blugnu/test/test"
+)
+
+func TestExpectType(t *testing.T) {
+ With(t)
+
+ Run(HelperTests([]HelperScenario{
+ {Scenario: "subsequent tests are evaluated when test passes",
+ Act: func() {
+ result := require.Type[int](1)
+ Expect(result).To(Equal(1))
+
+ Expect(false).To(BeTrue())
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected true",
+ )
+ },
+ },
+ {Scenario: "subsequent tests are not evaluated when test fails",
+ Act: func() {
+ result := require.Type[int]("string")
+ Expect(result).To(Equal(0))
+ Expect(false).To(BeTrue())
+ },
+ Assert: func(result *R) {
+ result.Expect(
+ "expected type: int",
+ "got : string",
+ )
+ },
+ },
+ }...))
+}
+
+func ExampleType() {
+ test.Example()
+
+ // Type returns the value as the expected type when it
+ // is of that type
+ var got any = 1 / 2.0
+
+ result := require.Type[float64](got)
+
+ fmt.Printf("result: type is: %T\n", result)
+ fmt.Printf("result: value is: %v\n", result)
+
+ // Type terminates the current test if the value is not
+ // of the required type
+ got = "1 / 2.0"
+
+ require.Type[float64](got)
+
+ Expect(false, "this will not be evaluated").To(BeTrue())
+
+ //Output:
+ // result: type is: float64
+ // result: value is: 0.5
+ //
+ // expected type: float64
+ // got : string
+}
diff --git a/run.go b/run.go
index 6b49b3e..7e2c6b5 100644
--- a/run.go
+++ b/run.go
@@ -6,10 +6,16 @@ import (
"github.com/blugnu/test/internal/testframe"
)
+// Runnable is an interface implemented by types that can be executed
+// in a test frame using the [Run] function provided by the interface.
type Runnable interface {
Run()
}
+// Run executes the provided [Runnable] in the current test frame.
+//
+// It must be called from a Test..(*testing.T) func; it is not
+// supported in Example..() funcs.
func Run(r Runnable) {
t, ok := testframe.Peek[*testing.T]()
if !ok {
diff --git a/run_test.go b/run_test.go
index 79a7445..e675c98 100644
--- a/run_test.go
+++ b/run_test.go
@@ -30,7 +30,7 @@ func TestRun_Test(t *testing.T) {
})
Expect(result.FailedTests).To(ContainItem("TestRun_Test/named_test"))
- result.Expect("expected true, got false")
+ result.Expect("expected true")
}
func TestRun_Testcases(t *testing.T) {
@@ -187,7 +187,7 @@ func TestRun_Testcases(t *testing.T) {
))
})
- result.Expect(TestFailed, "expected 5, got 4")
+ result.Expect(test.Failed, "expected 5, got 4")
}))
Run(Test("skip method", func() {
diff --git a/run-flaky.go b/runnable-flaky.go
similarity index 93%
rename from run-flaky.go
rename to runnable-flaky.go
index f839b4c..c979884 100644
--- a/run-flaky.go
+++ b/runnable-flaky.go
@@ -4,6 +4,8 @@ import (
"fmt"
"strings"
"time"
+
+ "github.com/blugnu/test/test"
)
// flakyRunner runs a test function that is prone to flakiness. It implements
@@ -32,7 +34,7 @@ func (tr flakyRunner) Run() {
T().Helper()
outcome, attempts, reports, elapsed := tr.runTestWithRetries()
- if outcome == TestPassed {
+ if outcome == test.Passed {
return
}
@@ -60,12 +62,12 @@ func (tr flakyRunner) Run() {
// test, the number of attempts made, the reports of each failed attempt, and the
// total elapsed time.
//
-// If the test passes, the reports are discarded and the outcome is [TestPassed].
+// If the test passes, the reports are discarded and the outcome is [test.Passed].
// If the test fails after all attempts, the reports contain the details of each
-// failed attempt, and the outcome is [TestFailed].
+// failed attempt, and the outcome is [test.Failed].
//
// The elapsed time is truncated to milliseconds for consistency in reporting.
-func (tr flakyRunner) runTestWithRetries() (TestOutcome, uint, [][]string, time.Duration) {
+func (tr flakyRunner) runTestWithRetries() (test.Outcome, uint, [][]string, time.Duration) {
T().Helper()
var (
@@ -75,10 +77,10 @@ func (tr flakyRunner) runTestWithRetries() (TestOutcome, uint, [][]string, time.
reports [][]string
)
- handleOutcome := func(outcome TestOutcome) (TestOutcome, uint, [][]string, time.Duration) {
+ handleOutcome := func(outcome test.Outcome) (test.Outcome, uint, [][]string, time.Duration) {
elapsed = elapsed.Truncate(time.Millisecond)
- if outcome == TestPassed {
+ if outcome == test.Passed {
reports = nil // discard reports if the test passed
}
@@ -103,8 +105,8 @@ func (tr flakyRunner) runTestWithRetries() (TestOutcome, uint, [][]string, time.
elapsed = time.Since(start)
attempt++
- if result.Outcome == TestPassed {
- return handleOutcome(TestPassed)
+ if result.Outcome == test.Passed {
+ return handleOutcome(test.Passed)
}
reports = append(reports, result.Report)
@@ -112,7 +114,7 @@ func (tr flakyRunner) runTestWithRetries() (TestOutcome, uint, [][]string, time.
time.Sleep(tr.wait)
}
- return handleOutcome(TestFailed)
+ return handleOutcome(test.Failed)
}
// FlakyOption is an option function type for an option that modifies
diff --git a/run-flaky_test.go b/runnable-flaky_test.go
similarity index 76%
rename from run-flaky_test.go
rename to runnable-flaky_test.go
index de7d8ad..9bd3269 100644
--- a/run-flaky_test.go
+++ b/runnable-flaky_test.go
@@ -5,6 +5,7 @@ import (
"time"
. "github.com/blugnu/test"
+ "github.com/blugnu/test/test"
)
func TestFlaky(t *testing.T) {
@@ -19,16 +20,16 @@ func TestFlaky(t *testing.T) {
"Flaky test failed after 3 attempts",
"",
"attempt 1:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
"",
"attempt 2:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
"",
"attempt 3:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
)
result = TestHelper(func() {
@@ -41,12 +42,12 @@ func TestFlaky(t *testing.T) {
"Flaky test failed after 2 attempts in 1.0",
"",
"attempt 1:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
"",
"attempt 2:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
)
result = TestHelper(func() {
@@ -58,7 +59,7 @@ func TestFlaky(t *testing.T) {
}
}))
})
- result.Expect(TestPassed)
+ result.Expect(test.Passed)
result = TestHelper(func() {
Run(FlakyTest("configured max attempts", func() {
@@ -69,8 +70,8 @@ func TestFlaky(t *testing.T) {
"Flaky test failed after 1 attempt",
"",
"attempt 1:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
)
result = TestHelper(func() {
@@ -83,8 +84,8 @@ func TestFlaky(t *testing.T) {
"Flaky test failed after 1 attempt in 2", // precise time is not guaranteed but should be at least 2(00ms)
"",
"attempt 1:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
)
result = TestHelper(func() {
@@ -96,7 +97,7 @@ func TestFlaky(t *testing.T) {
"Flaky test failed after 1 attempt",
"",
"attempt 1:",
- " run-flaky_test.go",
- " expected true, got false",
+ " runnable-flaky_test.go",
+ " expected true",
)
}
diff --git a/run-helper-tests.go b/runnable-helper-tests.go
similarity index 96%
rename from run-helper-tests.go
rename to runnable-helper-tests.go
index db3d042..fcd8ad3 100644
--- a/run-helper-tests.go
+++ b/runnable-helper-tests.go
@@ -37,7 +37,7 @@ func (r helpertestRunner) Run() {
result := TestHelper(scn.Act)
if scn.Assert == nil {
- result.Expect(TestPassed)
+ result.Expect(test.Passed)
return
}
diff --git a/run-helper-tests_test.go b/runnable-helper-tests_test.go
similarity index 100%
rename from run-helper-tests_test.go
rename to runnable-helper-tests_test.go
diff --git a/run-test.go b/runnable-test.go
similarity index 64%
rename from run-test.go
rename to runnable-test.go
index 19ac3f0..639731e 100644
--- a/run-test.go
+++ b/runnable-test.go
@@ -3,30 +3,42 @@ package test
import (
"testing"
+ "github.com/blugnu/test/internal/testframe"
"github.com/blugnu/test/test"
)
-// testRunner is a simple wrapper around a test function that allows it to
-// be run as a subtest in the current test frame. It implements the Runnable
-// interface, allowing it to be used with the Run() function.
+// testRunner implements [Runnable] to execute a function with an associated
+// test name.
type testRunner struct {
name string
fn func()
parallel bool
}
+func newTestRunner(name string, fn func(), parallel bool) testRunner {
+ return testRunner{name: name, fn: fn, parallel: parallel}
+}
+
+type Helper interface {
+ Helper()
+}
+
// Run runs the named test function as a subtest in the current test frame
func (tr testRunner) Run() {
- t := T()
+ type T interface {
+ Helper()
+ Run(string, func(*testing.T)) bool
+ }
+ t := testframe.MustPeek[T]()
t.Helper()
- if tr.parallel && IsParallel() {
+ if tr.parallel && testframe.IsParallel() {
test.Invalid("ParallelTest() cannot be run from a parallel test")
}
t.Run(tr.name, func(t *testing.T) {
t.Helper()
- With(t)
+ testframe.PushWithCleanup(t)
if tr.parallel {
t.Parallel()
@@ -43,25 +55,17 @@ func (tr testRunner) Run() {
// fail the test as invalid since it is not allowed to nest parallel
// tests.
func ParallelTest(name string, fn func()) testRunner {
- T().Helper()
-
- if IsParallel() {
+ if testframe.IsParallel() {
+ T().Helper()
test.Invalid("ParallelTest() cannot be run from a parallel test")
}
- return testRunner{
- name: name,
- fn: fn,
- parallel: true,
- }
+ return newTestRunner(name, fn, true)
}
// Test creates a named test runner that can be used to run a test function as a
// subtest in the current test frame. The name is used as the subtest name,
// and the function is the test code to be executed.
func Test(name string, fn func()) testRunner {
- return testRunner{
- name: name,
- fn: fn,
- }
+ return newTestRunner(name, fn, false)
}
diff --git a/run-testcases.go b/runnable-testcases.go
similarity index 86%
rename from run-testcases.go
rename to runnable-testcases.go
index d8612ec..83a2e1a 100644
--- a/run-testcases.go
+++ b/runnable-testcases.go
@@ -23,18 +23,27 @@ type TestExecutor[T any] interface {
// a default name is derived in the format "testcase-NNN" where NNN is
// the 1-based index of the test case in the list of test cases.
//
-// If the test case data has a bool debug/Debug field that is set true, the
-// test case will be marked as a debug test case.
+// If the test case data has a bool field named debug/Debug that is set true,
+// the test case will be marked as a debug test case. When 1 or more debug test
+// cases are detected only those cases will be executed.
//
-// If the test case data has a bool skip/Skip field that is set true, the
-// test case will be marked as a skip test case.
+// If the test case data has a bool field named skip/Skip that is set true,
+// the test case will be skipped, even if it is also marked as a debug test case.
func Case[T any](name string, tc T) testcase.Registration[T] {
return func(r *testcase.Runner[T], flags testcase.Flags) {
r.AddCase(name, tc, flags)
}
}
-// Cases creates a Runner to run a set of test cases.
+// Cases registers a slice of test cases with the runner. This is a convenience
+// function that is equivalent to calling Case() for each test case in the slice
+// with an empty name.
+//
+// If the test case type T is a struct (or pointer to a struct) and has a
+// string field named "Scenario", "scenario", "Name", or "name", with a value
+// that is not empty and not whitespace, that will be used as the name. Otherwise
+// a default name is derived in the format "testcase-NNN" where NNN is the 1-based
+// index of the test case in the list of test cases.
func Cases[T any](cases []T) testcase.Registration[T] {
return func(r *testcase.Runner[T], flags testcase.Flags) {
for _, c := range cases {
@@ -43,7 +52,7 @@ func Cases[T any](cases []T) testcase.Registration[T] {
}
}
-// Debug adds a test case to the runner and marks it as a debug target.
+// Debug adds a test case to the runner and marks it as a debug case.
//
// When running test cases, if any cases are marked as debug only those
// cases will be run. This is useful for debugging specific test cases
diff --git a/skipNow.go b/skip-now.go
similarity index 100%
rename from skipNow.go
rename to skip-now.go
diff --git a/skipNow_test.go b/skip-now_test.go
similarity index 100%
rename from skipNow_test.go
rename to skip-now_test.go
diff --git a/stopWatch.go b/stop-watch.go
similarity index 100%
rename from stopWatch.go
rename to stop-watch.go
diff --git a/test-helper.go b/test-helper.go
index afa1208..a17ec38 100644
--- a/test-helper.go
+++ b/test-helper.go
@@ -15,14 +15,6 @@ import (
"github.com/blugnu/test/test"
)
-type TestOutcome int
-
-const (
- TestPassed TestOutcome = iota
- TestFailed
- TestPanicked
-)
-
const (
cFailedTests = "failed tests"
cTestOutcome = "test outcome"
@@ -30,19 +22,6 @@ const (
cRecovered = "recovered"
)
-func (to TestOutcome) String() string {
- switch to {
- case TestPassed:
- return "TestPassed"
- case TestFailed:
- return "TestFailed"
- case TestPanicked:
- return "TestPanicked"
- default:
- return fmt.Sprintf("TestOutcome(%d)", to)
- }
-}
-
// R is a struct that contains the result of executing a test function.
type R struct {
// value recovered if the test execution caused a panic
@@ -55,8 +34,8 @@ type R struct {
Log []string
// test outcome
- // (TestPassed, TestFailed, TestPanicked)
- Outcome TestOutcome
+ // (test.Passed, test.Failed, test.Panicked)
+ Outcome test.Outcome
// names of any tests that failed
FailedTests []string
@@ -74,7 +53,7 @@ type R struct {
// At least one argument must be provided to Expect() to specify the expected
// outcome of the test. The arguments can be:
//
-// - a TestOutcome value (TestPassed, TestFailed, TestPanicked)
+// - a test.Outcome value (test.Passed, test.Failed, test.Panicked)
// - a string or slice of strings that are expected to be present in the test report
// - a combination of the above, with options to control the assertion behavior
//
@@ -87,15 +66,15 @@ type R struct {
// If no arguments are provided, Expect() will fail the test with an error message
// indicating that at least one expected outcome or report line is required.
//
-// If the test outcome is expected to be TestPanicked, the first argument must be a
-// TestOutcome value (TestPanicked) with a single string argument that is expected to
+// If the test outcome is expected to be test.Panicked, the first argument must be a
+// test.Outcome value (test.Panicked) with a single string argument that is expected to
// match the string representation (%v) of the value recovered from the panic.
func (r *R) Expect(exp ...any) {
r.t.Helper()
if len(exp) == 0 {
// if no arguments are given, we expect the test to have passed
- test.Invalid("R.Expect: no arguments; an expected TestOutcome and/or test report are required")
+ test.Invalid("R.Expect: no arguments; an expected test.Outcome and/or test report are required")
}
// we have some expectations so can mark the result as having been checked
@@ -112,7 +91,7 @@ func (r *R) Expect(exp ...any) {
r.assertOutcome(expectedOutcome, opts...)
switch {
- case expectedOutcome == TestPanicked:
+ case expectedOutcome == test.Panicked:
r.assertPanicked(expectedReport, opts...)
case len(expectedReport) > 0:
@@ -142,15 +121,15 @@ func (r *R) Expect(exp ...any) {
}
// now we check that the report contains the expected lines
- Expect(r.Report).To(ContainSlice(expectedReport),
+ Expect(r.Report, "test report").To(ContainSlice(expectedReport),
append(opts, strings.Contains, opt.UnquotedStrings())...,
)
- case expectedOutcome == TestFailed && opt.IsSet(opts, opt.IgnoreReport(true)):
+ case expectedOutcome == test.Failed && opt.IsSet(opts, opt.IgnoreReport(true)):
Expect(r.FailedTests, cFailedTests).To(ContainItem(r.t.Name()), opts...)
default:
- r.assertWhenTestPassed(opts...)
+ r.TestPassed(opts...)
}
}
@@ -176,7 +155,7 @@ func (r *R) Expect(exp ...any) {
//
// When testing for an invalid test:
//
-// - the test outcome is expected to be TestFailed
+// - the test outcome is expected to be test.Failed
// - the test report is expected to start with the '<== INVALID TEST' label
// - the test report is expected to contain any report lines specified
func (r *R) ExpectInvalid(report ...any) {
@@ -191,15 +170,45 @@ func (r *R) ExpectWarning(msg string) {
r.Expect("<== WARNING: " + msg)
}
+// TestPassed checks that there were no failed tests and no report
+func (r *R) TestPassed(opts ...any) {
+ T().Helper()
+
+ // in this case, we want the failure report to provide details of
+ // any unexpectedly failed tests and an unexpected report, so we
+ // remove any IsRequired option to ensure that both expectations
+ // are evaluated
+ opts = opt.Unset(opts, opt.IsRequired(true))
+
+ Expect(r.FailedTests, cFailedTests).Should(BeEmptyOrNil(), opts...)
+
+ Expect(r.Report, cTestReport).Should(BeEmptyOrNil(),
+ append(opts, opt.FailureReport(func(...any) []string {
+ const preambleLen = 2
+
+ report := make([]string, preambleLen, len(r.Report)+preambleLen)
+ report[0] = "expected: "
+ report[1] = "got:"
+ for _, s := range r.Report {
+ report = append(report, "| "+s)
+ }
+
+ return report
+ }))...,
+ )
+}
+
// analyseArgs processes the expected arguments passed to Expect().
-// It separates the expected report strings from the options and determines
-// the expected outcome of the test based on the options provided.
-// It returns the expected outcome, the report strings, and the options.
-func (r *R) analyseArgs(exp ...any) (TestOutcome, []string, []any) {
+// Any expected report strings are separated from the options and
+// the expected outcome of the test is determined, based on the options.
+//
+// The function returns the expected outcome, the expected report
+// strings, and the options.
+func (r *R) analyseArgs(exp ...any) (test.Outcome, []string, []any) {
var (
+ outcome = test.Passed
report []string
opts []any
- outcome = TestPassed
)
// separate the expected report strings from any options
@@ -214,10 +223,10 @@ func (r *R) analyseArgs(exp ...any) (TestOutcome, []string, []any) {
}
}
- if opt.IsSet(opts, TestPanicked) {
- outcome = TestPanicked
- } else if opt.IsSet(opts, TestFailed) || len(report) > 0 {
- outcome = TestFailed
+ if opt.IsSet(opts, test.Panicked) {
+ outcome = test.Panicked
+ } else if opt.IsSet(opts, test.Failed) || len(report) > 0 {
+ outcome = test.Failed
}
return outcome, report, opts
@@ -226,7 +235,7 @@ func (r *R) analyseArgs(exp ...any) (TestOutcome, []string, []any) {
// assertOutcome checks that the test outcome matches the expected outcome.
//
// The function is used internally by Expect() to verify the test outcome.
-func (r *R) assertOutcome(expected TestOutcome, opts ...any) {
+func (r *R) assertOutcome(expected test.Outcome, opts ...any) {
T().Helper()
Expect(r.Outcome, cTestOutcome).To(Equal(expected),
@@ -237,7 +246,7 @@ func (r *R) assertOutcome(expected TestOutcome, opts ...any) {
}
switch {
- case r.Outcome == TestPanicked:
+ case r.Outcome == test.Panicked:
report = append(report, "")
report = append(report, "recovered:")
report = append(report, fmt.Sprintf(" %[1]T(%[1]v)", r.Recovered))
@@ -289,39 +298,11 @@ func (r *R) assertPanicked(expectedReport []string, opts ...any) {
)
}
-// assertWhenTestPassed checks that there were no failed tests and no report
-func (r *R) assertWhenTestPassed(opts ...any) {
- T().Helper()
-
- // in this case, we want the failure report to provide details of
- // any unexpectedly failed tests and an unexpected report, so we
- // remove any IsRequired option to ensure that both expectations
- // are evaluated
- opts = opt.Unset(opts, opt.IsRequired(true))
-
- Expect(r.FailedTests, cFailedTests).Should(BeEmptyOrNil(), opts...)
-
- Expect(r.Report, cTestReport).Should(BeEmptyOrNil(),
- append(opts, opt.FailureReport(func(...any) []string {
- const preambleLen = 2
-
- report := make([]string, preambleLen, len(r.Report)+preambleLen)
- report[0] = "expected: "
- report[1] = "got:"
- for _, s := range r.Report {
- report = append(report, "| "+s)
- }
-
- return report
- }))...,
- )
-}
-
// TestHelper runs a function that executes a function in an internal test runner,
// independent of the current test, returning an R value that captures the
// following:
//
-// - the test outcome (TestPassed, TestFailed, TestPanicked)
+// - the test outcome (test.Passed, test.Failed, test.Panicked)
// - names of any tests that failed
// - stdout output
// - stderr output
@@ -363,7 +344,7 @@ func TestHelper(f func()) R {
})
if recovered != nil {
- outcome = TestPanicked
+ outcome = test.Panicked
}
_, report, failed := analyseReport(stdout)
@@ -393,17 +374,17 @@ func runInternalMatchAll(pat, match string) (bool, error) {
// It is used to run a test function in a separate test runner in order to
// inspect the outcome of the test function without that affecting the state of the
// current test.
-func runInternal(t *testing.T, f func(*testing.T)) ([]string, []string, TestOutcome) {
+func runInternal(t *testing.T, f func(*testing.T)) ([]string, []string, test.Outcome) {
t.Helper()
- result := TestFailed
+ result := test.Failed
stdout, stderr := Record(func() {
it := []testing.InternalTest{{
Name: t.Name(),
F: f,
}}
if testing.RunTests(runInternalMatchAll, it) {
- result = TestPassed
+ result = test.Passed
}
})
diff --git a/test-helper_test.go b/test-helper_test.go
index 2839488..e8bb99e 100644
--- a/test-helper_test.go
+++ b/test-helper_test.go
@@ -7,30 +7,6 @@ import (
"github.com/blugnu/test/test"
)
-func TestTestOutcome(t *testing.T) {
- With(t)
-
- type testcase struct {
- TestOutcome
- result string
- }
- Run(Testcases(
- ForEach(func(tc testcase) {
- // ACT
- result := tc.String()
-
- // ASSERT
- Expect(result).To(Equal(tc.result))
- }),
- Cases([]testcase{
- {TestOutcome: TestPassed, result: "TestPassed"},
- {TestOutcome: TestFailed, result: "TestFailed"},
- {TestOutcome: TestPanicked, result: "TestPanicked"},
- {TestOutcome: 99, result: "TestOutcome(99)"},
- }),
- ))
-}
-
func TestTestHelper(t *testing.T) {
With(t)
@@ -49,7 +25,7 @@ func TestTestHelper(t *testing.T) {
result := TestHelper(func() {
panic("whoops!")
})
- result.Expect(TestPanicked, "whoops!")
+ result.Expect(test.Panicked, "whoops!")
}))
Run(Test("additional output to stdout", func() {
@@ -62,7 +38,7 @@ func TestTestHelper(t *testing.T) {
})
// the report consists only of test report output
- result.Expect(TestFailed, "expected false, got true")
+ result.Expect(test.Failed, "expected false")
}))
}
@@ -74,42 +50,42 @@ func TestR_Expect(t *testing.T) {
Act: func() {
sut := R{
t: T(),
- Outcome: TestPassed,
+ Outcome: test.Passed,
}
sut.Expect()
},
Assert: func(result *R) {
- result.ExpectInvalid("R.Expect: no arguments; an expected TestOutcome and/or test report are required")
+ result.ExpectInvalid("R.Expect: no arguments; an expected test.Outcome and/or test report are required")
},
},
{Scenario: "expected to panic (no report)",
Act: func() {
sut := R{
t: T(),
- Outcome: TestPanicked,
+ Outcome: test.Panicked,
Recovered: "recovered",
}
- sut.Expect(TestPanicked)
+ sut.Expect(test.Panicked)
},
},
{Scenario: "expected to panic (matched recovered string)",
Act: func() {
sut := R{
t: T(),
- Outcome: TestPanicked,
+ Outcome: test.Panicked,
Recovered: "recovered",
}
- sut.Expect(TestPanicked, "recovered")
+ sut.Expect(test.Panicked, "recovered")
},
},
{Scenario: "expected to panic (too many strings specified)",
Act: func() {
sut := R{
t: T(),
- Outcome: TestPanicked,
+ Outcome: test.Panicked,
Recovered: "recovered",
}
- sut.Expect(TestPanicked, "recovered", "and a second string")
+ sut.Expect(test.Panicked, "recovered", "and a second string")
},
Assert: func(result *R) {
result.ExpectInvalid("R.Expect: only 1 string may be specified to match a recovered value from an expected panic (got 2)")
@@ -119,17 +95,17 @@ func TestR_Expect(t *testing.T) {
Act: func() {
sut := R{
t: T(),
- Outcome: TestPanicked,
+ Outcome: test.Panicked,
Recovered: "recovered",
Stack: []byte("panic(\n runtime.panic.go\nstack trace\n with_call_sites.go:123:456"),
}
- sut.Expect(TestPassed)
+ sut.Expect(test.Passed)
},
Assert: func(result *R) {
result.Expect(
"test outcome:",
- " expected: TestPassed",
- " got : TestPanicked",
+ " expected: test.Passed",
+ " got : test.Panicked",
"",
"recovered:",
" string(recovered)",
@@ -143,15 +119,15 @@ func TestR_Expect(t *testing.T) {
Act: func() {
sut := R{
t: T(),
- Outcome: TestFailed,
+ Outcome: test.Failed,
}
- sut.Expect(TestPassed)
+ sut.Expect(test.Passed)
},
Assert: func(result *R) {
result.Expect(
"test outcome:",
- " expected: TestPassed",
- " got : TestFailed",
+ " expected: test.Passed",
+ " got : test.Failed",
)
},
},
@@ -159,16 +135,16 @@ func TestR_Expect(t *testing.T) {
Act: func() {
sut := R{
t: T(),
- Outcome: TestFailed,
+ Outcome: test.Failed,
Report: []string{"actual report"},
}
- sut.Expect(TestPassed)
+ sut.Expect(test.Passed)
},
Assert: func(result *R) {
result.Expect(
"test outcome:",
- " expected: TestPassed",
- " got : TestFailed",
+ " expected: test.Passed",
+ " got : test.Failed",
"with report:",
"| actual report",
)
@@ -178,10 +154,10 @@ func TestR_Expect(t *testing.T) {
Act: func() {
sut := R{
t: T(),
- Outcome: TestFailed,
+ Outcome: test.Failed,
Report: []string{"actual report"},
}
- sut.Expect(TestFailed)
+ sut.Expect(test.Failed)
},
Assert: func(result *R) {
result.Expect(
@@ -196,14 +172,32 @@ func TestR_Expect(t *testing.T) {
Act: func() {
sut := R{
t: T(),
- Outcome: TestFailed,
+ Outcome: test.Failed,
}
- sut.Expect(TestFailed, "some expected failure report")
+ sut.Expect(test.Failed, "some expected failure report")
},
Assert: func(result *R) {
result.ExpectWarning("test failed as expected, but no test report or failures were recorded")
},
},
+ {Scenario: "expected report as slice",
+ Act: func() {
+ sut := R{
+ t: T(),
+ Outcome: test.Failed,
+ FailedTests: []string{"TestR_Expect/expected_report_as_slice"},
+ Report: []string{
+ "test-helper_test.go:999", // actual line number will vary and is ignored
+ " actual report",
+ " spans two lines",
+ },
+ }
+ sut.Expect([]string{
+ "actual report",
+ "spans two lines",
+ })
+ },
+ },
}...))
}
@@ -232,7 +226,8 @@ func Test_runInternal(t *testing.T) {
// the output to avoid false negatives in tests that expect an empty output for
// a passing test.
- t := RequireType[*testing.T](T())
+ t, ok := T().(*testing.T)
+ Expect(ok).To(BeTrue())
// ACT
stdout, stderr, outcome := runInternal(t, func(_ *testing.T) {
@@ -243,7 +238,7 @@ func Test_runInternal(t *testing.T) {
})
// ASSERT
- Expect(outcome).To(Equal(TestPassed))
+ Expect(outcome).To(Equal(test.Passed))
Expect(stdout).Should(BeEmpty())
Expect(stderr).Should(BeNil())
}))
diff --git a/test/invalid.go b/test/invalidate.go
similarity index 63%
rename from test/invalid.go
rename to test/invalidate.go
index cadc5d2..c5c8cdc 100644
--- a/test/invalid.go
+++ b/test/invalidate.go
@@ -16,6 +16,45 @@ type runner interface {
FailNow()
}
+// Error is used to indicate a test is invalid due to some error having occurred.
+// This is intended to be used in test helpers and matchers to report an error
+// that invalidates a test.
+//
+// i.e. the error does not indicate that the test failed, but rather that
+// the test is invalid and therefore unreliable, due to an error that occurred
+// during execution or evaluation of a test helper or matcher.
+//
+// It should not be confused with the [github.com/blugnu/test.Error] or
+// [github.com/blugnu/test.Errorf] functions.
+//
+// If a valid test frame is available, it will report the error using the [Errorf]
+// method of that test frame.
+//
+// If no valid test frame is available, this function will panic with the
+// provided error and message(s). Panicking ensures that test execution fails,
+// avoiding a false positive outcome.
+//
+// # Alternatives
+//
+// To indicate that a test is invalid without any specific error, use the [Invalid]
+// function.
+//
+// To draw attention to a non-fatal issue in a test, use the [Warning] function.
+func Error(err error, msg ...string) {
+ if s := strings.Join(msg, "\n"); len(s) > 0 {
+ err = fmt.Errorf("%w\n%s", err, s)
+ }
+
+ if t, ok := testframe.Peek[runner](); ok {
+ t.Helper()
+ t.Errorf("<== INVALID TEST\nERROR: %s", err.Error())
+ t.FailNow()
+ return
+ }
+
+ panic(fmt.Errorf("INVALID TEST\n%w", err))
+}
+
// Invalid is used to mark a test as invalid. It should be called by a matcher
// when the test cannot be run due to an invalid condition, such as attempting to
// use a matcher with an unsupported type, or when the test is not properly set up.
@@ -36,54 +75,28 @@ func Invalid(msg ...string) {
// if we can obtain a TestRunner from the current test frame then we will
// use it to report the test as invalid, otherwise we must panic, to avoid
// a test yielding a false positive result
- t, ok := testframe.Peek[runner]()
- if !ok {
- panic(s)
- }
-
- t.Helper()
- t.Errorf("<== %s", s)
- t.FailNow()
-}
-
-// Error is used to indicate an error in a test. This should be used to report
-// errors that occur during the execution of a test rendering the test outcome
-// invalid.
-//
-// i.e. the error does not indicate that the test failed, but rather that
-// the test is invalid and therefore unreliable, due to an error that occurred
-// during its execution.
-//
-// If a valid test frame is available, it will report the error using the Error
-// method.
-//
-// If no valid test frame is available, this function will panic with the
-// provided error and message(s), avoiding a false positive test outcome.
-func Error(err error, msg ...string) {
- t, tok := testframe.Peek[runner]()
- s := strings.Join(msg, "\n")
-
- switch {
- // we have a t we can use to report the error
- case tok && len(s) > 0:
- t.Helper()
- t.Errorf("<== INVALID TEST\nERROR: %s\n%s", err.Error(), s)
- case tok:
+ if t, ok := testframe.Peek[runner](); ok {
t.Helper()
- t.Errorf("<== INVALID TEST\nERROR: %s", err.Error())
-
- // no t, we must panic
- case len(s) > 0:
- panic(fmt.Errorf("INVALID TEST\n%w\n%s", err, s))
- default:
- panic(fmt.Errorf("INVALID TEST\n%w", err))
+ t.Errorf("<== %s", s)
+ t.FailNow()
+ return
}
+
+ panic(s)
}
// Warning is used to report a warning in a test. This should be used to
// indicate a condition that is not an error, but may indicate a problem or
// unexpected behavior in the test.
//
+// Although a warning does not invalidate a test, it does fail any current
+// test execution.
+//
+// For example, if all test cases in a table-driven test are skipped, a
+// warning is produced indicating that the test did not run any test cases.
+// This does not represent an error or failure in any individual test case,
+// but fails the test, avoiding a false positive result.
+//
// If a valid test frame is available, it will report the warning using the
// Errorf, otherwise it will panic with the warning message.
//
@@ -97,11 +110,11 @@ func Warning(msg string) {
// if we can obtain a TestRunner from the current test frame then we will
// use it to report the test as invalid, otherwise we must panic, to avoid
// a test yielding a false positive result
- t, ok := testframe.Peek[runner]()
- if !ok {
- panic(msg)
+ if t, ok := testframe.Peek[runner](); ok {
+ t.Helper()
+ t.Errorf("<== " + msg)
+ return
}
- t.Helper()
- t.Errorf("<== " + msg)
+ panic(msg)
}
diff --git a/test/invalid_test.go b/test/invalidate_test.go
similarity index 100%
rename from test/invalid_test.go
rename to test/invalidate_test.go
diff --git a/test/outcome.go b/test/outcome.go
new file mode 100644
index 0000000..48e9793
--- /dev/null
+++ b/test/outcome.go
@@ -0,0 +1,24 @@
+package test
+
+import "fmt"
+
+type Outcome int
+
+const (
+ Passed Outcome = iota
+ Failed
+ Panicked
+)
+
+func (to Outcome) String() string {
+ switch to {
+ case Passed:
+ return "test.Passed"
+ case Failed:
+ return "test.Failed"
+ case Panicked:
+ return "test.Panicked"
+ default:
+ return fmt.Sprintf("test.Outcome(%d)", to)
+ }
+}
diff --git a/test/outcome_test.go b/test/outcome_test.go
new file mode 100644
index 0000000..f6140ea
--- /dev/null
+++ b/test/outcome_test.go
@@ -0,0 +1,32 @@
+package test_test
+
+import (
+ "testing"
+
+ . "github.com/blugnu/test"
+ "github.com/blugnu/test/test"
+)
+
+func TestOutcome(t *testing.T) {
+ With(t)
+
+ type testcase struct {
+ test.Outcome
+ result string
+ }
+ Run(Testcases(
+ ForEach(func(tc testcase) {
+ // ACT
+ result := tc.String()
+
+ // ASSERT
+ Expect(result).To(Equal(tc.result))
+ }),
+ Cases([]testcase{
+ {Outcome: test.Passed, result: "test.Passed"},
+ {Outcome: test.Failed, result: "test.Failed"},
+ {Outcome: test.Panicked, result: "test.Panicked"},
+ {Outcome: 99, result: "test.Outcome(99)"},
+ }),
+ ))
+}
diff --git a/testframe.go b/testframe.go
index 770243c..01fb622 100644
--- a/testframe.go
+++ b/testframe.go
@@ -98,9 +98,5 @@ func With(t TestingT) {
panic(testframe.ErrNoTestFrame)
}
- testframe.Push(t)
-
- t.Cleanup(func() {
- testframe.Pop()
- })
+ testframe.PushWithCleanup(t)
}
diff --git a/type.go b/type.go
deleted file mode 100644
index 7f5e85b..0000000
--- a/type.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package test
-
-import (
- "fmt"
- "reflect"
-
- "github.com/blugnu/test/matchers/typecheck"
- "github.com/blugnu/test/opt"
-)
-
-// ExpectType tests that a value is of an expected type. If the test passes,
-// the value is returned as that type, with true. If the test fails the zero
-// value of the specified type is returned, with false.
-//
-// If the value being of the expected type is essential to the test, consider
-// using the [RequireType] function instead, which will return the value or
-// fail the test immediately, avoiding the need to check the ok value.
-//
-// If a test does not use the returned value, consider using the [BeOfType]
-// matcher instead, to avoid lint warnings about unused return values.
-func ExpectType[T any](got any, opts ...any) (T, bool) {
- GetT().Helper()
-
- result, ok := got.(T)
- if ok {
- return result, true
- }
-
- var (
- zero T
- expectedType = fmt.Sprintf("%T", zero)
- )
-
- // if we could not determine the type of the expected value using the
- // zero value of the type, we can use a dummy function and reflect
- // the type of the first argument to that function.
- //
- // Q: why not just use the dummy func technique every time?
- // A: because it is more expensive than using the zero value, and
- // using the zero value provides a more precise (package-qualified)
- // type name, when successful
- if expectedType == "" {
- fn := func(T) { /* NO-OP */ }
- fn(zero) // ensures that the dummy function is 'covered' by tests
-
- expectedType = reflect.TypeOf(fn).In(0).Name()
- }
-
- gotType := fmt.Sprintf("%T", got)
-
- opts = append(opts,
- opt.FailureReport(func(...any) []string {
- return []string{
- "expected type: " + expectedType,
- "got : " + gotType,
- }
- }),
- )
-
- Expect(gotType, opts...).To(Equal(expectedType), opts...)
-
- return zero, false
-}
-
-// RequireType tests that a value is of an expected type. If the test passes,
-// the value is returned as that type otherwise the test fails immediately
-// without evaluating any further expectations.
-//
-// If a test does not use the returned value, consider using the [BeOfType]
-// matcher instead, to avoid lint warnings about unused return values.
-func RequireType[T any](got any, opts ...any) T {
- GetT().Helper()
-
- z, _ := ExpectType[T](got, append(opts, opt.Required())...)
-
- return z
-}
-
-// BeOfType returns a matcher that checks if the value is of the expected type.
-//
-// If a test wishes to perform further tests on the value that rely on having a
-// value of the expected type, consider using the [ExpectType] or [RequireType]
-// functions instead, which will return the value as the expected
-// type, or fail the test immediately.
-func BeOfType[T any]() typecheck.Matcher[T] {
- return typecheck.Matcher[T]{}
-}
diff --git a/type_test.go b/type_test.go
deleted file mode 100644
index fb31e9e..0000000
--- a/type_test.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package test_test
-
-import (
- "errors"
- "fmt"
- "testing"
-
- . "github.com/blugnu/test"
- "github.com/blugnu/test/test"
-)
-
-func TestExpectType(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "expecting int got int",
- Act: func() {
- result, ok := ExpectType[int](1)
- Expect(result).To(Equal(1))
- Expect(ok).To(BeTrue())
- },
- },
- {Scenario: "expecting int got string",
- Act: func() {
- result, ok := ExpectType[int]("string")
- Expect(result).To(Equal(0))
- Expect(ok).To(BeFalse())
- },
- Assert: func(result *R) {
- result.Expect(
- "expected type: int",
- "got : string",
- )
- },
- },
- {Scenario: "expecting named int got bool",
- Act: func() {
- result, ok := ExpectType[int](false, "named value")
- Expect(result).To(Equal(0))
- Expect(ok).To(BeFalse())
- },
- Assert: func(result *R) {
- result.Expect([]string{
- "named value:",
- " expected type: int",
- " got : bool",
- })
- },
- },
- {Scenario: "expecting error, got nil",
- Act: func() {
- result, ok := ExpectType[error](nil)
- Expect(ok).To(BeFalse())
- Expect(result).Should(BeNil())
- },
- Assert: func(result *R) {
- result.Expect(
- "expected type: error",
- "got : ",
- )
- },
- },
- {Scenario: "expecting error, got error",
- Act: func() {
- result, ok := ExpectType[error](errors.New("an error occurred"))
- Expect(ok).To(BeTrue())
- Expect(result.Error()).To(Equal("an error occurred"))
- },
- },
- }...))
-}
-
-func TestRequireType(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "expecting int got int",
- Act: func() {
- result := RequireType[int](1)
- Expect(result).To(Equal(1))
- },
- },
- {Scenario: "expecting int got string",
- Act: func() {
- RequireType[int]("string")
- Expect(false, "this will not be evaluated").To(BeTrue())
- },
- Assert: func(result *R) {
- result.Expect(
- "expected type: int",
- "got : string",
- )
- },
- },
- }...))
-}
-
-func TestShould_BeOfType(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "int is int",
- Act: func() { Expect(42).Should(BeOfType[int]()) },
- },
- {Scenario: "int is not string",
- Act: func() { Expect(42).Should(BeOfType[string]()) },
- Assert: func(result *R) {
- result.Expect(
- "expected type: string",
- "got : int",
- )
- },
- },
- }...))
-}
-
-func TestShouldNot_BeOfType(t *testing.T) {
- With(t)
-
- Run(HelperTests([]HelperScenario{
- {Scenario: "int is not string",
- Act: func() { Expect(42).ShouldNot(BeOfType[string]()) },
- },
- {Scenario: "int is string",
- Act: func() { Expect(42).ShouldNot(BeOfType[int]()) },
- Assert: func(result *R) {
- result.Expect(
- "should not be of type: int",
- )
- },
- },
- }...))
-}
-
-func ExampleExpectType() {
- test.Example()
-
- // ExpectType returns the value as the expected type and true if the
- // value is of that type
- var got any = 1 / 2.0
- result, ok := ExpectType[float64](got)
-
- fmt.Printf("ok is %v\n", ok)
- fmt.Printf("result: type is: %T\n", result)
- fmt.Printf("result: value is: %v\n", result)
-
- // ExpectType returns the zero value of the expected type and false if the
- // value is not of that type (the return values can be ignored if the
- // test is only concerned with checking the type)
- got = "1 / 2.0"
- ExpectType[float64](got)
-
- //Output:
- // ok is true
- // result: type is: float64
- // result: value is: 0.5
- //
- // expected type: float64
- // got : string
-}
-
-func ExampleRequireType() {
- test.Example()
-
- // RequireType returns the value as the expected type when it
- // is of that type
- var got any = 1 / 2.0
- result := RequireType[float64](got)
-
- fmt.Printf("result: type is: %T\n", result)
- fmt.Printf("result: value is: %v\n", result)
-
- // RequireType terminates the current test if the value is not
- // of the required type
- got = "1 / 2.0"
- RequireType[float64](got)
- Expect(false, "this will not be evaluated").To(BeTrue())
-
- //Output:
- // result: type is: float64
- // result: value is: 0.5
- //
- // expected type: float64
- // got : string
-}