Testing
Smart Contract Test Structure
Let's break down the test we wrote to verify the MYTEST
contract:
#define NO_UEFI
#include "contract_testing.h"
class ContractTestingMyTest : protected ContractTesting
{
public:
ContractTestingMyTest()
{
initEmptySpectrum();
initEmptyUniverse();
INIT_CONTRACT(MYTEST);
}
MYTEST::add_output add(sint64 a, sint64 b)
{
MYTEST::add_input input;
MYTEST::add_output output;
input.a = a;
input.b = b;
callFunction(MYTEST_CONTRACT_INDEX, 1, input, output);
return output;
}
};
TEST(MyTest, TestAdd)
{
ContractTestingMyTest test;
MYTEST::add_output output = test.add(1, 2);
EXPECT_EQ(output.c, 3);
}
We define #define NO_UEFI
because the test is executed in a standard OS environment, not within UEFI:
#define NO_UEFI
Include the testing framework:
#include "contract_testing.h"
The ContractTesting
base class provides the interface and logic needed to test Qubic contracts. Contract interactions such as function calls or procedure invocations must happen within a subclass like ContractTestingMyTest
:
class ContractTestingMyTest : protected ContractTesting {}
The constructor initializes the test environment:
initEmptySpectrum()
initializes the storage for user QUs.initEmptyUniverse()
sets up the asset universe.INIT_CONTRACT(MYTEST);
loads our contract into the testing runtime.
ContractTestingMyTest()
{
initEmptySpectrum();
initEmptyUniverse();
INIT_CONTRACT(MYTEST);
}
You can skip initEmptySpectrum()
or initEmptyUniverse()
if your test doesn't involve QUs or assets.
If MyTest
contract calls functions or procedures from XXX
contract, you must also need INIT_CONTRACT(XXX);
To test the add
function, we call callFunction
with:
- The contract index (
MYTEST_CONTRACT_INDEX
) - The registered function ID (
1
) - The
input
andoutput
structs
After the call, output
contains the result returned by the contract:
MYTEST::add_output add(sint64 a, sint64 b)
{
MYTEST::add_input input;
MYTEST::add_output output;
input.a = a;
input.b = b;
callFunction(MYTEST_CONTRACT_INDEX, 1, input, output);
return output;
}
Finally, we define the test case using the TEST
macro. We create a ContractTestingMyTest
instance, call the add
function with values 1
and 2
, and assert the result is 3
:
TEST(MyTest, TestAdd)
{
ContractTestingMyTest test;
MYTEST::add_output output = test.add(1, 2);
EXPECT_EQ(output.c, 3);
}
Calling INIT_CONTRACT()
will reset the contract state. So each time you create an instance of ContractTestingMyTest
will reset the contract state, spectrum and the universe.
Invoke User Procedure
We have learned how to call contract function above, pretty simple right? Now let's learn how we can call our procedure from our test
Assumming we have the below procedure in our contract :
sint64 myNumber;
struct setMyNumber_input
{
sint64 myNumber;
};
struct setMyNumber_output
{
};
PUBLIC_PROCEDURE(setMyNumber)
{
state.myNumber = input.myNumber;
}
REGISTER_USER_FUNCTIONS_AND_PROCEDURES()
{
REGISTER_USER_PROCEDURE(setMyNumber, 1);
}
So the test code will looke like:
class ContractTestingMyTest : protected ContractTesting
{
public:
...
MYTEST::setMyNumber_output setMyNumber(id user, sint64 number)
{
MYTEST::setMyNumber_input input;
MYTEST::setMyNumber_output output;
input.myNumber = number;
EXPECT_TRUE(invokeUserProcedure(MYTEST_CONTRACT_INDEX, 1, input, output, user, 0));
return output;
}
...
}
TEST(MyTest, SetMyNumber)
{
ContractTestingMyTest test;
// Although the contract doesn't require QUs for execution,
// the caller must exist in the spectrum (i.e., have at least 1 QU)
// in order to successfully invoke a procedure.
increaseEnergy(user, 1'000'000); // Give user 1M QUs
MYTEST::setMyNumber_output output = test.setMyNumber(user, 42);
}
It’s almost like calling a function, right? But there are some differences.
To invoke a procedure, we use the invokeUserProcedure
function. The first argument is the contract index, the second is the procedure ID, followed by the input and output structs.
Since invoking a procedure in Qubic is actually make a transaction, we must also provide the sender (the user who creates the transaction) and the amount of QUs being transferred — which is 0 in our case.
Finally, we check the return value of invokeUserProcedure
to ensure the procedure call was successful by asserting it returns true
.
Calling increaseEnergy()
without initEmptySpectrum()
without throw error at runtime.
Query Contract State
We have successfully invoked our procedure to set the myNumber
state. But how can we verify that it was actually updated?
There are two ways to query the contract state in our test. Let's explore them now.
1. Use Function To Query State
This is the traditional way to query state in both our tests and the real production environment. We simply create a function in our contract that returns the myNumber
state, then call this function from our test.
Let's see how to write this:
struct getMyNumber_input
{
};
struct getMyNumber_output
{
sint64 myNumber;
};
PUBLIC_FUNCTION(getMyNumber)
{
output.myNumber = state.myNumber;
}
REGISTER_USER_FUNCTIONS_AND_PROCEDURES()
{
REGISTER_USER_FUNCTION(getMyNumber, 1);
}
And then call the getMyNumber
in our test:
class ContractTestingMyTest : protected ContractTesting
{
public:
// ...
MYTEST::setMyNumber_output setMyNumber(id user, sint64 number)
{
// ...
}
MYTEST::getMyNumber_output getMyNumber()
{
MYTEST::getMyNumber_input input;
MYTEST::getMyNumber_output output;
callFunction(MYTEST_CONTRACT_INDEX, 1, input, output);
return output;
}
// ...
}
TEST(MyTest, SetAndGetMyNumber)
{
ContractTestingMyTest test;
// Although the contract doesn't require QUs for execution,
// the caller must exist in the spectrum (i.e., have at least 1 QU)
// in order to successfully invoke a procedure.
increaseEnergy(user, 1'000'000); // Give user 1M QUs
MYTEST::setMyNumber_output output = test.setMyNumber(user, 42);
MYTEST::getMyNumber_output getOutput = test.getMyNumber();
// The myNumber state should be 42
EXPECT_EQ(getOutput.myNumber, 42);
}
2. Use Contract Instance To Query State
As shown, the state is members of the contract struct. This means that once we have an instance of the contract, we can directly access its state.
A pointer to the contract instance is stored in the contractStates
array. To retrieve our contract's pointer, we simply access it using the contract index contractStates[NAME_CONTRACT_INDEX]
:
TEST(MyTest, SetMyNumber)
{
ContractTestingMyTest test;
increaseEnergy(user, 1'000'000);
MYTEST::setMyNumber_output output = test.setMyNumber(user, 42);
char* state = (char*)contractStates[MYTEST_CONTRACT_INDEX];
// Read the first 64 bits from state pointer and cast it to sint64 type
EXPECT_EQ(*((sint64*)state), 42);
}
Now you can query the state without creating a contract function. However, this approach still feels a bit messy since it involves handling pointers.
A cleaner solution is to create a struct that inherits from the state struct and implement a function to retrieve myNumber
:
struct MYTESTGetter : public MYTEST
{
sint64 getMyNumber()
{
return this->myNumber;
}
};
TEST(MyTest, SetMyNumber)
{
ContractTestingMyTest test;
increaseEnergy(user, 1'000'000);
MYTEST::setMyNumber_output output = test.setMyNumber(user, 42);
// Cast the pointer to MYTESTGetter pointer so we can call our getMyNumber()
MYTESTGetter* state = (MYTESTGetter*)contractStates[MYTEST_CONTRACT_INDEX];
EXPECT_EQ(state->getMyNumber(), 42);
}
Invoke System Procedure
In the test environment, we can directly invoke system procedures to verify whether our logic works correctly.
But keep in mind: when we deploy our contract, system procedures are automatically triggered by the Qubic network and cannot be invoked manually.
class ContractTestingMyTest : protected ContractTesting
{
public:
// ...
void beginEpoch()
{
callSystemProcedure(MYTEST_CONTRACT_INDEX, BEGIN_EPOCH);
}
void endEpoch()
{
callSystemProcedure(MYTEST_CONTRACT_INDEX, END_EPOCH);
}
// ...
}
TEST(MyTest, TestBeginEpoch)
{
ContractTestingMyTest test;
test.beginEpoch();
test.endEpoch();
}
Mock Data
As mentioned in QPI date and time functions, they will not work correctly in the test environment without mocking the data (for example, qpi.epoch()
will always return 0
).
So, how can we mock this data?
qpi.epoch()
TEST(MyTest, TestEpoch)
{
system.epoch = 199;
// In contract call qpi.epoch() will return 199
// output.epoch = qpi.epoch()
}
qpi.tick()
TEST(MyTest, TestEpoch)
{
system.tick = 2000;
// In contract call qpi.tick() will return 2000
// output.tick = qpi.tick()
}
qpi.year|month|day|hour|minute|second|millisecond()
TEST(MyTest, TestEpoch)
{
// To set current date for the core environment
updateTime();
// Then reflect it to qpi
updateQpiTime();
// Now qpi.year() or qpi.month(), ... will return correct value
// We can also set time to any number
utcTime.Year = 2025;
utcTime.Month = 10;
utcTime.Day = 15;
// ...
// Then reflect it to qpi
updateQpiTime();
}