Testing
Writing automated tests for your software is important to ensure no regressions sneak in, everything works as expected and that you’re not corrupting user data.
The Platform SDK is designed in such a way that testing it is quite simple because everything can be mocked and some core components can even be completely replaced with custom implementations.
Prerequisites
We recommend to use Jest for testing but any other framework will do. You will also need PNPM and Rush to install and build the mono-repository.
Example
We will use the Portfolio class as an example as it exposes a few underlying components of the Platform SDK.
Mocking Network Requests
This one isn’t specific to the Platform SDK but we do recommend to use nock for mocking of network requests. This will make your life a lot easier when you are bootstrapping a coin in tests because a coin will always try to connect to a network to gather some information.
1beforeAll(() => nock.disableNetConnect()); 2 3beforeEach(async () => { 4 nock.cleanAll(); 5 6 nock(/.+/) 7 .get("/api/node/configuration") 8 .reply(200, require("test/fixtures/client/configuration.json")) 9 .get("/api/peers")10 .reply(200, require("test/fixtures/client/peers.json"))11 .get("/api/node/configuration/crypto")12 .reply(200, require("test/fixtures/client/cryptoConfiguration.json"))13 .get("/api/node/syncing")14 .reply(200, require("test/fixtures/client/syncing.json"));15});
This is what a typical network mocking setup would look like if you are writing a test that would involve the @ardenthqhq/sdk-ark
integration when bootstrapping the environment. This ensures that the tests won’t be able to use your real network connection which will ensure that your tests throw an exception if you are missing any mocks.
If you are missing any mocks you should make sure that you add them to guarantee consistent results when running your tests but also ensure that they are as fast as they possibly can be.
Mocking Implementations
Every now and then you’ll find yourself in a situation where you need to test that a function behaves differently depending on certain variables like the type of network or kind of configuration it is using.
This is where Jest comes in handy with the ability to mock return values or even mock the implementation of a function.
1it("should aggregate the balances of all wallets", async () => { 2 const [a, b, c] = await Promise.all([ 3 profile.wallets().importByMnemonic("a", "ARK", "ark.devnet"), 4 profile.wallets().importByMnemonic("b", "ARK", "ark.devnet"), 5 profile.wallets().importByMnemonic("c", "ARK", "ark.devnet"), 6 ]); 7 a.data().set(WalletData.Balance, 1e8); 8 b.data().set(WalletData.Balance, 1e8); 9 c.data().set(WalletData.Balance, 1e8);10 11 jest.spyOn(a.network(), "isLive").mockReturnValue(true);12 jest.spyOn(a.network(), "isTest").mockReturnValue(false);13 jest.spyOn(a.network(), "ticker").mockReturnValue("ARK");14 15 jest.spyOn(b.network(), "isLive").mockReturnValue(true);16 jest.spyOn(b.network(), "isTest").mockReturnValue(false);17 jest.spyOn(b.network(), "ticker").mockReturnValue("ARK");18 19 jest.spyOn(c.network(), "isLive").mockReturnValue(true);20 jest.spyOn(c.network(), "isTest").mockReturnValue(false);21 jest.spyOn(c.network(), "ticker").mockReturnValue("ARK");22 23 await container.get<IExchangeRateService>(Identifiers.ExchangeRateService).syncAll(profile, "ARK");24 25 expect(profile.portfolio().breakdown()[0].source).toBe(3);26 expect(profile.portfolio().breakdown()[0].target).toBe(0.00015144);27 expect(profile.portfolio().breakdown()[0].shares).toBe(100);28});
In the above example we are testing that the portfolio of a profile can be calculated. There is one rule that needs to be kept in mind when aggregating the portfolio. That rule is that only wallets from production networks should be taken into account which means ARK will be part of your portfolio but DARK won’t be because it isn’t a real coin.
To achieve this we import 3 wallets that all belong to the same network so that we don’t require duplicate network mocks. Then we mock them to be all wallets that use a live network and finally try to access to our portfolio to see that all our data matches what we would expect it to be.
Swapping Implementations
Mocking is a handy tool when you quickly want to modify smaller behaviours or functions but what if you want to change how a whole class behaves? That’s where swapping implementations comes in.
1export class StubStorage implements Storage { 2 readonly #storage; 3 4 public constructor() { 5 try { 6 this.#storage = JSON.parse(readFileSync(resolve(__dirname, "env.json")).toString()); 7 } catch { 8 this.#storage = {}; 9 }10 }11 12 public async all<T = Record<string, unknown>>(): Promise<T> {13 return this.#storage;14 }15 16 public async get<T = any>(key: string): Promise<T | undefined> {17 return this.#storage[key];18 }19 20 public async set(key: string, value: string | object): Promise<void> {21 this.#storage[key] = value;22 23 writeFileSync(resolve(__dirname, "env.json"), JSON.stringify(this.#storage));24 }25 26 public async has(key: string): Promise<boolean> {27 return Object.keys(this.#storage).includes(key);28 }29 30 public async forget(key: string): Promise<void> {31 //32 }33 34 public async flush(): Promise<void> {35 //36 }37 38 public async count(): Promise<number> {39 return 0;40 }41 42 public async snapshot(): Promise<void> {43 //44 }45 46 public async restore(): Promise<void> {47 //48 }49}50 51// Ensure that we are overwriting the existing binding!52container.rebind(Identifiers.Storage, new StubStorage());
The above example is taken from the test suite of the profiles
package and shows you how to swap out a core component of the package with your own implementation. This example implements a storage that is mostly a stub and will only keep data in-memory for the duration of the test it is used in.
This is useful because it means we won’t have to deal with any kind of persistence issues or overlaps between tests and can preload a fixture for the storage if it is available.