Skip to content

Commit 6b20ef7

Browse files
alpejulienrbrt
andauthored
docs: system test tutorial (#20812)
Co-authored-by: Julien Robert <julien@rbrt.fr>
1 parent 7f1eeb1 commit 6b20ef7

File tree

2 files changed

+221
-1
lines changed

2 files changed

+221
-1
lines changed

tests/systemtests/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ Uses:
1414
* testify
1515
* gjson
1616
* sjson
17-
Server and client side are executed on the host machine
17+
18+
Server and client side are executed on the host machine.
1819

1920
## Developer
2021

@@ -24,6 +25,10 @@ System tests cover the full stack via cli and a running (multi node) network. Th
2425
to run compared to unit or integration tests.
2526
Therefore, we focus on the **critical path** and do not cover every condition.
2627

28+
## How to use
29+
30+
Read the [getting_started.md](getting_started.md) guide to get started.
31+
2732
### Execute a single test
2833

2934
```sh
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# Getting started with a new system test
2+
3+
## Preparation
4+
5+
Build a new binary from current branch and copy it to the `tests/systemtests/binaries` folder by running system tests.
6+
In project root:
7+
8+
```shell
9+
make test-system
10+
```
11+
12+
Or via manual steps
13+
14+
```shell
15+
make build
16+
mkdir -p ./tests/systemtests/binaries
17+
cp ./build/simd ./tests/systemtests/binaries/
18+
```
19+
20+
## Part 1: Writing the first system test
21+
22+
Switch to the `tests/systemtests` folder to work from here.
23+
24+
If there is no test file matching your use case, start a new test file here.
25+
for example `bank_test.go` to begin with:
26+
27+
```go
28+
//go:build system_test
29+
30+
package systemtests
31+
32+
import (
33+
"testing"
34+
)
35+
36+
func TestQueryTotalSupply(t *testing.T) {
37+
sut.ResetChain(t)
38+
sut.StartChain(t)
39+
40+
cli := NewCLIWrapper(t, sut, verbose)
41+
raw := cli.CustomQuery("q", "bank", "total-supply")
42+
t.Log("### got: " + raw)
43+
}
44+
```
45+
46+
The file begins with a Go build tag to exclude it from regular go test runs.
47+
All tests in the `systemtests` folder build upon the *test runner* initialized in `main_test.go`.
48+
This gives you a multi node chain started on your box.
49+
It is a good practice to reset state in the beginning so that you have a stable base.
50+
51+
The system tests framework comes with a CLI wrapper that makes it easier to interact or parse results.
52+
In this example we want to execute `simd q bank total-supply --output json --node tcp://localhost:26657` which queries
53+
the bank module.
54+
Then print the result to for the next steps
55+
56+
### Run the test
57+
58+
```shell
59+
go test -mod=readonly -tags='system_test' -v ./... --run TestQueryTotalSupply --verbose
60+
```
61+
62+
This give very verbose output. You would see all simd CLI commands used for starting the server or by the client to interact.
63+
In the example code, we just log the output. Watch out for
64+
65+
```shell
66+
bank_test.go:15: ### got: {
67+
"supply": [
68+
{
69+
"denom": "stake",
70+
"amount": "2000000190"
71+
},
72+
{
73+
"denom": "testtoken",
74+
"amount": "4000000000"
75+
}
76+
],
77+
"pagination": {
78+
"total": "2"
79+
}
80+
}
81+
```
82+
83+
At the end is a tail from the server log printed. This can sometimes be handy when debugging issues.
84+
85+
86+
### Tips
87+
88+
* Passing `--nodes-count=1` overwrites the default node count and can speed up your test for local runs
89+
90+
## Part 2: Working with json
91+
92+
When we have a json response, the [gjson](https://github.com/tidwall/gjson) lib can shine. It comes with jquery like
93+
syntax that makes it easy to navigation within the document.
94+
95+
For example `gjson.Get(raw, "supply").Array()` gives us all the childs to `supply` as an array.
96+
Or `gjson.Get("supply.#(denom==stake).amount").Int()` for the amount of the stake token as int64 type.
97+
98+
In order to test our assumptions in the system test, we modify the code to use `gjson` to fetch the data:
99+
100+
```go
101+
raw := cli.CustomQuery("q", "bank", "total-supply")
102+
103+
exp := map[string]int64{
104+
"stake": int64(500000000 * sut.nodesCount),
105+
"testtoken": int64(1000000000 * sut.nodesCount),
106+
}
107+
require.Len(t, gjson.Get(raw, "supply").Array(), len(exp), raw)
108+
109+
for k, v := range exp {
110+
got := gjson.Get(raw, fmt.Sprintf("supply.#(denom==%q).amount", k)).Int()
111+
assert.Equal(t, v, got, raw)
112+
}
113+
```
114+
115+
The assumption on the staking token usually fails due to inflation minted on the staking token. Let's fix this in the next step
116+
117+
### Run the test
118+
119+
```shell
120+
go test -mod=readonly -tags='system_test' -v ./... --run TestQueryTotalSupply --verbose
121+
```
122+
123+
### Tips
124+
125+
* Putting the `raw` json response to the assert/require statements helps with debugging on failures. You are usually lacking
126+
context when you look at the values only.
127+
128+
129+
## Part 3: Setting state via genesis
130+
131+
First step is to disable inflation. This can be done via the `ModifyGenesisJSON` helper. But to add some complexity,
132+
we also introduce a new token and update the balance of the account for key `node0`.
133+
The setup code looks quite big and unreadable now. Usually a good time to think about extracting helper functions for
134+
common operations. The `genesis_io.go` file contains some examples already. I would skip this and take this to showcase the mix
135+
of `gjson`, `sjson` and stdlib json operations.
136+
137+
```go
138+
sut.ResetChain(t)
139+
cli := NewCLIWrapper(t, sut, verbose)
140+
141+
sut.ModifyGenesisJSON(t, func(genesis []byte) []byte {
142+
// disable inflation
143+
genesis, err := sjson.SetRawBytes(genesis, "app_state.mint.minter.inflation", []byte(`"0.000000000000000000"`))
144+
require.NoError(t, err)
145+
146+
// add new token to supply
147+
var supply []json.RawMessage
148+
rawSupply := gjson.Get(string(genesis), "app_state.bank.supply").String()
149+
require.NoError(t, json.Unmarshal([]byte(rawSupply), &supply))
150+
supply = append(supply, json.RawMessage(`{"denom": "mytoken","amount": "1000000"}`))
151+
newSupply, err := json.Marshal(supply)
152+
require.NoError(t, err)
153+
genesis, err = sjson.SetRawBytes(genesis, "app_state.bank.supply", newSupply)
154+
require.NoError(t, err)
155+
156+
// add amount to any balance
157+
anyAddr := cli.GetKeyAddr("node0")
158+
newBalances := GetGenesisBalance(genesis, anyAddr).Add(sdk.NewInt64Coin("mytoken", 1000000))
159+
newBalancesBz, err := newBalances.MarshalJSON()
160+
require.NoError(t, err)
161+
newState, err := sjson.SetRawBytes(genesis, fmt.Sprintf("app_state.bank.balances.#[address==%q]#.coins", anyAddr), newBalancesBz)
162+
require.NoError(t, err)
163+
return newState
164+
})
165+
sut.StartChain(t)
166+
```
167+
168+
Next step is to add the new token to the assert map. But we can also make it more resilient to different node counts.
169+
170+
```go
171+
exp := map[string]int64{
172+
"stake": int64(500000000 * sut.nodesCount),
173+
"testtoken": int64(1000000000 * sut.nodesCount),
174+
"mytoken": 1000000,
175+
}
176+
```
177+
178+
```shell
179+
go test -mod=readonly -tags='system_test' -v ./... --run TestQueryTotalSupply --verbose --nodes-count=1
180+
```
181+
182+
## Part 4: Set state via TX
183+
184+
Complexer workflows and tests require modifying state on a running chain. This works only with builtin logic and operations.
185+
If we want to burn some our new tokens, we need to submit a bank burn message to do this.
186+
The CLI wrapper works similar to the query. Just pass the parameters. It uses the `node0` key as *default*:
187+
188+
```go
189+
// and when
190+
txHash := cli.Run("tx", "bank", "burn", "node0", "400000mytoken")
191+
RequireTxSuccess(t, txHash)
192+
```
193+
194+
`RequireTxSuccess` or `RequireTxFailure` can be used to ensure the expected result of the operation.
195+
Next, check that the changes are applied.
196+
197+
```go
198+
exp["mytoken"] = 600_000 // update expected state
199+
raw = cli.CustomQuery("q", "bank", "total-supply")
200+
for k, v := range exp {
201+
got := gjson.Get(raw, fmt.Sprintf("supply.#(denom==%q).amount", k)).Int()
202+
assert.Equal(t, v, got, raw)
203+
}
204+
assert.Equal(t, int64(600_000), cli.QueryBalance(cli.GetKeyAddr("node0"), "mytoken"))
205+
```
206+
207+
While tests are still more or less readable, it can gets harder the longer they are. I found it helpful to add
208+
some comments at the beginning to describe what the intention is. For example:
209+
210+
```go
211+
// scenario:
212+
// given a chain with a custom token on genesis
213+
// when an amount is burned
214+
// then this is reflected in the total supply
215+
```

0 commit comments

Comments
 (0)