diff --git a/CHANGELOG.md b/CHANGELOG.md index feeec7dd60..fc3843c0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (testutil/integration) [#15556](https://github.com/cosmos/cosmos-sdk/pull/15556) Introduce `testutil/integration` package for module integration testing. * (types) [#15735](https://github.com/cosmos/cosmos-sdk/pull/15735) Make `ValidateBasic() error` method of `Msg` interface optional. Modules should validate messages directly in their message handlers ([RFC 001](https://docs.cosmos.network/main/rfc/rfc-001-tx-validation)). * (x/genutil) [#15679](https://github.com/cosmos/cosmos-sdk/pull/15679) Allow applications to specify a custom genesis migration function for the `genesis migrate` command. * (client) [#15458](https://github.com/cosmos/cosmos-sdk/pull/15458) Add a `CmdContext` field to client.Context initialized to cobra command's context. diff --git a/testutil/integration/example_test.go b/testutil/integration/example_test.go index e15c3801e7..4811109483 100644 --- a/testutil/integration/example_test.go +++ b/testutil/integration/example_test.go @@ -8,6 +8,7 @@ import ( storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/runtime" "github.com/cosmos/cosmos-sdk/testutil/integration" + sdk "github.com/cosmos/cosmos-sdk/types" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" "github.com/cosmos/cosmos-sdk/x/auth" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" @@ -46,8 +47,13 @@ func Example() { mintModule := mint.NewAppModule(encodingCfg.Codec, mintKeeper, accountKeeper, nil, nil) // create the application and register all the modules from the previous step - // replace the name and the logger by testing values in a real test case (e.g. t.Name() and log.NewTestLogger(t)) - integrationApp := integration.NewIntegrationApp("example", log.NewLogger(io.Discard), keys, authModule, mintModule) + // replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t)) + integrationApp := integration.NewIntegrationApp( + log.NewLogger(io.Discard, log.OutputJSONOption()), + keys, + encodingCfg.Codec, + authModule, mintModule, + ) // register the message and query servers authtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), authkeeper.NewMsgServerImpl(accountKeeper)) @@ -79,8 +85,10 @@ func Example() { panic(err) } + sdkCtx := sdk.UnwrapSDKContext(integrationApp.Context()) + // we should also check the state of the application - got := mintKeeper.GetParams(integrationApp.SDKContext()) + got := mintKeeper.GetParams(sdkCtx) if diff := cmp.Diff(got, params); diff != "" { panic(diff) } @@ -109,8 +117,13 @@ func Example_oneModule() { authModule := auth.NewAppModule(encodingCfg.Codec, accountKeeper, authsims.RandomGenesisAccounts, nil) // create the application and register all the modules from the previous step - // replace the name and the logger by testing values in a real test case (e.g. t.Name() and log.NewTestLogger(t)) - integrationApp := integration.NewIntegrationApp("example-one-module", log.NewLogger(io.Discard), keys, authModule) + // replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t)) + integrationApp := integration.NewIntegrationApp( + log.NewLogger(io.Discard), + keys, + encodingCfg.Codec, + authModule, + ) // register the message and query servers authtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), authkeeper.NewMsgServerImpl(accountKeeper)) @@ -122,11 +135,23 @@ func Example_oneModule() { result, err := integrationApp.RunMsg(&authtypes.MsgUpdateParams{ Authority: authority, Params: params, - }) + }, + // this allows to the begin and end blocker of the module before and after the message + integration.WithAutomaticBeginEndBlock(), + // this allows to commit the state after the message + integration.WithAutomaticCommit(), + ) if err != nil { panic(err) } + // verify that the begin and end blocker were called + // NOTE: in this example, we are testing auth, which doesn't have any begin or end blocker + // so verifying the block height is enough + if integrationApp.LastBlockHeight() != 2 { + panic(fmt.Errorf("expected block height to be 2, got %d", integrationApp.LastBlockHeight())) + } + // in this example the result is an empty response, a nil check is enough // in other cases, it is recommended to check the result value. if result == nil { @@ -140,8 +165,10 @@ func Example_oneModule() { panic(err) } + sdkCtx := sdk.UnwrapSDKContext(integrationApp.Context()) + // we should also check the state of the application - got := accountKeeper.GetParams(integrationApp.SDKContext()) + got := accountKeeper.GetParams(sdkCtx) if diff := cmp.Diff(got, params); diff != "" { panic(diff) } diff --git a/testutil/integration/options.go b/testutil/integration/options.go new file mode 100644 index 0000000000..a67327c01b --- /dev/null +++ b/testutil/integration/options.go @@ -0,0 +1,25 @@ +package integration + +// Config is the configuration for the integration app. +type Config struct { + AutomaticBeginEndBlock bool + AutomaticCommit bool +} + +// Option is a function that can be used to configure the integration app. +type Option func(*Config) + +// WithAutomaticBlockCreation enables begin/end block calls. +func WithAutomaticBeginEndBlock() Option { + return func(cfg *Config) { + cfg.AutomaticBeginEndBlock = true + } +} + +// WithAutomaticCommit enables automatic commit. +// This means that the integration app will automatically commit the state after each msgs. +func WithAutomaticCommit() Option { + return func(cfg *Config) { + cfg.AutomaticCommit = true + } +} diff --git a/testutil/integration/router.go b/testutil/integration/router.go index 096614759a..3cbf34172b 100644 --- a/testutil/integration/router.go +++ b/testutil/integration/router.go @@ -1,9 +1,10 @@ package integration import ( + "context" "fmt" - "github.com/cometbft/cometbft/abci/types" + cmtabcitypes "github.com/cometbft/cometbft/abci/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" "cosmossdk.io/log" @@ -18,18 +19,19 @@ import ( authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" ) +const appName = "integration-app" + // App is a test application that can be used to test the integration of modules. type App struct { *baseapp.BaseApp - ctx sdk.Context - logger log.Logger - + ctx sdk.Context + logger log.Logger queryHelper *baseapp.QueryServiceTestHelper } // NewIntegrationApp creates an application for testing purposes. This application is able to route messages to their respective handlers. -func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*storetypes.KVStoreKey, modules ...module.AppModuleBasic) *App { +func NewIntegrationApp(logger log.Logger, keys map[string]*storetypes.KVStoreKey, appCodec codec.Codec, modules ...module.AppModule) *App { db := dbm.NewMemDB() interfaceRegistry := codectypes.NewInterfaceRegistry() @@ -38,11 +40,25 @@ func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*st } txConfig := authtx.NewTxConfig(codec.NewProtoCodec(interfaceRegistry), authtx.DefaultSignModes) - - bApp := baseapp.NewBaseApp(fmt.Sprintf("integration-app-%s", nameSuffix), logger, db, txConfig.TxDecoder()) + bApp := baseapp.NewBaseApp(appName, logger, db, txConfig.TxDecoder(), baseapp.SetChainID(appName)) bApp.MountKVStores(keys) - bApp.SetInitChainer(func(ctx sdk.Context, req types.RequestInitChain) (types.ResponseInitChain, error) { - return types.ResponseInitChain{}, nil + + bApp.SetInitChainer(func(ctx sdk.Context, req cmtabcitypes.RequestInitChain) (cmtabcitypes.ResponseInitChain, error) { + for _, mod := range modules { + if m, ok := mod.(module.HasGenesis); ok { + m.InitGenesis(ctx, appCodec, m.DefaultGenesis(appCodec)) + } + } + + return cmtabcitypes.ResponseInitChain{}, nil + }) + + moduleManager := module.NewManager(modules...) + bApp.SetBeginBlocker(func(ctx sdk.Context, req cmtabcitypes.RequestBeginBlock) (cmtabcitypes.ResponseBeginBlock, error) { + return moduleManager.BeginBlock(ctx, req) + }) + bApp.SetEndBlocker(func(ctx sdk.Context, req cmtabcitypes.RequestEndBlock) (cmtabcitypes.ResponseEndBlock, error) { + return moduleManager.EndBlock(ctx, req) }) router := baseapp.NewMsgServiceRouter() @@ -53,7 +69,10 @@ func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*st panic(fmt.Errorf("failed to load application version from store: %w", err)) } - ctx := bApp.NewContext(true, cmtproto.Header{}) + bApp.InitChain(cmtabcitypes.RequestInitChain{ChainId: appName}) + bApp.Commit() + + ctx := bApp.NewContext(true, cmtproto.Header{ChainID: appName}) return &App{ BaseApp: bApp, @@ -70,7 +89,27 @@ func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*st // The result of the message execution is returned as a Any type. // That any type can be unmarshaled to the expected response type. // If the message execution fails, an error is returned. -func (app *App) RunMsg(msg sdk.Msg) (*codectypes.Any, error) { +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.AutomaticBeginEndBlock { + height := app.LastBlockHeight() + 1 + app.logger.Info("Running beging block", "height", height) + app.BeginBlock(cmtabcitypes.RequestBeginBlock{Header: cmtproto.Header{Height: height, ChainID: appName}}) + defer func() { + app.logger.Info("Running end block", "height", height) + app.EndBlock(cmtabcitypes.RequestEndBlock{}) + }() + } + app.logger.Info("Running msg", "msg", msg.String()) handler := app.MsgServiceRouter().Handler(msg) @@ -96,10 +135,14 @@ func (app *App) RunMsg(msg sdk.Msg) (*codectypes.Any, error) { return response, nil } -func (app *App) SDKContext() sdk.Context { +// Context returns the application context. +// It can be unwraped to a sdk.Context, with the sdk.UnwrapSDKContext function. +func (app *App) Context() context.Context { return app.ctx } +// QueryHelper returns the application query helper. +// It can be used when registering query services. func (app *App) QueryHelper() *baseapp.QueryServiceTestHelper { return app.queryHelper }