-
Notifications
You must be signed in to change notification settings - Fork 42
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
Refactoring Clients usage #20
Conversation
src/coinbase/tests/utils.ts
Outdated
}; | ||
|
||
export const walletsApiMock = { | ||
getWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of these are setting specific expectations on the mocks. We should be verifying that the expected arguments are being passed before we return a value (or error).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Started doing that here: https://github.com/coinbase/coinbase-sdk-nodejs/pull/21/files#diff-ac84f98e46bfd44c15fed0df3bb4e8c4865c2ba08401083898fd9dbb1bb4486eR49
Need to also assert on the request body.
We definitely need to be able to mock multiple responses for things like getAddress
when we derive multiple addresses in the instantiation flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have addressed the feedback. All mock outputs will now be generated dynamically. I will share more details in the PR.
ab3bef5
to
ce39bc7
Compare
src/coinbase/tests/utils.ts
Outdated
export const addressesApiMock = { | ||
requestFaucetFunds: jest.fn().mockResolvedValue({ data: { transaction_hash: "0xdeadbeef" } }), | ||
getAddress: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), | ||
getAddressBalance: jest.fn().mockResolvedValue({ data: { VALID_BALANCE_MODEL } }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are still returning hard-coded values, rather than ensuring that the returned values match what was passed in as parameters.
For example, if I the mock call is getAddressBalance(walletId, addressId, assetId)
, then I would expect to get back a Balance
model that has the same asset ID. Right now we are always returning VALID_BALANCE_MODEL, which means we are always returning an ETH balance even if the asset ID passed is something else like USDC.
So we should not be setting "mockResolvedValue" statically here; instead, we should set them on each test as appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these should all just be jest.fn(), no return values
Then the expectations should be set on each test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I changed with jest.fn()
src/coinbase/tests/address_test.ts
Outdated
@@ -101,85 +40,121 @@ describe("Address", () => { | |||
}); | |||
|
|||
it("should return the correct list of balances", async () => { | |||
axiosMock.onGet().reply(200, VALID_ADDRESS_BALANCE_LIST); | |||
Coinbase.apiClients.address = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way of replacing the value of Coinbase.apiClients.address with the existing ...addressesApiMock, and then the one method that we want to mock, seems inelegant.
Instead, I think apiClients.address should be set to a mock object for all of the tests in this file.
We should also be using more describe
and before
s to ensure the expectations are being set at the correct point of the test lifecycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. We also should more closely align the usage of describe
, before
, and it
to the Ruby SDK spec tests. The test frameworks across Node.js and Ruby are quite similar so there is an opportunity for us to have very tight alignment on the structure of our tests / precisely what tests get run.
a4e4896
to
a507f49
Compare
Coinbase.apiClients.address!.getAddressBalance = mockFn(request => { | ||
const { asset_id } = request; | ||
balanceModel = { | ||
amount: "1000000000000000000", | ||
asset: { | ||
asset_id, | ||
network_id: Coinbase.networkList.BaseSepolia, | ||
}, | ||
}; | ||
return { data: balanceModel }; | ||
}); | ||
Coinbase.apiClients.address!.listAddressBalances = mockFn(() => { | ||
return { data: VALID_ADDRESS_BALANCE_LIST }; | ||
}); | ||
Coinbase.apiClients.address!.requestFaucetFunds = mockFn(() => { | ||
return { data: { transaction_hash: transactionHash } }; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are these in the beforeAll
block? It might be better for us to place these closer to the test that they are used in. It is unclear which test they are used in with this setup. We can use nested describe
blocks to provide further context for the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I moved this beforeAll
setup under the .create
section. I used beforeAll because the mocks don't need to be updated after each test.
src/coinbase/tests/address_test.ts
Outdated
} catch (error) { | ||
expect(error).toBeInstanceOf(APIError); | ||
} | ||
const getAddressBalance = jest.fn().mockRejectedValue(new APIError("")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mixed usage of jest.fn() and mockFn
from ./utils.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch! I'm updating.
export const mockFn = (...args) => jest.fn(...args) as any; | ||
export const mockReturnValue = data => jest.fn().mockResolvedValue({ data }); | ||
export const mockReturnRejectedValue = data => jest.fn().mockRejectedValue(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets consider using the builtin jest methods directly to promote standard usage.
This added layer of indirection may not be necessary. If we prefer to keep this, lets use it consistently in all test files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using long jest.fn().mockResolvedValue or jest.fn(...args) statements takes up too much space in test cases, so I aimed to make them shorter. Sure, I'll leave the final decision to the team.
src/coinbase/tests/utils.ts
Outdated
}; | ||
|
||
export const walletsApiMock = { | ||
getWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have addressed the feedback. All mock outputs will now be generated dynamically. I will share more details in the PR.
src/coinbase/tests/utils.ts
Outdated
export const addressesApiMock = { | ||
requestFaucetFunds: jest.fn().mockResolvedValue({ data: { transaction_hash: "0xdeadbeef" } }), | ||
getAddress: jest.fn().mockResolvedValue({ data: VALID_ADDRESS_BALANCE_LIST }), | ||
getAddressBalance: jest.fn().mockResolvedValue({ data: { VALID_BALANCE_MODEL } }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I changed with jest.fn()
src/coinbase/tests/address_test.ts
Outdated
} catch (error) { | ||
expect(error).toBeInstanceOf(APIError); | ||
} | ||
const getAddressBalance = jest.fn().mockRejectedValue(new APIError("")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch! I'm updating.
export const mockFn = (...args) => jest.fn(...args) as any; | ||
export const mockReturnValue = data => jest.fn().mockResolvedValue({ data }); | ||
export const mockReturnRejectedValue = data => jest.fn().mockRejectedValue(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using long jest.fn().mockResolvedValue or jest.fn(...args) statements takes up too much space in test cases, so I aimed to make them shorter. Sure, I'll leave the final decision to the team.
src/coinbase/tests/wallet_test.ts
Outdated
walletId = randomUUID(); | ||
// Mock the API calls | ||
Coinbase.apiClients.wallet = walletsApiMock; | ||
Coinbase.apiClients.address = addressesApiMock; | ||
Coinbase.apiClients.wallet!.createWallet = mockFn(request => { | ||
const { network_id } = request.wallet; | ||
apiResponses[walletId] = { | ||
id: walletId, | ||
network_id, | ||
default_address: newAddressModel(walletId), | ||
}; | ||
return { data: apiResponses[walletId] }; | ||
}); | ||
Coinbase.apiClients.wallet!.getWallet = mockFn(walletId => { | ||
walletModel = apiResponses[walletId]; | ||
return { data: apiResponses[walletId] }; | ||
}); | ||
Coinbase.apiClients.address!.createAddress = mockFn(walletId => { | ||
return { data: apiResponses[walletId].default_address }; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use dynamic mocks to store wallet data in the apiResponses variable. This means that when a request is made to getWallet or createAddress, it only accesses the data for the specific walletId that was generated.
@@ -41,47 +44,84 @@ describe("Coinbase tests", () => { | |||
}); | |||
|
|||
describe("should able to interact with the API", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @yuga-cb mentioned, we will be using more describe blocks in this file. I will create a follow-up PR to implement these changes.
) | ||
.replyOnce(200, address); | ||
jest.clearAllMocks(); | ||
const getAddress = jest.fn(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, we mock the same function twice so I don't use utils function
@@ -42,7 +42,7 @@ export class Coinbase { | |||
Weth: "weth", | |||
}; | |||
|
|||
apiClients: ApiClients = {}; | |||
static apiClients: ApiClients = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Coinbase.apiClients is now globally accessible, so there's no need to pass clients to the Model class anymore.
expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId); | ||
expect(getAddressBalance).toHaveBeenCalledTimes(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To create tests similar to the ones in Ruby, I've added toHaveBeenCalledWith and toHaveBeenCalledTimes assertions.
Reference: https://github.com/coinbase/coinbase-sdk-ruby/blob/master/spec/unit/coinbase/address_spec.rb#L94
I've addressed the feedback. I will get this PR merged first to unblock @John-peterson-coinbase, and then I will create a follow-up PR for the describe changes. |
What changed? Why?
toHaveBeenCalledWith
andtoHaveBeenCalledTimes
.Qualified Impact