Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

feat(mock-tests): Mock Arc Client for testing purposes #26

Merged
merged 6 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ custom features to work with multiple nodes and retry logic.

- [x] Possibility to use custom http client [WithHTTPClient](https://github.com/bitcoin-sv/go-broadcast-client/blob/main/broadcast/broadcast-client/client_builder.go#L19)

- [x] Mock Client for testing purposes [details](#MockClientBuilder)

## How to use it?

### Create client
Expand Down Expand Up @@ -302,3 +304,45 @@ type Transaction struct {
| 109 | `REJECTED` | The transaction has been rejected by the Bitcoin network.

*Source* [Arc API](https://github.com/bitcoin-sv/arc/blob/main/README.md)


## MockClientBuilder

Mock Client allows you to test your code without using an actual client and without connecting to any nodes.

### WithMockArc Method

This method allows you to create a client with a different Mock Type passed as parameter.

```go
client := broadcast_client_mock.Builder().
WithMockArc(broadcast_client_mock.MockSucces).
Build()
```

| MockType | Description
|---------------|-----------------------------------------------------------------------------------
| `MockSucces` | Client will return a successful response from all methods.
| `MockFailure` | Client will return an error that no miner returned a response from all methods.
| `MockTimeout` | Client will return a successful response after a timeout from all methods.

#### MockTimeout

MockTimeout will return a successfull response after around ~10ms more than the timeout provided in the context that is passed to it's method.


Example:

```go
client := broadcast_client_mock.Builder().
WithMockArc(broadcast_client_mock.MockTimeout).
Build()

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Creating a timeout context with 2 seconds timeout
defer cancel()

result, err := client.GetPolicyQuote(ctx) // client will return a response after around 2 seconds and 10 milliseconds, therefore exceeding the timeout
```

If you pass the context without a timeout, the client will instantly return a successful response (just like from a MockSuccess type).

59 changes: 59 additions & 0 deletions broadcast/broadcast-client-mock/mock_client_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package broadcast_client_mock

import (
broadcast_api "github.com/bitcoin-sv/go-broadcast-client/broadcast"
"github.com/bitcoin-sv/go-broadcast-client/broadcast/internal/arc/mocks"
"github.com/bitcoin-sv/go-broadcast-client/broadcast/internal/composite"
)

// MockType is an enum that is used as parameter to WithMockArc
// client builder in order to create different types of mock.
type MockType int

const (
MockSuccess MockType = iota
MockFailure
MockTimeout
)

type builder struct {
factories []composite.BroadcastFactory
}

// Builder is used to prepare the mock broadcast client. It is recommended
// to use that builder for creating the mock broadcast client.
func Builder() *builder {
return &builder{}
}

// WithMockArc creates a mock client for testing purposes. It takes mock type as argument
// and creates a mock client that satisfies the client interface with methods that return
// success or specific error based on this mock type argument. It allows for creating
// multiple mock clients.
func (cb *builder) WithMockArc(mockType MockType) *builder {
var clientToReturn broadcast_api.Client

switch mockType {
case MockSuccess:
clientToReturn = mocks.NewArcClientMock()
case MockFailure:
clientToReturn = mocks.NewArcClientMockFailure()
case MockTimeout:
clientToReturn = mocks.NewArcClientMockTimeout()
default:
clientToReturn = mocks.NewArcClientMock()
}

cb.factories = append(cb.factories, func() broadcast_api.Client {
return clientToReturn
})
return cb
}

// Build builds the broadcast client based on the provided configuration.
func (cb *builder) Build() broadcast_api.Client {
if len(cb.factories) == 1 {
return cb.factories[0]()
}
return composite.NewBroadcasterWithDefaultStrategy(cb.factories...)
}
254 changes: 254 additions & 0 deletions broadcast/broadcast-client-mock/mock_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package broadcast_client_mock

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/bitcoin-sv/go-broadcast-client/broadcast"
)

func TestMockClientSuccess(t *testing.T) {
t.Run("Should successfully query for Policy Quote from mock Arc Client with Success Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockSuccess).
Build()

// when
result, err := broadcaster.GetPolicyQuote(context.Background())

// then
assert.NoError(t, err)
assert.NotNil(t, result)
})

t.Run("Should successfully query for Fee Quote from mock Arc Client with Success Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockSuccess).
Build()

// when
result, err := broadcaster.GetFeeQuote(context.Background())

// then
assert.NoError(t, err)
assert.NotNil(t, result)
})

t.Run("Should successfully query for transaction from mock Arc Client with Success Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockSuccess).
Build()

// when
result, err := broadcaster.QueryTransaction(context.Background(), "test-txid")

// then
assert.NoError(t, err)
assert.NotNil(t, result)
})

t.Run("Should return successful submit transaction response from mock Arc Client with Success Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockSuccess).
Build()

// when
result, err := broadcaster.SubmitTransaction(context.Background(), &broadcast.Transaction{RawTx: "test-rawtx"})

// then
assert.NoError(t, err)
assert.NotNil(t, result)
})

t.Run("Should return successful submit batch transactions response from mock Arc Client with Success Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockSuccess).
Build()

// when
result, err := broadcaster.SubmitBatchTransactions(context.Background(), []*broadcast.Transaction{{RawTx: "test-rawtx"}, {RawTx: "test2-rawtx"}})

// then
assert.NoError(t, err)
assert.NotNil(t, result)
})
}

