Skip to main content
Version: v1.3

Example Tests

In the previous sections we showed how you can call() or post() smart contract function requests. We also created a few wrapper functions to simplify calling these functions even further. Now we will look at how to use the SoloContext to create full-blown tests for the dividend example smart contract.

Let's start with a simple test. We are going to use the member function to add a valid new member/factor combination to the member group.

func TestAddMemberOk(t *testing.T) {
ctx := wasmsolo.NewSoloContext(t, dividend.ScName, dividend.OnDispatch)

member1 := ctx.NewSoloAgent("member1")
dividendMember(ctx, member1, 100)
require.NoError(t, ctx.Err)
}

The above test first deploys the dividend smart contract to a new chain, and returns a SoloContext ctx. Then it uses ctx to create a new SoloAgent member1. When creating a SoloAgent a new TangleA data structure based on a DAG used to store blocks and their relations. The Tangle is the core underlying data structure of IOTA. address is created for that agent and its on-chain account is pre-seeded with 10M base tokens so that it is ready to be used in tests. The SoloAgent can be used whenever an address or agent ID needs to be provided, it can be used to sign a request to identify it as the sender, and it can be used to inspect the asset balances on its Tangle address and its on-chain account.

In this case, we simply create member1, and pass it to the dividend contract's member function, which will receive the address of member1 and a dispersal factor of 100. Finally, we check if ctx has received an error during the execution of the call. Remember that the only way to pass an error from a WasmLib function call is through a panic() call. The code that handles the actual call will intercept any panic() that was raised, and return an error. The SoloContext saves this error for later inspection, because the function descriptor used in the call itself has no way of passing back this error.

The next two example tests each call the contract's member function in the exact same way, but in both cases one required parameter is omitted. The idea is to test that the function properly panics by checking the ctx.Err value is not nil after the call. Finally, the error message text is checked to see if it contains the expected error message.

func TestAddMemberFailMissingAddress(t *testing.T) {
ctx := wasmsolo.NewSoloContext(t, dividend.ScName, dividend.OnDispatch)

member := dividend.ScFuncs.Member(ctx)
member.Params.Factor().SetValue(100)
member.Func.Post()
require.Error(t, ctx.Err)
require.Contains(t, ctx.Err.Error(), "missing mandatory address")
}

func TestAddMemberFailMissingFactor(t *testing.T) {
ctx := wasmsolo.NewSoloContext(t, dividend.ScName, dividend.OnDispatch)

member1 := ctx.NewSoloAgent("member1")
member := dividend.ScFuncs.Member(ctx)
member.Params.Address().SetValue(member1.ScAgentID().Address())
member.Func.Post()
require.Error(t, ctx.Err)
require.Contains(t, ctx.Err.Error(), "missing mandatory factor")
}

Each test has to set up the chain/contract/context from scratch. We will often use a specific setupTest() function to do all setup work that is shared by many tests.

We cannot use the dividendMember wrapper function in these two tests because of the missing required function parameters. So we have simply copy/pasted the code, and removed the Params proxy initialization lines that we wanted to be detected as missing.

Now let's see a more complex example:

func TestDivide1Member(t *testing.T) {
ctx := wasmsolo.NewSoloContext(t, dividend.ScName, dividend.OnDispatch)

member1 := ctx.NewSoloAgent("member1")
bal := ctx.Balances(member1)

dividendMember(ctx, member1, 100)
require.NoError(t, ctx.Err)

bal.Chain += ctx.GasFee
bal.Originator += ctx.StorageDeposit - ctx.GasFee
bal.VerifyBalances(t)

const dividendToDivide = 1*isc.Million + 1
dividendDivide(ctx, dividendToDivide)
require.NoError(t, ctx.Err)

bal.Chain += ctx.GasFee
bal.Originator -= ctx.GasFee
bal.Add(member1, dividendToDivide)
bal.VerifyBalances(t)
}

The first half of the code is almost identical to our first test above. We set up the test, create an agent, set the factor for that agent to 100, and verify that no error occurred. Notice how we additionally call ctx.Balances() to make a snapshot of all the original asset balances including those of the agent.

Then in the next lines we update the asset balances with the changes we expect to happen because of the call to the member function. And then we verify these changes against the actual asset balances by calling bal.VerifyBalances(t).

Next we transfer 1M + 1 tokens as part of the post request to the divide function. We subsequently check that no error has occurred. Finally, we again modify the asset balances to reflect the expected changes and verify these changes against the actual asset balances.

