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

Commit

Permalink
feat(mock-tests): Mock Arc Client for testing purposes (#26)
Browse files Browse the repository at this point in the history
* feat(mock-tests): Mock Arc Client for testing purposes

* feat(BUX-222): timeout mock arc client added and godocs for creating a mock arc

* refactor: timeout mock client added, documentation and better packaging

* fix: readme link fix

* fix: linter error fix

* fix: unused client field
  • Loading branch information
kuba-4chain authored Sep 18, 2023
1 parent 0976520 commit ad2f0fc
Show file tree
Hide file tree
Showing 10 changed files with 611 additions and 1 deletion.
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

0 comments on commit ad2f0fc

Please sign in to comment.