func TestMockClientFailure(t *testing.T) {
t.Run("Should return error from GetPolicyQuote method of mock Arc Client with Failure Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockFailure).
Build()

// when
result, err := broadcaster.GetPolicyQuote(context.Background())

// then
assert.Error(t, err)
assert.Nil(t, result)
assert.EqualError(t, err, broadcast.ErrNoMinerResponse.Error())
})

t.Run("Should return error from GetFeeQuote method of mock Arc Client with Failure Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockFailure).
Build()

// when
result, err := broadcaster.GetFeeQuote(context.Background())

// then
assert.Error(t, err)
assert.Nil(t, result)
assert.EqualError(t, err, broadcast.ErrNoMinerResponse.Error())
})

t.Run("Should return error from QueryTransaction method of mock Arc Client with Failure Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockFailure).
Build()

// when
result, err := broadcaster.QueryTransaction(context.Background(), "test-txid")

// then
assert.Error(t, err)
assert.Nil(t, result)
assert.EqualError(t, err, broadcast.ErrAllBroadcastersFailed.Error())
})

t.Run("Should return error from SubmitTransaction method of mock Arc Client with Failure Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockFailure).
Build()

// when
result, err := broadcaster.SubmitTransaction(context.Background(), &broadcast.Transaction{RawTx: "test-rawtx"})

// then
assert.Error(t, err)
assert.Nil(t, result)
assert.EqualError(t, err, broadcast.ErrAllBroadcastersFailed.Error())
})

t.Run("Should return error from SubmitBatchTransaction method of mock Arc Client with Failure Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockFailure).
Build()

// when
result, err := broadcaster.SubmitBatchTransactions(context.Background(), []*broadcast.Transaction{{RawTx: "test-rawtx"}, {RawTx: "test2-rawtx"}})

// then
assert.Error(t, err)
assert.Nil(t, result)
assert.EqualError(t, err, broadcast.ErrAllBroadcastersFailed.Error())
})
}

func TestMockClientTimeout(t *testing.T) {
const defaultTestTime = 200*time.Millisecond

t.Run("Should successfully query for Policy Quote after a timeout period from mock Arc Client with Timeout Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockTimeout).
Build()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTime)
defer cancel()
startTime := time.Now()

// when
result, err := broadcaster.GetPolicyQuote(ctx)

// then
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Greater(t, time.Since(startTime), defaultTestTime)
})

t.Run("Should successfully query for Fee Quote after a timeout period from mock Arc Client with Timeout Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockTimeout).
Build()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTime)
defer cancel()
startTime := time.Now()

// when
result, err := broadcaster.GetFeeQuote(ctx)

// then
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Greater(t, time.Since(startTime), defaultTestTime)
})

t.Run("Should successfully query for transaction after a timeout period from mock Arc Client with Timeout Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockTimeout).
Build()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTime)
defer cancel()
startTime := time.Now()

// when
result, err := broadcaster.QueryTransaction(ctx, "test-txid")

// then
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Greater(t, time.Since(startTime), defaultTestTime)
})

t.Run("Should return successful submit transaction response after a timeout period from mock Arc Client with Timeout Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockTimeout).
Build()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTime)
defer cancel()
startTime := time.Now()

// when
result, err := broadcaster.SubmitTransaction(ctx, &broadcast.Transaction{RawTx: "test-rawtx"})

// then
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Greater(t, time.Since(startTime), defaultTestTime)
})

t.Run("Should return successful submit batch transactions response after a timeout period from mock Arc Client with Timeout Mock Type", func(t *testing.T) {
// given
broadcaster := Builder().
WithMockArc(MockTimeout).
Build()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTime)
defer cancel()
startTime := time.Now()

// when
result, err := broadcaster.SubmitBatchTransactions(ctx, []*broadcast.Transaction{{RawTx: "test-rawtx"}, {RawTx: "test2-rawtx"}})

// then
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Greater(t, time.Since(startTime), defaultTestTime)
})
}
2 changes: 1 addition & 1 deletion broadcast/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// It should be returned for all defined clients in the future.
var ErrClientUndefined = errors.New("client is undefined")

// ErrAllBroadcastersFailed is returned when all configured broadcasters failed to broadcast the transaction.
// ErrAllBroadcastersFailed is returned when all configured broadcasters failed to query or broadcast the transaction.
var ErrAllBroadcastersFailed = errors.New("all broadcasters failed")

// ErrURLEmpty is returned when the API URL is empty.
Expand Down
4 changes: 4 additions & 0 deletions broadcast/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import (
"context"
)

// FeeQuoter it the interface that wraps GetFeeQuote method.
// It retrieves the Fee Quote from the configured miners.
type FeeQuoter interface {
GetFeeQuote(ctx context.Context) ([]*FeeQuote, error)
}

// PolicyQuoter it the interface that wraps GetPolicyQuote method.
// It retrieves the Policy Quote from the configured miners.
type PolicyQuoter interface {
GetPolicyQuote(ctx context.Context) ([]*PolicyQuoteResponse, error)
}
Expand Down
Loading
Loading