diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..91f72ae --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,30 @@ +name: docs +on: + push: + tags: + - 'v*.*.*' +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: actions/cache@v2 + with: + key: ${{ github.ref }} + path: .cache + - run: sudo apt-get install -y libcairo2-dev libfreetype6-dev libffi-dev libjpeg-dev libpng-dev libz-dev + - run: pip install -r docs/requirements.txt + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + - name: Setup git + run: | + git config --global user.name mockio-actions + git config --global user.email mockio-actions@ovechkin-dm.github.io + git fetch origin gh-pages --depth=1 + - name: Deploy docs + run: "mike deploy --push --update-aliases $(echo ${{ github.ref_name }} | cut -d'.' -f1-2 | xargs printf '%s.0' ) latest" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6eb4299..d129318 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ coverage.txt -.cache \ No newline at end of file +.cache +.idea +*.dylib \ No newline at end of file diff --git a/README.md b/README.md index 2a54ddd..1ba3e08 100644 --- a/README.md +++ b/README.md @@ -10,118 +10,37 @@ # Mock library for golang without code generation Mockio is a Golang library that provides functionality for mocking and stubbing functions and methods in tests inspired by mockito. The library is designed to simplify the testing process by allowing developers to easily create test doubles for their code, which can then be used to simulate different scenarios. -# Features -* No code generation required, mocks are created at runtime -* Simple and easy to use API -* Support for parallel test running -* Extensive use of generics, which provides additional type check at compile time +# Documentation -## Installation +Latest documentation is available [here](https://ovechkin-dm.github.io/mockio/latest/) -To use Mockio in your Golang project, you can install it using the go get command: +# Quick start + +Install latest version of the library using go get command: ```bash -go get github.com/ovechkin-dm/mockio +go get -u github.com/ovechkin-dm/mockio ``` -## Quick start +Create a simple mock and test: ```go -package simple +package main import ( - . "github.com/ovechkin-dm/mockio/mock" - "testing" + . "github.com/ovechkin-dm/mockio/mock" + "testing" ) -type myInterface interface { - Foo(a int) int +type Greeter interface { + Greet(name string) string } -func TestSimple(t *testing.T) { - SetUp(t) - m := Mock[myInterface]() - WhenSingle(m.Foo(Any[int]())).ThenReturn(42) - _ = m.Foo(10) - Verify(m, AtLeastOnce()).Foo(10) +func TestGreet(t *testing.T) { + SetUp(t) + m := Mock[Greeter]() + WhenSingle(m.Greet("John")).ThenReturn("Hello, John!") + if m.Greet("John") != "Hello, John!" { + t.Fail() + } } - - -``` - -## Stubs and Matchers -Once you have a mock object, you can start stubbing methods or functions using the `WhenSingle`, `WhenDouble`, and `When` functions. These functions allow you to define a set of conditions under which the stubbed method or function will return a certain value. - -Because golang does not support method overloading, and we still want additional type check on returning values three separate methods were introduced for stubbing: -* `WhenSingle` is used when there is only one return value for the method -* `WhenDouble` is used when there is a `(A, B)` tuple as return value for the method -* `When` for multiple return values - -Example usage: -```go -// Stub a method to always return a specific value -mockObject := Mock[MyInterface]() -WhenSingle(mockObject.MethodCall(Exact[Int](1))).ThenReturn("value") -``` - -Mockio also provides a set of matchers that you can use to define more complex conditions for your stubbed methods. -```go -// Use a matcher to stub a method with a specific input -mockObject := Mock[MyInterface]() -WhenSingle(mockObject.MethodCall(Equal("input"))).ThenReturn("value") -``` - -## Verification -Mockio also provides functionality for verifying that certain methods were called with specific inputs. You can use the `Verify` function to verify that a specific method or function was called a certain number of times, or with a specific set of inputs. -```go -// Verify that a method was called exactly once -mockObject := Mock[MyInterface]() -mockObject.MethodCall("input") -Verify(mockObject, Once()).MethodCall(Equal("input")) -``` - -## Argument Captors -Mockio also provides functionality for capturing the arguments passed to a mocked method or function. You can use the Captor function to create an argument captor, which you can then use to retrieve the captured arguments. -```go -// Use an argument captor to capture the argument passed to a function -mockObject := Mock[MyInterface]() -argumentCaptor := Captor[int]() -WhenSingle(mockObject.MethodCall(argumentCaptor.Capture())).ThenReturn("value") -mockObject.MethodCall(42) -capturedArgument := argumentCaptor.Last() // 42 -``` - -## Reporting -The `ErrorReporter` interface defines how errors should be reported in the library. It has a single method `Fatalf` which takes a format string and its arguments and panics with a formatted error message. -```go - -``` - -## Returner -The `Returner` interfaces define how the mocked function should return values. There are three different `Returner` interfaces: - -- `Returner1[T any]` for functions with one return value -- `ReturnerE[T any]` for functions with two return values, where the second one is an error -- `ReturnerAll` for functions that return multiple values -Each of these interfaces provides methods ThenReturn and ThenAnswer for setting the mocked function's return value(s). ThenReturn takes one or more values and sets them as the return value(s) of the mocked function. ThenAnswer takes a function that returns the value(s) to be used as the return value(s) of the mocked function. - -## Call verifiers - -The `MethodVerifier` interface defines how method calls should be verified. There are four concrete implementations of this interface: - -* `AtLeastOnce()` verifies that the mocked method was called at least once -* `Once()` verifies that the mocked method was called exactly once -* `Times(n int)` verifies that the mocked method was called n times -* `Never()` verifies that the mocked method was never called -The `Verify` method of the MethodVerifier interface takes a MethodVerificationData object which contains information about the method call, such as the number of times it was called. If the verification fails, an error is returned. - -The `InstanceVerifier` interface defines how instances should be verified. It has a single method RecordInteraction which takes an InvocationData object containing information about the method call. If the verification fails, error is being reported. - -## Conclusion -The mock package provides a powerful library for creating and managing mock objects in Go. With its support for capturing arguments, matching arguments, and verifying method calls, it makes it easy to test complex systems with many dependencies. Its well-defined interfaces and clear documentation make it easy to use and extend, and its support for multiple return values and errors makes it suitable for a wide range of use cases. - - -## Limitations -* **Restricted support for processor architectures**. For now library only supports amd64 and arm64 architectures, but can be extended to others if there is demand for it. -* **Go >= 1.18** -* **Concurrency limitations** - * For now, you have to use every call to library in the same goroutine, on which `SetUp()` was called. \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/features/captors.md b/docs/features/captors.md new file mode 100644 index 0000000..f326a96 --- /dev/null +++ b/docs/features/captors.md @@ -0,0 +1,68 @@ +# Argument Captors + +Argument captors are a powerful feature that allow you to capture the arguments passed to a method when it is +called. This is useful when you want to verify that a method was called with specific arguments, but you don't know what +those arguments will be ahead of time. + +## Creating a Captor + +To create a captor, you simply call the `Captor` function with the type of the argument you want to capture: + +```go +c := Captor[string]() +``` + +## Using a Captor + +To use a captor, you pass it as an argument to the `When` function. When the method is called, the captor will capture the +argument and store it in the captor's value: + +```go +When(greeter.Greet(c.Capture())).ThenReturn("Hello, world!") +``` + +## Retrieving the Captured Values + +Argument captor records an argument on each stub call. You can retrieve the captured values by calling the `Values` method + +```go +capturedValues := c.Values() +``` + +If you want to retrieve just the last captured value, you can call the `Last` method + +```go +capturedValue := c.Last() +``` + +## Example usage + +In this example we will create a mock, and use an argument captor to capture the arguments passed to the `Greet` method: + +```go +package main + +import ( + . "github.com/ovechkin-dm/mockio/mock" + "testing" +) + +type Greeter interface { + Greet(name any) string +} + +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + c := Captor[string]() + When(greeter.Greet(c.Capture())).ThenReturn("Hello, world!") + _ = greeter.Greet("John") + _ = greeter.Greet("Jane") + if c.Values()[0] != "John" { + t.Error("Expected John, got", c.Values()[0]) + } + if c.Values()[1] != "Jane" { + t.Error("Expected Jane, got", c.Values()[1]) + } +} +``` \ No newline at end of file diff --git a/docs/features/configuration.md b/docs/features/configuration.md new file mode 100644 index 0000000..d07892f --- /dev/null +++ b/docs/features/configuration.md @@ -0,0 +1,158 @@ +# Configuration + +## Using options + +MockIO library can be configured by providing options from `mockopts` package inside `SetUp` function like this: +```go +package main + +import ( + . "github.com/ovechkin-dm/mockio/mock" + "github.com/ovechkin-dm/mockio/mockopts" + "testing" +) + +func TestSimple(t *testing.T) { + SetUp(t, mockopts.WithoutStackTrace()) +} + +``` + +## StrictVerify +**StrictVerify** adds extra checks on each test teardown. +It will fail the test if there are any unverified calls. +It will also fail the test if there are any calls that were not expected. + +### Unverified calls check + +Consider the following example: +```go +package main + +import ( + . "github.com/ovechkin-dm/mockio/mock" + "github.com/ovechkin-dm/mockio/mockopts" + "testing" +) + +type Greeter interface { + Greet(name string) string +} + +func TestSimple(t *testing.T) { + SetUp(t, mockopts.StrictVerify()) + greeter := Mock[Greeter]() + When(greeter.Greet("John")).ThenReturn("Hello, John!") +} + +``` +In this case, the test will fail because the `Greet` method was not called with the expected argument. +If we want this test to pass, we need to call greeter with the expected argument: +```go +func TestSimple(t *testing.T) { + SetUp(t, mockopts.StrictVerify()) + greeter := Mock[Greeter]() + When(greeter.Greet("John")).ThenReturn("Hello, John!") + greeter.Greet("John") +} +``` + +### Unexpected calls check + +Consider the following example: + +```go +func TestSimple(t *testing.T) { + SetUp(t, mockopts.StrictVerify()) + greeter := Mock[Greeter]() + When(greeter.Greet("John")).ThenReturn("Hello, John!") + greeter.Greet("John") + greeter.Greet("Jane") +} +``` + +In this case, the test will fail because the `Greet` method was called with an unexpected argument. +If we want this test to pass, we need to remove the unexpected call, or add an expectation for it: +```go +func TestSimple(t *testing.T) { + SetUp(t, mockopts.StrictVerify()) + greeter := Mock[Greeter]() + When(greeter.Greet("John")).ThenReturn("Hello, John!") + When(greeter.Greet("Jane")).ThenReturn("Hello, Jane!") + greeter.Greet("John") + greeter.Greet("Jane") +} +``` + +## WithoutStackTrace +**WithoutStackTrace** option disables stack trace printing in case of test failure. + +Consider the following example: +```go +package main + +import ( + . "github.com/ovechkin-dm/mockio/mock" + "testing" +) + +type Greeter interface { + Greet(name string) string +} + +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + VerifyNoMoreInteractions(greeter) +} + +``` + +If we run this test, we will see the following error: +``` +=== RUN TestSimple + reporter.go:75: At: + go/pkg/mod/github.com/ovechkin-dm/mockio@v0.7.2/registry/registry.go:130 +0x45 + Cause: + No more interactions expected, but unverified interactions found: + Greeter.Greet(John) at demo/hello_test.go:16 +0xf2 + Trace: + demo.TestSimple.VerifyNoMoreInteractions.VerifyNoMoreInteractions.func1() + go/pkg/mod/github.com/ovechkin-dm/mockio@v0.7.2/registry/registry.go:130 +0x45 + demo.TestSimple(0xc00018c4e0?) + demo/hello_test.go:17 +0x15a + testing.tRunner(0xc00018c4e0, 0x647ca0) + /usr/local/go/src/testing/testing.go:1689 +0xfb + created by testing.(*T).Run in goroutine 1 + /usr/local/go/src/testing/testing.go:1742 +0x390 + +--- FAIL: TestSimple (0.00s) + +FAIL +``` + +By adding `mockopts.WithoutStackTrace()` to the `SetUp` function, we can disable stack trace printing: +```go +func TestSimple(t *testing.T) { + SetUp(t, mockopts.WithoutStackTrace()) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + VerifyNoMoreInteractions(greeter) +} +``` + +Now the error will look like this: +``` +=== RUN TestSimple + reporter.go:75: At: + go/pkg/mod/github.com/ovechkin-dm/mockio@v0.7.2/registry/registry.go:130 +0x45 + Cause: + No more interactions expected, but unverified interactions found: + Greeter.Greet(John) at demo/hello_test.go:17 +0x10b +--- FAIL: TestSimple (0.00s) + +FAIL +``` \ No newline at end of file diff --git a/docs/features/error-reporting.md b/docs/features/error-reporting.md new file mode 100644 index 0000000..9dfeb6b --- /dev/null +++ b/docs/features/error-reporting.md @@ -0,0 +1,232 @@ +# Error reporting + +`Mockio` library supports providing custom error reporting in `SetUp()` function. +This can be helpful if you want to introduce custom error reporting or logging. +Reporter should implement `ErrorReporter` interface. +```go +type ErrorReporter interface { + Fatalf(format string, args ...any) + Errorf(format string, args ...any) + Cleanup(func()) +} + +``` + +* `Fatalf` - should be used to report fatal errors. It should stop the test execution. +* `Errorf` - should be used to report non-fatal errors. It should continue the test execution. +* `Cleanup` - should be used to register a cleanup function. It should be called after the test execution. + +## Error output + +### Incorrect `When` usage + +Example: + +```go +When(1) +``` + +Output: +``` +At: + /demo/error_reporting_test.go:22 +0xad +Cause: + When() requires an argument which has to be 'a method call on a mock'. + For example: When(mock.GetArticles()).ThenReturn(articles) +``` + +### Verify from different goroutine + +Example: + +```go +SetUp(r) +mock := Mock[Foo]() +wg := sync.WaitGroup{} +wg.Add(1) +go func() { + SetUp(r) + Verify(mock, Once()) + wg.Done() +}() +wg.Wait() +``` + +Output: +``` +At: + /demo/error_reporting_test.go:35 +0xc5 +Cause: + Argument passed to Verify() is { DynamicProxy[reporting.Foo] } and is not a mock, or a mock created in a different goroutine. + Make sure you place the parenthesis correctly. + Example of correct verification: + Verify(mock, Times(10)).SomeMethod() +``` + +### Non-mock verification + +Example: + +```go +Verify(100, Once()) +``` + +Output: +``` +At: + /demo/error_reporting_test.go:46 +0x105 +Cause: + Argument passed to Verify() is 100 and is not a mock, or a mock created in a different goroutine. + Make sure you place the parenthesis correctly. + Example of correct verification: + Verify(mock, Times(10)).SomeMethod() +``` + +### Invalid use of matchers + +Example: + +```go +When(mock.Baz(AnyInt(), AnyInt(), 10)).ThenReturn(10) +``` + +Output: +``` +At: + /demo/error_reporting_test.go:55 +0x110 +Cause: + Invalid use of matchers + 3 expected, 2 recorded: + /demo/error_reporting_test.go:55 +0xab + /demo/error_reporting_test.go:55 +0xbc + method: + Foo.Baz(int, int, int) int + expected: + (int,int,int) + got: + (Any[int],Any[int]) + This can happen for 2 reasons: + 1. Declaration of matcher outside When() call + 2. Mixing matchers and exact values in When() call. Is this case, consider using "Exact" matcher. +``` + +### Expected method call + +Example: + +```go +When(mock.Baz(AnyInt(), AnyInt(), AnyInt())).ThenReturn(10) +_ = mock.Baz(10, 10, 11) +Verify(mock, Once()).Baz(AnyInt(), AnyInt(), Exact(10)) +``` + +Output: +``` +At: + /demo/error_reporting_test.go:88 +0x262 +Cause: + expected num method calls: 1, got : 0 + Foo.Baz(Any[int], Any[int], Exact(10)) + However, there were other interactions with this method: + Foo.Baz(10, 10, 11) at /demo/error_reporting_test.go:87 +0x193 +``` + +### Number of method calls + +Example: + +```go +When(mock.Baz(AnyInt(), AnyInt(), AnyInt())).ThenReturn(10) +_ = mock.Baz(10, 10, 10) +Verify(mock, Times(20)).Baz(AnyInt(), AnyInt(), AnyInt()) +``` + +Output: +``` +At: + /demo/error_reporting_test.go:121 +0x25a +Cause: + expected num method calls: 20, got : 1 + Foo.Baz(Any[int], Any[int], Any[int]) + Invocations: + /demo/error_reporting_test.go:120 +0x191 +``` + +### Empty captor + +Example: + +```go +c := Captor[int]() +_ = c.Last() +``` + +Output: +``` +At: + /demo/error_reporting_test.go:130 +0x92 +Cause: + no values were captured for captor +``` + +### Invalid return values + +Example: + +```go +When(mock.Baz(AnyInt(), AnyInt(), AnyInt())).ThenReturn(10, 20) +``` + +Output: +``` +At: + /demo/error_reporting_test.go:140 +0x1a7 +Cause: + invalid return values +expected: + Foo.Baz(int, int, int) int +got: + Foo.Baz(int, int, int) (string, int) +``` + +### No more interactions + +Example: + +```go +When(mock.Baz(AnyInt(), AnyInt(), AnyInt())).ThenReturn("test", 10) +_ = mock.Baz(10, 10, 10) +_ = mock.Baz(10, 20, 10) +VerifyNoMoreInteractions(mock) +``` + +Output: +``` +At: + /demo/mockio/registry/registry.go:130 +0x45 +Cause: + No more interactions expected, but unverified interactions found: + Foo.Baz(10, 10, 10) at /demo/error_reporting_test.go:150 +0x1a8 + Foo.Baz(10, 20, 10) at /demo/error_reporting_test.go:151 +0x1c6 +``` + +### Unexpected matcher declaration + +Example: + +```go +When(mock.Baz(AnyInt(), AnyInt(), AnyInt())).ThenReturn(10) +mock.Baz(AnyInt(), AnyInt(), AnyInt()) +Verify(mock, Once()).Baz(10, 10, 10) +``` + +```go +At: + /demo/error_reporting_test.go:175 +0x23f +Cause: + Unexpected matchers declaration. + at /demo/error_reporting_test.go:174 +0x185 + at /demo/error_reporting_test.go:174 +0x196 + at /demo/error_reporting_test.go:174 +0x1a7 + Matchers can only be used inside When() method call. +``` \ No newline at end of file diff --git a/docs/features/matchers.md b/docs/features/matchers.md new file mode 100644 index 0000000..41fd0f6 --- /dev/null +++ b/docs/features/matchers.md @@ -0,0 +1,460 @@ +# Matchers +MockIO library provides a lot of ways to match arguments of the method calls. +Matchers are used to define the expected arguments of the method calls. + +We will use the following interface for the examples: +```go +package main + +import ( + . "github.com/ovechkin-dm/mockio/mock" + "testing" +) + +type Greeter interface { + Greet(name any) string +} + +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") +} +``` + +## Any +The `Any[T]()` matcher matches any value of the type `T`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Any[string]())).ThenReturn("hello world") + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## AnyInt +The `AnyInt()` matcher matches any integer value. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Any[int]())).ThenReturn("hello world") + if greeter.Greet(10) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` +This test will fail, because the argument is not an integer: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Any[int]())).ThenReturn("hello world") + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## AnyString +The `AnyString()` matcher matches any string value. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Any[string]())).ThenReturn("hello world") + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` +This test will fail, because the argument is not a string: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Any[int]())).ThenReturn("hello world") + if greeter.Greet(10) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## AnyInterface +The `AnyInterface()` matcher matches any value of any type. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(AnyInterface())).ThenReturn("hello world") + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +This test will also succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(AnyInterface())).ThenReturn("hello world") + if greeter.Greet(10) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## AnyContext +The `AnyContext()` matcher matches any context.Context value. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(AnyContext())).ThenReturn("hello world") + if greeter.Greet(context.Background()) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## AnyOfType +The `AnyOfType[T](t T)` matcher matches any value of the type `T` or its subtype. It is useful for type inference. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(AnyOfType(10))).ThenReturn("hello world") + if greeter.Greet(10) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` +Note that when we are using AnyOfType, we don't need to specify the type explicitly. + +## Nil +The `Nil[T]()` matcher matches any nil value of the type `T`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Nil[any]())).ThenReturn("hello world") + if greeter.Greet(nil) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## NotNil +The `NotNil[T]()` matcher matches any non-nil value of the type `T`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(NotNil[any]())).ThenReturn("hello world") + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(NotNil[any]())).ThenReturn("hello world") + if greeter.Greet(nil) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## Regex +The `Regex(pattern string)` matcher matches any string that matches the regular expression `pattern`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Regex("J.*"))).ThenReturn("hello world") + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## Substring +The `Substring(sub string)` matcher matches any string that contains the substring `sub`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(Substring("oh"))).ThenReturn("hello world") + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## SliceLen +The `SliceLen(length int)` matcher matches any slice with the length `length`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(SliceLen[int](2))).ThenReturn("hello world") + if greeter.Greet([]int{1, 2}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(SliceLen[int](2))).ThenReturn("hello world") + if greeter.Greet([]int{1, 2, 3}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## MapLen +The `MapLen(length int)` matcher matches any map with the length `length`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(MapLen[int, string](2))).ThenReturn("hello world") + if greeter.Greet(map[int]string{1: "one", 2: "two"}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(MapLen[int, string](2))).ThenReturn("hello world") + if greeter.Greet(map[int]string{1: "one", 2: "two", 3: "three"}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## SliceContains +The `SliceContains[T any](values ...T)` matcher matches any slice that contains all the values `values`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(SliceContains[int](1, 2))).ThenReturn("hello world") + if greeter.Greet([]int{1, 2, 3}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(SliceContains[int](1, 2))).ThenReturn("hello world") + if greeter.Greet([]int{1, 3}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## MapContains +The `MapContains[K any, V any](keys ...K)` matcher matches any map that contains all the keys `keys`. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(MapContains[int, string](1, 2))).ThenReturn("hello world") + if greeter.Greet(map[int]string{1: "one", 2: "two", 3: "three"}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(MapContains[int, string](1, 2))).ThenReturn("hello world") + if greeter.Greet(map[int]string{1: "one", 3: "three"}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## SliceEqualUnordered + +The `SliceEqualUnordered[T any](values []T)` matcher matches any slice that contains the same elements as `values`, but in any order. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(SliceEqualUnordered[int](1, 2))).ThenReturn("hello world") + if greeter.Greet([]int{2, 1}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(SliceEqualUnordered[int](1, 2))).ThenReturn("hello world") + if greeter.Greet([]int{1, 3}) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## Exact + +The `Exact` matcher matches any value that is equal to the expected value. +`Exact` uses `==` operator to compare values. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + world1 := "world" + When(greeter.Greet(Exact(&world1))).ThenReturn("hello world") + if greeter.Greet(&world1) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +However, this test will fail, because although the values are equal, they are different pointers: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + world1 := "world" + world2 := "world" + When(greeter.Greet(Exact(&world1))).ThenReturn("hello world") + if greeter.Greet(&world2) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## Equal + +The `Equal` matcher matches any value that is equal to the expected value. `Equal` uses `reflect.DeepEqual` to compare values. + +This test will succeed, because `reflect.DeepEqual` compares values by their content: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + world1 := "world" + world2 := "world" + When(greeter.Greet(Equal(&world1))).ThenReturn("hello world") + if greeter.Greet(&world2) != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +## NotEqual + +The `NotEqual` matcher matches any value that is not equal to the expected value. `NotEqual` uses `reflect.DeepEqual` to compare values. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(NotEqual("John"))).ThenReturn("hello world") + if greeter.Greet("world") != "hello world" { + t.Error("Expected 'hello John'") + } +} + +``` + +## OneOf + +The `OneOf` matcher matches any value that is equal to one of the expected values. `OneOf` uses `reflect.DeepEqual` to compare values. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet(OneOf("John", "Jane"))).ThenReturn("hello John or Jane") + if greeter.Greet("Jane") != "hello John or Jane" { + t.Error("expected 'hello John or Jane'") + } +} +``` +## Custom matcher + +Here is an example of a custom matcher that matches odd numbers only: + +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + odd := CreateMatcher[int]("odd", func(args []any, v int) bool { + return v%2 == 1 + }) + When(greeter.Greet(Match(odd))).ThenReturn("hello odd number") + if greeter.Greet(1) != "hello odd number" { + t.Error("expected ''hello odd number''") + } +} +``` + +For more examples on custom matchers see `Examples` section. + diff --git a/docs/features/method-stubbing.md b/docs/features/method-stubbing.md new file mode 100644 index 0000000..e5b7b3b --- /dev/null +++ b/docs/features/method-stubbing.md @@ -0,0 +1,137 @@ +# Method stubbing + +Method stubbing is a technique used in unit testing to replace a method with a stub. A stub is a small piece of code +that simulates the behavior of the method it replaces. This allows you to test the behavior of the code that calls the +method without actually executing the method itself. + +Basic usage of method stubbing in Mockio looks like this: + +```go +When(mock.SomeMethod(AnyInt())).ThenReturn("some value") +``` + +* `When` is a function that takes a method call as an argument and returns a `Returner` object. +* Inside the method call argument you can use any matcher from the library's API. In this example we used `AnyInt()` matcher. +* `ThenReturn` is a method of the `Returner` + +This is basic usage of method stubbing. But there are also some useful extensions to this API. + +## When + +`When` is a function that allows you to stub a method. +Keep in mind, that `When` is a generic function, so it does not provide any type check on return value. + + +## WhenSingle + +`WhenSingle` is a function that allows you to stub a method to return a single value. +It is almost the same as `When`, but it provides additional type check on return value. + +Consider Following interface: +```go +type Foo interface { + Bar(int) string +} +``` + +You can stub `Bar` method like this: +```go +WhenSingle(mock.Bar(AnyInt())).ThenReturn("some value") +``` + +However, this will not compile: +```go +WhenSingle(mock.Bar(AnyInt())).ThenReturn(42) +``` + +But this will: +```go +When(mock.Bar(AnyInt())).ThenReturn(42) +``` + +## WhenDouble + +`WhenDouble` is a function that allows you to stub a method to return two values. +It is almost the same as `When`, but it provides additional type check on return values. + +Consider Following interface: +```go +type Foo interface { + Bar(int) (string, error) +} +``` + +You can stub `Bar` method like this: +```go +WhenDouble(mock.Bar(AnyInt())).ThenReturn("some value", nil) +``` + +However, this will not compile: +```go +WhenDouble(mock.Bar(AnyInt())).ThenReturn("some value", 42) +``` + +But this will: +```go +When(mock.Bar(AnyInt())).ThenReturn("some value", 42) +``` + +## ThenAnswer + +`Answer` is a function that allows you to stub a method to return a value based on the arguments passed to the method. + +Consider following interface: +```go +type Foo interface { + Bar(int) string +} +``` + +You can stub `Bar` method like this: +```go +mock := Mock[Foo]() +WhenSingle(mock.Bar(AnyInt())).ThenAnswer(func(args []any) string { + return fmt.Sprintf("Hello, %d", args[0].(int)) +}) +``` + +When `Bar` method is called with argument `42`, it will return `"Hello, 42"`. + +## ThenReturn + +You can chain multiple `ThenReturn` calls to return different values on subsequent calls: + +```go +When(mock.SomeMethod(AnyInt())). + ThenReturn("first value"). + ThenReturn("second value") +``` + +Calling `SomeMethod` first time will return `"first value"`, second time `"second value"`, and so on. + +## Implicit `Exact` matchers + +Consider following interface: + +```go +type Foo interface { + Bar(int, int) string +} + +``` + +To stub `Bar` method, we can use something like this: +```go +When(mock.Bar(Exact(1), Exact(2))).ThenReturn("some value") +``` + +However, this can be simplified to: +```go +When(mock.Bar(1, 2)).ThenReturn("some value") +``` + +In short, you can omit using matchers when you want to match exact values, but they all should be exact. +For example, this will not work: +```go +When(mock.Bar(1, Exact(2))).ThenReturn("some value") +``` diff --git a/docs/features/parallel-execution.md b/docs/features/parallel-execution.md new file mode 100644 index 0000000..f41ef0f --- /dev/null +++ b/docs/features/parallel-execution.md @@ -0,0 +1,72 @@ +# Parallel execution + +## Parallelism + +It is possible to run multiple tests with mockio in parallel using the `--parallel` option. This option is available in the `test` and `run` commands. + +## Concurrency + +Library supports invoking stubbed methods from different goroutine. + +```go +func TestParallelSuccess(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + wg := sync.WaitGroup{} + wg.Add(2) + When(greeter.Greet("John")).ThenReturn("hello world") + go func() { + greeter.Greet("John") + wg.Done() + }() + go func() { + greeter.Greet("John") + wg.Done() + }() + wg.Wait() + Verify(greeter, Times(2)).Greet("John") +} +``` + +However, library does not support stubbing methods from different goroutine. +This test will result in error: + +```go +func TestParallelFail(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + When(greeter.Greet("John")).ThenReturn("hello world") + wg.Done() + }() + go func() { + When(greeter.Greet("John")).ThenReturn("hello world") + wg.Done() + }() + wg.Wait() + if greeter.Greet("John") != "hello world" { + t.Error("Expected 'hello world'") + } +} +``` + +The main rule is that call to `When` should be in the same goroutine in which the mock is created. + +Also, each time you create a mock in a newly created goroutine, you need to call `SetUp(t)` again to initialize the mockio library in that goroutine. + +```go +func TestParallelSuccess(t *testing.T) { + SetUp(t) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("John")).ThenReturn("hello world") + wg.Done() + }() + wg.Wait() +} +``` \ No newline at end of file diff --git a/docs/features/verification.md b/docs/features/verification.md new file mode 100644 index 0000000..dc0426c --- /dev/null +++ b/docs/features/verification.md @@ -0,0 +1,132 @@ +# Verification + +We will use the following interface for the examples: +```go +package main + +import ( + . "github.com/ovechkin-dm/mockio/mock" + "testing" +) + +type Greeter interface { + Greet(name any) string +} + +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + +} +``` + +## Verify + +To verify that a method was called, use the `Verify` function. +If the method was called, the test will pass. If the method was not called, the test will fail. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + Verify(greeter, Once()).Greet("John") +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + Verify(greeter, Once()).Greet("Jane") +} +``` + +### AtLeastOnce + +Verify that a method was called at least once: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + Verify(greeter, AtLeastOnce()).Greet("John") +} +``` + +### Once + +Verify that a method was called exactly once: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + Verify(greeter, Once()).Greet("John") +} +``` + +### Times + +Verify that a method was called a specific number of times: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + greeter.Greet("John") + Verify(greeter, Times(2)).Greet("John") +} +``` + + +## VerifyNoMoreInteractions + +To verify that no other methods were called on the mock object, use the `VerifyNoMoreInteractions` function. +It will fail the test if there are any unverified calls. + +This test will succeed: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("Jane")).ThenReturn("hello world") + greeter.Greet("John") + Verify(greeter, Once()).Greet("John") + VerifyNoMoreInteractions(greeter) +} +``` + +This test will fail: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("John")).ThenReturn("hello world") + greeter.Greet("John") + VerifyNoMoreInteractions(greeter) +} +``` + +## Verify after `ThenReturn` + +Since it is common to actually verify that a stub was used correctly, you can use the `Verify` function after the `ThenReturn` function: +```go +func TestSimple(t *testing.T) { + SetUp(t) + greeter := Mock[Greeter]() + When(greeter.Greet("John")).ThenReturn("hello world").Verify(Once()) + greeter.Greet("John") + VerifyNoMoreInteractions(greeter) +} +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..fc87e50 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,73 @@ +# Mockio + +Golang library for mocking without code generation, inspired by Mockito. + +## Installing library + +Install latest version of the library using `go get` command: + +```bash +go get -u github.com/ovechkin-dm/mockio +``` + +## Creating test + +Let's create an interface that we want to mock: + +```go +type Greeter interface { + Greet(name string) string +} +``` + +Now we will use `dot import` to simplify the usage of the library: + +```go +import ( + ."github.com/ovechkin-dm/mockio/mock" + "testing" +) +``` + +Now we can create a mock for the `Greeter` interface, and test it's method `Greet`: + +```go +func TestGreet(t *testing.T) { + SetUp(t) + m := Mock[Greeter]() + WhenSingle(m.Greet("John")).ThenReturn("Hello, John!") + if m.Greet("John") != "Hello, John!" { + t.Fail() + } +} +``` + +## Full example +Here is the full listing for our simple test: + +```go +package main + +import ( + . "github.com/ovechkin-dm/mockio/mock" + "testing" +) + +type Greeter interface { + Greet(name string) string +} + +func TestGreet(t *testing.T) { + SetUp(t) + m := Mock[Greeter]() + WhenSingle(m.Greet("John")).ThenReturn("Hello, John!") + if m.Greet("John") != "Hello, John!" { + t.Fail() + } +} + +``` + +That's it! You have created a mock for the `Greeter` interface without any code generation. +As you can see, the library is very simple and easy to use. +And no need to generate mocks for your interfaces. diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 0000000..76c30b3 --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,20 @@ +# Limitations + +## Architecture + +Because library uses assembly code to generate mocks, it is not possible to use it in pure Go code. +This means that you cannot use it in a project that is intended to be cross-compiled to multiple platforms. + +For now supported platforms are: + +- AMD64 +- ARM64 (M1-M3 macs are supported) + +This list may be extended in the future. + +## Backwards compatibility and new Go versions + +This library is tested for GO 1.18 up to 1.23 + +Caution: there is no guarantee that it will work with future versions of Go. +However there is not much that can break the library, so it should be easy to fix it if it stops working. As of latest mockio version, almost all of dependencies on golang internal runtime features were removed. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..df03405 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +mike @ git+https://github.com/jimporter/mike.git +mkdocs +mkdocs-glightbox +mkdocs-open-in-new-tab +mkdocs-material +cairosvg +pillow \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..0d7a260 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,83 @@ +site_name: mockio +site_url: https://ovechkin-dm.github.io/mockio/ +site_description: >- + Mock library for Go + +repo_name: ovechkin-dm/mockio +repo_url: https://github.com/ovechkin-dm/mockio + +theme: + name: material + icon: + logo: fontawesome/brands/golang + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: purple + toggle: + icon: material/brightness-4 + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: purple + toggle: + icon: material/brightness-7 + name: Switch to dark mode + features: + - content.code.annotate + - content.code.copy + - navigation.indexes + - navigation.sections + - navigation.tracking + - toc.follow +markdown_extensions: + - admonition + - attr_list + - md_in_html + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + auto_title: true + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + + +nav: + - Home: index.md + - Features: + - Method stubbing: features/method-stubbing.md + - Matchers: features/matchers.md + - Verification: features/verification.md + - Configuration: features/configuration.md + - Argument captors: features/captors.md + - Parallel execution: features/parallel-execution.md + - Error reporting: features/error-reporting.md + - Limitations: limitations.md + +extra_css: + - stylesheets/extra.css + +extra_javascript: + - https://unpkg.com/tablesort@5.3.0/dist/tablesort.min.js + - javascripts/tablesort.js + +extra: + version: + provider: mike + +plugins: + - glightbox + - mike: + alias_type: copy + canonical_version: latest + - open-in-new-tab + - search + - social \ No newline at end of file