Build on Flow: JS Testing - 2. Setup Emulator and Deploy Contracts
Max Daunarovich
Posted on May 26, 2023
Intro
Last Time we've covered how you can get started with testing in your project. That article covers use of init
and make
commands in JS Testing Framework and how to use them to scaffold your testing enviroment.
We will assume you've gone through that process and have ready setup.
This time we will cover how you can deploy your newly developed contract to emulator to be able to run scripts and transactions against it.
Prerequisites
You will need to install Flow CLI in order to be able to spin up Flow Emulator. Follow instructions for your OS on Developer Portal.
If you haven't prepared your environment - follow instructions from previous article.
Setup Cadence Folder
Most of the methods will allow you to use Cadence code straight from the .cdc
files. The only requirement is to use specific folder structure in designated folder.
Let's create cadence
folder in the root of your project. Then create 3 new folders inside of it:
-
contracts
- will hold all the contracts you would want to deploy -
transactions
- for any transactions code -
scripts
- same for scripts
Please note that folders should be called exactly in the same manner or they won't work. We have planned the task, which would allow you to use custom names, but it's not implemented yet
In order for framework to know what location you want to use you will need to call init
method:
import path from "path";
import { emulator, init } from "@onflow/flow-js-testing";
describe("interactions - sendTransaction", () => {
// Instantiate emulator and path to Cadence files
beforeEach(async () => {
const basePath = path.resolve(__dirname, "../cadence");
await init(basePath);
return emulator.start();
});
// Stop emulator, so it could be restarted
afterEach(async () => {
return emulator.stop();
});
});
Setup Emulator
In order to test any network interactions, we need instance of emulator running. Frameworks handle the process with emulator
instance, which exposes two methods - start
and stop
. Those will handle running flow emulator
process with necessary flags and extra steps after emulator will be stopped.
You can reference code above to see how we use it in
beforeEach
/afterEach
blocks.
How to Deploy a contract?
Framework have two handy functions, which will help you deploy contracts:
-
deployContract
- allows you to pass contract code as a string (handy, when you are just playing around) -
deployContractByName
- will allow you to deploy contract defined via file in yourcadence
folder (which you pass, when calling
They are quite similar in function, but deployContractByName
provides you an easy way of deploying contracts located in your cadence/contracts
folder. Let's create a file HelloWorld.cdc
inside ouf our contracts
folder and paste the following in:
pub contract HelloWorld{
pub let greetings: String
init(){
self.greetings = "Hello, from Cadence"
}
}
Our little contract only have single field, which we can read in the future. Let me show you how we can test it.
Create new test suit with make
command:
npx @onflow/flow-js-testing make first
This command will run make
tool provided by testing framework and create a first.js
file, which will have a basic setup for you. Since deploying contract on Flow is just sending specifically designed transaction, we will use shallPass
method to ensure everything works:
import path from "path";
import { init, emulator, shallPass, deployContractByName } from "@onflow/flow-js-testing";
// Increase timeout if your tests failing due to timeout
jest.setTimeout(10000);
describe("deploy", () => {
beforeEach(async () => {
const basePath = path.resolve(__dirname, "../cadence");
const logging = false;
await init(basePath);
return emulator.start({ logging });
});
// Stop emulator, so it could be restarted
afterEach(async () => {
return emulator.stop();
});
// We will put next block here
// ========>
});
Our first test will be super simple. We will wrap deployContractByName
with shallPass
and await the result of that computation:
test("deploy Hello", async () => {
await shallPass(deployContractByName({ name: "Hello" }));
});
Well done! 👍 But scenarios like that is never the case. Contracts are usually much more complicated and accept multiple arguments to init it's field. Let's create another contract Message.cdc
in our contracts
folder and populate it with following Cadence code:
pub contract Message{
pub let message: String
init(message: String){
self.message = message
}
}
Now we need to pass arguments, when we deploy contract. We can do this by specifying args
field on the object we pass into deployContractByName
. It should be an array of values in the same order we defined them on init
method of our contract.
test("deploy Message", async () => {
const message = "Cadence is great!";
await shallPass(
deployContractByName({
name: "Message",
args: [message],
})
);
});
Note that framework will handle all the type conversions for you! 😎
You can try to add another argument to ensure that test will fail, ensuring you are doing
everything correctly.
Another function we have at our disposal is deployContract
. It has pretty much the same API, except it expects code
value for your Cadence instead of a file name. Let's make a simple test to
illustrate how you can use it.
test("deploy Custom", async () => {
await shallPass(
deployContract({
code: `
pub contract Custom{
pub let message: String
init(){
self.message = "This is custom contract! Neat, ha? :)"
}
}
`,
})
);
});
You can pass arguments in the same fashion we did above for deployContractByName
by specifying args
field.
Originally the purpose of this function was to process and consume arguments from
deployContractByName
and wasn't exported. After some consideration we've exported it to enable
people who just learning Cadence to experiment quikly
You can find detailed explanation of both functions on [Flow Developer Portal - Contract Management (https://developers.flow.com/tooling/flow-js-testing/contracts)
Too slow!
Sometimes scenario that you are trying to test takes more time than default Jest timeout - 5 seconds. You can control this by increasing the timeout to next reasonable value:
// Place this after import lines
import path from "path";
import { deployContractByName, emulator, executeScript, init } from "@onflow/flow-js-testing";
jest.setTimeout(20000);
// the rest of the test setup below 👇
Some CI environments might be slower than you local machine, so if you see randomly failing tests - try to increase this value.
That's all, folks! Next time we will take a look how we can execute scripts, sign and send
transactions.
Posted on May 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.