Build on Flow: JS Testing - 4. Storage Inspection
Max Daunarovich
Posted on May 26, 2023
Intro
In the previous episode of our epic journey, we've discovered how we can interact with Flow Emulator by sending transactions and executing scripts. Today we will continue exploring available functions that help us build quick and easy to read tests for our Cadence contracts.
Reading simple values from contracts and accounts is a pretty easy task, even if you write all the Cadence code manually. But ensuring that account has proper paths setup or specific item in its collection could be tedious.
The Framework provides several methods to inspect account storage fast and painlessly.
Let's start by creating another test suit:
npx @onflow/flow-js-testing make storage-inspection
Inspect Available Paths
To gather all available paths on accounts we will call getPaths
function, which will return an object containing three Sets:
-
publicPaths
-
privatePaths
storagePaths
You can inspect them as you would do with any other Set.
Let's confirm that Alice
has all the necessary bits to operate FLOW token (all new accounts do, but this will simplify the process).
test("get paths", async () => {
const Alice = await getAccountAddress("Alice");
const { publicPaths, privatePaths, storagePaths } = await getPaths(Alice);
// Newly created account shall have 2 public and 1 storage slot occupied for FLOW Vault
expect(publicPaths.size).toBe(2);
expect(publicPaths.has("flowTokenBalance")).toBe(true);
expect(publicPaths.has("flowTokenReceiver")).toBe(true);
// Newly created account doesn't have any private paths
expect(privatePaths.size).toBe(0);
// And it should have single storage path for FLOW token vault
expect(storagePaths.size).toBe(1);
expect(storagePaths.has("flowTokenVault")).toBe(true);
});
Get Type Restrictions on Path
A common case is to ensure that account has specific capability in its account and ensure specific type restrictions on it to confirm that transactions would work correctly. We can get an extended version of paths with extra information about them via getPathsWithType
:
test("read path types", async () => {
const { publicPaths } = await getPathsWithType("Alice");
const refTokenBalance = publicPaths.flowTokenBalance;
expect(refTokenBalance).not.toBe(undefined);
expect(refTokenBalance.restrictionsList.has("A.ee82856bf20e2aa6.FungibleToken.Balance")).toBe(
true
);
expect(refTokenBalance.restrictionsList.size).toBe(1);
expect(refTokenBalance.haveRestrictions("FungibleToken.Balance")).toBe(true);
});
You can comment out those expect
statements and use console.log(refTokenBalance)
to see all available options you can use to assert it's correctness.
Read Storage Values
Now let's store a value in Alice's storage and try to read it via getStorageValue
. We need to pass account name (i.e. "Alice"
) or address we resolved with getAccountAddress
and storage path
test("read storage", async () => {
const Alice = await getAccountAddress("Alice");
await shallPass(
sendTransaction({
code: `
transaction{
prepare(signer: AuthAccount){
signer.save(42, to: /storage/answer)
}
}
`,
signers: [Alice],
})
);
const { storagePaths } = await getPaths(Alice);
const answer = await getStorageValue(Alice, "answer");
expect(answer).toBe("42");
expect(storagePaths.has("answer")).toBe(true);
// We can also try to read non-existing value, which shall be resolved to null
// Notice that we pass string here ;)
const empty = await getStorageValue("Alice", "empty");
expect(empty).toBe(null);
});
Storage Stats
Another useful method you can emply during account inspection is getStorageStats
- you might need one if you think users might hold huge collections and check how much of the storage it will occupy:
test("Read storage stats", async () => {
const { capacity, used } = await getStorageStats("Alice");
expect(capacity).toBe(100000);
expect(used > 0).toBe(true);
});
Jest Helpers
Last, but not the least, there are a couple Jest helpers to simplify the inspection process and shorten your tests.
shallHavePath(account, pathName)
This function will allow to quickly confirm that path exists on account. Note, that second argument contains a full path, including domain
test("get paths", async () => {
const Alice = await getAccountAddress("Alice");
await shallHavePath(Alice, "/public/flowTokenBalance");
await shallHavePath(Alice, "/public/flowTokenReceiver");
});
shallHaveStorageValue(account, {props})
Using this function, we can quickly check storage valueโboth complex and simple ones. Let's start with simple:
test("read storage", async () => {
const Alice = await getAccountAddress("Alice");
await shallPass(
sendTransaction({
code: `
transaction{
prepare(signer: AuthAccount){
signer.save(42, to: /storage/answer)
}
}
`,
signers: [Alice],
})
);
// This time we only need to path path in storage domain
await shallHaveStorageValue(Alice, {
pathName: "answer",
expect: "42",
});
});
Or if we know that value in storage is complex - let's say some NFT or Vault, then we can provide a key to how we can access it and check our expectations:
test("compare complex storage value", async () => {
const Alice = await getAccountAddress("Alice");
// We will read "balance" field of the value stored in "/storage/flowTokenVault" slot
await shallHaveStorageValue(Alice, {
pathName: "flowTokenVault",
key: "balance",
expect: "0.00100000",
});
});
This is it for today! ๐
Let us know if you think we've missed some valuable examples ๐
Good luck and have fun! ๐
Posted on May 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.