Testing Cosmos SDK Applications
Testing is critical for blockchain applications. The Cosmos SDK provides several testing utilities and patterns to help you build robust applications.Testing Approaches
The SDK supports multiple testing levels:- Unit Tests - Test individual keepers and functions
- Integration Tests - Test module interactions
- Simulation Tests - Randomized testing for edge cases
- End-to-End Tests - Full chain testing
Unit Testing
Setting Up Test Applications
Create minimal test applications for unit testing fromsimapp/test_helpers.go:40-51:
func setup(withGenesis bool, invCheckPeriod uint) (*SimApp, GenesisState) {
db := dbm.NewMemDB()
appOptions := make(simtestutil.AppOptionsMap, 0)
appOptions[flags.FlagHome] = DefaultNodeHome
app := NewSimApp(log.NewNopLogger(), db, nil, true, appOptions)
if withGenesis {
return app, app.DefaultGenesis()
}
return app, GenesisState{}
}
Creating Test Instances
Build test apps with custom options fromsimapp/test_helpers.go:54-92:
func NewSimappWithCustomOptions(
t *testing.T,
isCheckTx bool,
options SetupOptions,
) *SimApp {
t.Helper()
privVal := mock.NewPV()
pubKey, err := privVal.GetPubKey()
require.NoError(t, err)
// Create validator set with single validator
validator := cmttypes.NewValidator(pubKey, 1)
valSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{validator})
// Generate genesis account
senderPrivKey := secp256k1.GenPrivKey()
acc := authtypes.NewBaseAccount(
senderPrivKey.PubKey().Address().Bytes(),
senderPrivKey.PubKey(),
0,
0,
)
balance := banktypes.Balance{
Address: acc.GetAddress().String(),
Coins: sdk.NewCoins(
sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000000000)),
),
}
app := NewSimApp(options.Logger, options.DB, nil, true, options.AppOpts)
genesisState := app.DefaultGenesis()
genesisState, err = simtestutil.GenesisStateWithValSet(
app.AppCodec(),
genesisState,
valSet,
[]authtypes.GenesisAccount{acc},
balance,
)
require.NoError(t, err)
if !isCheckTx {
stateBytes, err := cmtjson.MarshalIndent(genesisState, "", " ")
require.NoError(t, err)
// Initialize the chain
_, err = app.InitChain(&abci.RequestInitChain{
Validators: []abci.ValidatorUpdate{},
ConsensusParams: simtestutil.DefaultConsensusParams,
AppStateBytes: stateBytes,
})
require.NoError(t, err)
}
return app
}
Simple Test Helper
Create a basic test app fromsimapp/test_helpers.go:95-117:
func Setup(t *testing.T, isCheckTx bool) *SimApp {
t.Helper()
privVal := mock.NewPV()
pubKey, err := privVal.GetPubKey()
require.NoError(t, err)
validator := cmttypes.NewValidator(pubKey, 1)
valSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{validator})
senderPrivKey := secp256k1.GenPrivKey()
acc := authtypes.NewBaseAccount(
senderPrivKey.PubKey().Address().Bytes(),
senderPrivKey.PubKey(),
0,
0,
)
balance := banktypes.Balance{
Address: acc.GetAddress().String(),
Coins: sdk.NewCoins(
sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000000000)),
),
}
app := SetupWithGenesisValSet(t, valSet, []authtypes.GenesisAccount{acc}, balance)
return app
}
Integration Testing
Thetestutil/integration package provides a lightweight testing framework.
Integration App Structure
Fromtestutil/integration/router.go:32-40:
type App struct {
*baseapp.BaseApp
ctx sdk.Context
logger log.Logger
moduleManager module.Manager
queryHelper *baseapp.QueryServiceTestHelper
}
Creating Integration Apps
Fromtestutil/integration/router.go:44-120:
func NewIntegrationApp(
sdkCtx sdk.Context,
logger log.Logger,
keys map[string]*storetypes.KVStoreKey,
appCodec codec.Codec,
modules map[string]appmodule.AppModule,
baseAppOptions ...func(*baseapp.BaseApp),
) *App {
db := dbm.NewMemDB()
interfaceRegistry := codectypes.NewInterfaceRegistry()
moduleManager := module.NewManagerFromMap(modules)
basicModuleManager := module.NewBasicManagerFromManager(moduleManager, nil)
basicModuleManager.RegisterInterfaces(interfaceRegistry)
txConfig := authtx.NewTxConfig(
codec.NewProtoCodec(interfaceRegistry),
authtx.DefaultSignModes,
)
bApp := baseapp.NewBaseApp(
appName,
logger,
db,
txConfig.TxDecoder(),
baseAppOptions...,
)
bApp.MountKVStores(keys)
bApp.SetInitChainer(func(_ sdk.Context, _ *cmtabcitypes.RequestInitChain) (
*cmtabcitypes.ResponseInitChain, error,
) {
for _, mod := range modules {
if m, ok := mod.(module.HasGenesis); ok {
m.InitGenesis(sdkCtx, appCodec, m.DefaultGenesis(appCodec))
}
}
return &cmtabcitypes.ResponseInitChain{}, nil
})
router := baseapp.NewMsgServiceRouter()
router.SetInterfaceRegistry(interfaceRegistry)
bApp.SetMsgServiceRouter(router)
if err := bApp.LoadLatestVersion(); err != nil {
panic(err)
}
ctx := sdkCtx.WithBlockHeader(cmtproto.Header{ChainID: appName}).WithIsCheckTx(true)
return &App{
BaseApp: bApp,
logger: logger,
ctx: ctx,
moduleManager: *moduleManager,
queryHelper: baseapp.NewQueryServerTestHelper(ctx, interfaceRegistry),
}
}
Running Messages in Tests
Fromtestutil/integration/router.go:128-169:
func (app *App) RunMsg(msg sdk.Msg, option ...Option) (*codectypes.Any, error) {
// Set options
cfg := &Config{}
for _, opt := range option {
opt(cfg)
}
if cfg.AutomaticCommit {
defer app.Commit()
}
if cfg.AutomaticFinalizeBlock {
height := app.LastBlockHeight() + 1
if _, err := app.FinalizeBlock(
&cmtabcitypes.RequestFinalizeBlock{Height: height},
); err != nil {
return nil, fmt.Errorf("failed to run finalize block: %w", err)
}
}
app.logger.Info("Running msg", "msg", msg.String())
handler := app.MsgServiceRouter().Handler(msg)
if handler == nil {
return nil, fmt.Errorf(
"handler is nil, can't route message %s: %+v",
sdk.MsgTypeURL(msg),
msg,
)
}
msgResult, err := handler(app.ctx, msg)
if err != nil {
return nil, fmt.Errorf("failed to execute message %s: %w",
sdk.MsgTypeURL(msg), err)
}
var response *codectypes.Any
if len(msgResult.MsgResponses) > 0 {
response = msgResult.MsgResponses[0]
}
return response, nil
}
Integration Test Example
Fromtestutil/integration/example_test.go:30-116:
func Example() {
// Setup encoding config for testing modules
encodingCfg := moduletestutil.MakeTestEncodingConfig(
auth.AppModuleBasic{},
mint.AppModuleBasic{},
)
keys := storetypes.NewKVStoreKeys(authtypes.StoreKey, minttypes.StoreKey)
authority := authtypes.NewModuleAddress("gov").String()
logger := log.NewNopLogger()
cms := integration.CreateMultiStore(keys, logger)
newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger)
// Create account keeper
accountKeeper := authkeeper.NewAccountKeeper(
encodingCfg.Codec,
runtime.NewKVStoreService(keys[authtypes.StoreKey]),
authtypes.ProtoBaseAccount,
map[string][]string{minttypes.ModuleName: {authtypes.Minter}},
addresscodec.NewBech32Codec("cosmos"),
"cosmos",
authority,
)
authModule := auth.NewAppModule(
encodingCfg.Codec,
accountKeeper,
authsims.RandomGenesisAccounts,
nil,
)
// Create mint keeper
mintKeeper := mintkeeper.NewKeeper(
encodingCfg.Codec,
runtime.NewKVStoreService(keys[minttypes.StoreKey]),
nil,
accountKeeper,
nil,
authtypes.FeeCollectorName,
authority,
)
mintModule := mint.NewAppModule(
encodingCfg.Codec,
mintKeeper,
accountKeeper,
nil,
nil,
)
// Create integration app
integrationApp := integration.NewIntegrationApp(
newCtx,
logger,
keys,
encodingCfg.Codec,
map[string]appmodule.AppModule{
authtypes.ModuleName: authModule,
minttypes.ModuleName: mintModule,
},
)
// Register message servers
authtypes.RegisterMsgServer(
integrationApp.MsgServiceRouter(),
authkeeper.NewMsgServerImpl(accountKeeper),
)
minttypes.RegisterMsgServer(
integrationApp.MsgServiceRouter(),
mintkeeper.NewMsgServerImpl(mintKeeper),
)
minttypes.RegisterQueryServer(
integrationApp.QueryHelper(),
mintkeeper.NewQueryServerImpl(mintKeeper),
)
// Run test message
params := minttypes.DefaultParams()
params.BlocksPerYear = 10000
result, err := integrationApp.RunMsg(&minttypes.MsgUpdateParams{
Authority: authority,
Params: params,
})
if err != nil {
panic(err)
}
// Verify result
resp := minttypes.MsgUpdateParamsResponse{}
err = encodingCfg.Codec.Unmarshal(result.Value, &resp)
if err != nil {
panic(err)
}
// Check state
sdkCtx := sdk.UnwrapSDKContext(integrationApp.Context())
got, err := mintKeeper.Params.Get(sdkCtx)
if err != nil {
panic(err)
}
fmt.Println(got.BlocksPerYear)
// Output: 10000
}
Helper Functions for Testing
Adding Test Addresses
Fromsimapp/test_helpers.go:185-203:
func AddTestAddrsIncremental(
app *SimApp,
ctx sdk.Context,
accNum int,
accAmt sdkmath.Int,
) []sdk.AccAddress {
return addTestAddrs(app, ctx, accNum, accAmt, simtestutil.CreateIncrementalAccounts)
}
func addTestAddrs(
app *SimApp,
ctx sdk.Context,
accNum int,
accAmt sdkmath.Int,
strategy simtestutil.GenerateAccountStrategy,
) []sdk.AccAddress {
testAddrs := strategy(accNum)
bondDenom, err := app.StakingKeeper.BondDenom(ctx)
if err != nil {
panic(err)
}
initCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, accAmt))
for _, addr := range testAddrs {
initAccountWithCoins(app, ctx, addr, initCoins)
}
return testAddrs
}
Initializing Test Accounts
Fromsimapp/test_helpers.go:205-215:
func initAccountWithCoins(
app *SimApp,
ctx sdk.Context,
addr sdk.AccAddress,
coins sdk.Coins,
) {
err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, coins)
if err != nil {
panic(err)
}
err = app.BankKeeper.SendCoinsFromModuleToAccount(
ctx,
minttypes.ModuleName,
addr,
coins,
)
if err != nil {
panic(err)
}
}
Testing Genesis States
Fromsimapp/test_helpers.go:155-181:
func GenesisStateWithSingleValidator(
t *testing.T,
app *SimApp,
) GenesisState {
t.Helper()
privVal := mock.NewPV()
pubKey, err := privVal.GetPubKey()
require.NoError(t, err)
validator := cmttypes.NewValidator(pubKey, 1)
valSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{validator})
senderPrivKey := secp256k1.GenPrivKey()
acc := authtypes.NewBaseAccount(
senderPrivKey.PubKey().Address().Bytes(),
senderPrivKey.PubKey(),
0,
0,
)
balances := []banktypes.Balance{
{
Address: acc.GetAddress().String(),
Coins: sdk.NewCoins(
sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000000000)),
),
},
}
genesisState := app.DefaultGenesis()
genesisState, err = simtestutil.GenesisStateWithValSet(
app.AppCodec(),
genesisState,
valSet,
[]authtypes.GenesisAccount{acc},
balances...,
)
require.NoError(t, err)
return genesisState
}
Network Testing
Create test networks for end-to-end testing fromsimapp/test_helpers.go:218-247:
func NewTestNetworkFixture() network.TestFixture {
dir, err := os.MkdirTemp("", "simapp")
if err != nil {
panic(fmt.Sprintf("failed creating temporary directory: %v", err))
}
defer os.RemoveAll(dir)
app := NewSimApp(
log.NewNopLogger(),
dbm.NewMemDB(),
nil,
true,
simtestutil.NewAppOptionsWithFlagHome(dir),
)
appCtr := func(val network.ValidatorI) servertypes.Application {
return NewSimApp(
val.GetCtx().Logger,
dbm.NewMemDB(),
nil,
true,
simtestutil.NewAppOptionsWithFlagHome(
val.GetCtx().Config.RootDir,
),
bam.SetPruning(pruningtypes.NewPruningOptionsFromString(
val.GetAppConfig().Pruning,
)),
bam.SetMinGasPrices(val.GetAppConfig().MinGasPrices),
bam.SetChainID(val.GetCtx().Viper.GetString(flags.FlagChainID)),
)
}
return network.TestFixture{
AppConstructor: appCtr,
GenesisState: app.DefaultGenesis(),
EncodingConfig: testutil.TestEncodingConfig{
InterfaceRegistry: app.InterfaceRegistry(),
Codec: app.AppCodec(),
TxConfig: app.TxConfig(),
Amino: app.LegacyAmino(),
},
}
}
Best Practices
Use Table-Driven Tests
func TestKeeper(t *testing.T) {
tests := []struct {
name string
setup func(*SimApp, sdk.Context)
check func(*SimApp, sdk.Context)
wantErr bool
}{
{
name: "valid case",
setup: func(app *SimApp, ctx sdk.Context) {
// Setup test state
},
check: func(app *SimApp, ctx sdk.Context) {
// Verify state
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := Setup(t, false)
ctx := app.BaseApp.NewContext(false)
if tt.setup != nil {
tt.setup(app, ctx)
}
// Run test
if tt.check != nil {
tt.check(app, ctx)
}
})
}
}
Isolate Tests
- Create new app instances for each test
- Don’t share state between tests
- Use
t.Helper()in helper functions - Clean up resources with
defer
Test Edge Cases
- Test with zero values
- Test with maximum values
- Test invalid inputs
- Test error paths
Mock External Dependencies
- Use interfaces for keepers
- Create mock implementations for testing
- Use
gomockfor automatic mock generation
Next Steps
- Learn about Simulation Testing for advanced testing
- Review App Creation patterns
- Explore Genesis Configuration
- Study Module Development testing