Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement service and endpoint mocking support in tests #1005

Merged
merged 7 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions docs/develop/mocking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
seotitle: Mocking out your APIs and services for testing
seodesc: Learn how to mock out your APIs and services for testing, and how to use the built-in mocking support in Encore.
title: Mocking
subtitle: Testing your application in isolation
infobox: {
title: "Testing",
import: "encore.dev/et",
}
---

Encore comes with built-in support for mocking out APIs and services, which makes it easier to test your application in
isolation.

## Mocking Endpoints

Let's say you have an endpoint that calls an external API in our `products` service:

```go
//encore:api private
func GetPrice(ctx context.Context, p *PriceParams) (*PriceResponse, error) {
// Call external API to get the price
}
```

When testing this function, you don't want to call the real external API since that would be slow and cause your tests
to fail if the API is down. Instead, you want to mock out the API call and return a fake response.

In Encore, you can do this by adding a mock implementation of the endpoint using the `et.MockEndpoint` function inside your test:

```go
package shoppingcart

import (
"context"
"testing"

"encore.dev/et" // Encore's test support package

"your_app/products"
)


func Test_Something(t *testing.T) {
t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering

// Create a mock implementation of pricing API which will only impact this test and any sub-tests
et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
return &products.PriceResponse{Price: 100}, nil
})

// ... the rest of your test code here ...
}
```

When any code within the test, or any sub-test calls the `GetPrice` API, the mock implementation will be called instead.
The mock will not impact any other tests running in parallel. The function you pass to `et.MockEndpoint` must have the same
signature as the real endpoint.

If you want to mock out the API for all tests in the package, you can add the mock implementation to the `TestMain` function:

```go
package shoppingcart

import (
"context"
"os"
"testing"

"encore.dev/et"

"your_app/products"
)

func TestMain(m *testing.M) {
// Create a mock implementation of pricing API which will impact all tests within this package
et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
return &products.PriceResponse{Price: 100}, nil
})

// Now run the tests
os.Exit(m.Run())
}
```

Mocks can be changed at any time, including removing them by setting the mock implementation to `nil`.

## Mocking services

As well as mocking individual APIs, you can also mock entire services. This can be useful if you want to inject a different
set of dependencies into your service for testing, or a service that your code depends on. This can be done using the
`et.MockService` function:

```go
package shoppingcart

import (
"context"
"testing"

"encore.dev/et" // Encore's test support package

"your_app/products"
)

func Test_Something(t *testing.T) {
t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering

// Create a instance of the products service which will only impact this test and any sub-tests
et.MockService("products", &products.Service{
SomeField: "a testing value",
})

// ... the rest of your test code here ...
}
```

When any code within the test, or any sub-test calls the `products` service, the mock implementation will be called instead.
Unlike `et.MockEndpoint`, the mock implementation does not need to have the same signature, and can be any object. The only requirement
is that any of the services APIs that are called during the test must be implemented by as a receiver method on the mock object.
(This also includes APIs that are defined as package level functions in the service, and are not necessarily defined as receiver methods
on that services struct).

To help with compile time saftey on service mocking, for every service Encore will automatically generate an `Interface` interface
which contains all the APIs defined in the service. This interface can be passed as a generic argument to `et.MockService` to ensure
that the mock object implements all the APIs defined in the service:

```go
type myMockObject struct{}

func (m *myMockObject) GetPrice(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
return &products.PriceResponse{Price: 100}, nil
}

func Test_Something(t *testing.T) {
t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering

// This will cause a compile time error if myMockObject does not implement all the APIs defined in the products service
et.MockService[products.Interface]("products", &myMockObject{})
}
```
\
### Automatic generation of mock objects

Thanks to the generated `Interface` interface, it's possible to automatically generate mock objects for your services using
either [Mockery](https://vektra.github.io/mockery/latest/) or [GoMock](https://github.com/uber-go/mock).
10 changes: 9 additions & 1 deletion docs/develop/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ In general, Encore applications tend to focus more on integration tests
compared to traditional applications that are heavier on unit tests.
This is nothing to worry about and is the recommended best practice.

### Service Structs

In tests, [service structs](/docs/primitives/services-and-apis/service-structs) are initialised on demand when the first
API call is made to that service and then that instance of the service struct for all future tests. This means your tests
can run faster as they don't have to each initialise all the service struct's each time a new test starts.

However, in some situations you might be storing state in the service struct that would interfere with other tests. When
you have a test you want to have it's own instance of the service struct, you can use the `et.EnableServiceInstanceIsolation()` function within the test to enable this for just that test, while the rest of your tests will continue to use the shared instance.

## Test-only infrastructure

Encore allows tests to define infrastructure resources specifically for testing.
Expand All @@ -59,4 +68,3 @@ It lets you run unit tests directly from within your IDE with support for debug
There's no official VS Code plugin available yet, but we are happy to include your contribution if you build one. Reach out on [Slack](/slack) if you need help to get started.

For advice on debugging when using VS Code, see the [Debugging docs](/docs/how-to/debug).

6 changes: 6 additions & 0 deletions docs/menu.cue
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,12 @@
text: "Testing"
path: "/develop/testing"
file: "develop/testing"
inline_menu: [{
kind: "basic"
text: "Mocking"
path: "/develop/testing/mocking"
file: "develop/mocking"
}]
}, {
kind: "basic"
text: "Validation"
Expand Down
11 changes: 9 additions & 2 deletions docs/menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,14 @@
{
"Path": "/develop/testing",
"Title": "Testing",
"DocSlug": "develop/testing"
"DocSlug": "develop/testing",
"Children": [
{
"Path": "/develop/testing/mocking",
"Title": "Mocking",
"DocSlug": "develop/mocking"
}
]
},
{
"Path": "/develop/validation",
Expand Down Expand Up @@ -530,4 +537,4 @@
}
]
}
]
]
Loading
Loading