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

Refactoring Clients usage #20

Merged
merged 11 commits into from
May 21, 2024
Merged

Refactoring Clients usage #20

merged 11 commits into from
May 21, 2024

Conversation

erdimaden
Copy link
Contributor

@erdimaden erdimaden commented May 20, 2024

What changed? Why?

  • Removed axios-mock-adapter library and replaced existing usage with jest mock functions.
  • Implemented API mock function tests using toHaveBeenCalledWith and toHaveBeenCalledTimes.
  • Replaced model access with static usage of Coinbase.apiClients.

Qualified Impact

};

export const walletsApiMock = {
getWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })),
Copy link
Contributor

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).

Copy link
Contributor

@alex-stone alex-stone May 20, 2024

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.

Copy link
Contributor Author

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.

@erdimaden erdimaden force-pushed the feat/apiClients-refactoring branch from ab3bef5 to ce39bc7 Compare May 20, 2024 21:14
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 } }),
Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Contributor Author

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()

@@ -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 = {
Copy link
Contributor

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 befores to ensure the expectations are being set at the correct point of the test lifecycle.

Copy link
Contributor

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.

@erdimaden erdimaden force-pushed the feat/apiClients-refactoring branch from a4e4896 to a507f49 Compare May 21, 2024 06:10
Comment on lines +24 to +40
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 } };
});
Copy link
Contributor

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.

Copy link
Contributor Author

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.

} catch (error) {
expect(error).toBeInstanceOf(APIError);
}
const getAddressBalance = jest.fn().mockRejectedValue(new APIError(""));
Copy link
Contributor

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

Copy link
Contributor Author

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.

Comment on lines +16 to +18
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);
Copy link
Contributor

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.

Copy link
Contributor Author

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.

};

export const walletsApiMock = {
getWallet: jest.fn().mockResolvedValue(Promise.resolve({ data: VALID_WALLET_MODEL })),
Copy link
Contributor Author

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.

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 } }),
Copy link
Contributor Author

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()

} catch (error) {
expect(error).toBeInstanceOf(APIError);
}
const getAddressBalance = jest.fn().mockRejectedValue(new APIError(""));
Copy link
Contributor Author

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.

Comment on lines +16 to +18
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);
Copy link
Contributor Author

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.

Comment on lines 12 to 31
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 };
});
Copy link
Contributor Author

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", () => {
Copy link
Contributor Author

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();
Copy link
Contributor Author

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 = {};
Copy link
Contributor Author

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.

Comment on lines +115 to +116
expect(getAddressBalance).toHaveBeenCalledWith(address.getWalletId(), address.getId(), assetId);
expect(getAddressBalance).toHaveBeenCalledTimes(1);
Copy link
Contributor Author

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

@erdimaden
Copy link
Contributor Author

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.

@erdimaden erdimaden merged commit 5ab03a9 into master May 21, 2024
6 checks passed
@erdimaden erdimaden deleted the feat/apiClients-refactoring branch June 21, 2024 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants