0

I'm writing some code that uses a library called Vault. In this library we have a Client. My code makes use of this Client but I want to be able to easily test the code that uses it. I use only a couple methods from the library so I ended up creating an interface:

type VaultClient interface {
    Logical() *api.Logical
    SetToken(v string)
    NewLifetimeWatcher(i *api.LifetimeWatcherInput) (*api.LifetimeWatcher, error)
}

Now if my code is pointed at this interface everything is easily testable.. Except let's look at the Logical() method. It returns a struct here. My issue is that this Logical struct also has methods on it that allow you to Read, Write, ex:

func (c *Logical) Read(path string) (*Secret, error) {
    return c.ReadWithData(path, nil)
}

and these are being used in my project as well to do something like:

{{ VaultClient defined above }}.Logical().Write("something", something)

Here is the issue. The Logical returned from the call to .Logical() has a .Write and .Read method that I can't reach to mock. I don't want all the logic within those methods to run in my tests.

Ideally I'd like to be able to do something similar to what I did above and create an interface for Logical as well. I'm relatively new to Golang, but I'm struggling with the best approach here. From what I can tell that's not possible. Embedding doesn't work like inheritance so it seems like I have to return a Logical. That leaves my code unable to be tested as simply as I would like because all the logic within a Logical's methods can't be mocked.

I'm sort of at a loss here. I have scoured Google for an answer to this but nobody ever talks about this scenario. They only go as far as I went with the initial interface for the client.

Is this a common scenario? Other libraries I've used don't return structs like Logical. Instead they typically just return a bland struct that holds data and has no methods.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
user2767260
  • 283
  • 2
  • 10
  • Another way of testing is to configure the struct for testing (in this case the [`Client`](https://github.com/hashicorp/vault/blob/62d0ecff3d155c75f932075da47fea003fa7a772/api/client.go#L397)). I haven't tried but I think you can pass `*http.Client` in the NewClient. This way you can setup a test server with `srv := httptest.NewServer` and define the vault server behavior by yourself, then pass the `srv.Client()` to vault NewClient config for testing. – wijayaerick Mar 03 '21 at 04:13
  • I'm hoping to avoid testing things with an `httptest.NewServer` if at all possible. It feels more tedious that should be necessary for something like this. I'm totally open to the idea if there's no alternatives though! :) – user2767260 Mar 03 '21 at 04:56
  • @user2767260 You can change your original interface to return another interface, i.e. change `Logical() *api.Logical` to `Logical() ApiLogical` where `ApiLogical` is an interface declared by you and whose methods match those of `*api.Logical`, then you would use thin wrappers to satisfy the new `VaultClient` interface. As an example see: https://stackoverflow.com/a/58022874/965900 – mkopriva Mar 03 '21 at 07:47
  • @mkopriva Thank you! That is exactly what I was looking for. If you want to add an answer down below I'd be happy to accept it! :D – user2767260 Mar 03 '21 at 14:52

1 Answers1

0
package usecasevaultclient

// usecase.go
type VaultClient interface {
    Logical() *api.Logical
    SetToken(v string)
    NewLifetimeWatcher(i *api.LifetimeWatcherInput) (*api.LifetimeWatcher, error)
}

type vaultClient struct {
   repo RepoVaultClient
}

// create new injection
func NewVaultClient(repo RepoVaultClient) VaultClient {
  return &vaultClient{repo}
}

func(u *vaultClient) Logical() *api.Logical {
 // do your logic and call the repo of
   u.repo.ReadData()
   u.repo.WriteData()
}
func(u *vaultClient) SetToken(v string) {}
func(u *vaultClient) NewLifetimeWatcher(i *api.LifetimeWatcherInput) (*api.LifetimeWatcher, error)

// interfaces.go
type RepoVaultClient interface {
   ReadData() error
   WriteData() error
}

// repo_vaultclient_mock.go
import "github.com/stretchr/testify/mock"

type MockRepoVaultClient struct {
   mock.Mock
}

func (m *MockRepoVaultClient) ReadData() error {
      args := m.Called()
      return args.Error(0)
}

func (m *MockRepoVaultClient) WriteData() error {
      args := m.Called()
      return args.Error(0)
}


// vaultClient_test.go

func TestLogicalShouldBeSuccess(t *testing.T) {
  mockRepoVaultClient = &MockRepoVaultClient{}

  useCase := NewVaultClient(mockRepoVaultClient)

  mockRepoVaultClient.On("ReadData").Return(nil)
  mockRepoVaultClient.On("WriteData").Return(nil)

  // your logics gonna make this response as actual what u implemented
  response := useCase.Logical()

  assert.Equal(t, expected, response)
}

if you want to test the interface of Logical you need to mock the ReadData and WriteData , with testify/mock so u can defined the respond of return of those methods and you can compare it after you called the new injection of your interface

Pocket
  • 309
  • 3
  • 11
  • Hey Pocket, I updated my initial question a little bit to add more explanation. It's not so much that I need to perform logic within the `Logical` method, but instead I need to be able to test the methods that exist for a `Logical`. I added an example of my usage in the question above. `VaultClient.Logical().Write("something", something)` is my usage. The `VaultClient` is an interface so that's easy to test, but the `.Write` can't be mocked because it seems I _have_ to return an actual `Logical` from the `VaultClient` nonetheless. This seems to heavily conflict with my ability to test. – user2767260 Mar 03 '21 at 05:00
  • i think u should create the interface Repo what i wrote above, so in the method ReadData you should call the vault Read from logical, because what i know that you want test the interface you pass there, – Pocket Mar 03 '21 at 06:48