Now let's skip to the most complex test of all:

    func TestDivide3Members(t *testing.T) {
ctx := wasmsolo.NewSoloContext(t, dividend.ScName, dividend.OnDispatch)

member1 := ctx.NewSoloAgent("member1")
bal := ctx.Balances(member1)

dividendMember(ctx, member1, 25)
require.NoError(t, ctx.Err)

bal.Chain += ctx.GasFee
bal.Originator += ctx.StorageDeposit - ctx.GasFee
bal.VerifyBalances(t)

member2 := ctx.NewSoloAgent("member2")
bal = ctx.Balances(member1, member2)

dividendMember(ctx, member2, 50)
require.NoError(t, ctx.Err)

bal.Chain += ctx.GasFee
bal.Originator += ctx.StorageDeposit - ctx.GasFee
bal.VerifyBalances(t)

member3 := ctx.NewSoloAgent()
bal = ctx.Balances(member1, member2, member3)

dividendMember(ctx, member3, 75)
require.NoError(t, ctx.Err)

bal.Chain += ctx.GasFee
bal.Originator += ctx.StorageDeposit - ctx.GasFee
bal.VerifyBalances(t)

const dividendToDivide = 2*isc.Million - 1
dividendDivide(ctx, dividendToDivide)
require.NoError(t, ctx.Err)

remain := dividendToDivide - dividendToDivide*25/150 - dividendToDivide*50/150 - dividendToDivide*75/150
bal.Chain += ctx.GasFee
bal.Originator += remain - ctx.GasFee
bal.Add(member1, dividendToDivide*25/150)
bal.Add(member2, dividendToDivide*50/150)
bal.Add(member3, dividendToDivide*75/150)
bal.VerifyBalances(t)

const dividendToDivide2 = 2*isc.Million + 234
dividendDivide(ctx, dividendToDivide2)
require.NoError(t, ctx.Err)

remain = dividendToDivide2 - dividendToDivide2*25/150 - dividendToDivide2*50/150 - dividendToDivide2*75/150
bal.Chain += ctx.GasFee
bal.Originator += remain - ctx.GasFee
bal.Add(member1, dividendToDivide2*25/150)
bal.Add(member2, dividendToDivide2*50/150)
bal.Add(member3, dividendToDivide2*75/150)
bal.VerifyBalances(t)
}

This function creates 3 agents, and associates factors of 25, 50, and 75 respectively to them. That required 3 member requests, and we verify the expected balance changes after each request. Next the divide function is called, with 2M-1 tokens passed to it.

After this we verify that each agent has been distributed tokens according to its relative factor. Those factors are 25/150th, 50/150th, and 75/150th, respectively. Note that we cannot transfer fractional tokens, so we truncate to the nearest integer and ultimately any remaining tokens are not transferred, but remain in the sender's account.

Finally, we call divide again with 2M+234 tokens and again verify the asset balances afterwards.

Next we will show how to test Views and/or Funcs that return a result. Note that even though Funcs are usually invoked through a post() request, in which case any return values are inaccessible, they still can be invoked through a call() from within another Func, in which these return values can be used normally. Since solo executes post() requests synchronously, it is possible to have a Func return a result and test for certain result values.

func TestGetFactor(t *testing.T) {
ctx := wasmsolo.NewSoloContext(t, dividend.ScName, dividend.OnDispatch)

member1 := ctx.NewSoloAgent("member1")
dividendMember(ctx, member1, 25)
require.NoError(t, ctx.Err)

member2 := ctx.NewSoloAgent("member2")
dividendMember(ctx, member2, 50)
require.NoError(t, ctx.Err)

member3 := ctx.NewSoloAgent()
dividendMember(ctx, member3, 75)
require.NoError(t, ctx.Err)

value := dividendGetFactor(ctx, member3)
require.NoError(t, ctx.Err)
require.EqualValues(t, 75, value)

value = dividendGetFactor(ctx, member2)
require.NoError(t, ctx.Err)
require.EqualValues(t, 50, value)

value = dividendGetFactor(ctx, member1)
require.NoError(t, ctx.Err)
require.EqualValues(t, 25, value)
}

Here we first set up the same 3 dispersion factors, and then we retrieve the dispersion factors for each member in reverse order and verify their values.

In the next section we will describe a few more helper member functions of the SoloContext.