feat: integration test helpers (#15556)

This commit is contained in:
Julien Robert 2023-03-31 20:02:15 +02:00 committed by GitHub
parent 43a4db5a9e
commit aeaa301506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 372 additions and 158 deletions

View File

@ -3,6 +3,8 @@
## Changelog
* 2022-08-02: Initial Draft
* 2023-03-02: Add precision for integration tests
* 2023-03-23: Add precision for E2E tests
## Status
@ -55,13 +57,11 @@ These are almost like integration tests in that they exercise many things togeth
use mocks.
Example 1 journey vs illustrative tests - [depinject's BDD style tests](https://github.com/cosmos/cosmos-sdk/blob/main/depinject/features/bindings.feature), show how we can
rapidly build up many illustrative cases demonstrating behavioral rules without [very much
code](https://github.com/cosmos/cosmos-sdk/blob/main/depinject/binding_test.go) while maintaining high level readability.
rapidly build up many illustrative cases demonstrating behavioral rules without [very much code](https://github.com/cosmos/cosmos-sdk/blob/main/depinject/binding_test.go) while maintaining high level readability.
Example 2 [depinject table driven tests](https://github.com/cosmos/cosmos-sdk/blob/main/depinject/provider_desc_test.go)
Example 3 [Bank keeper tests](https://github.com/cosmos/cosmos-sdk/blob/2bec9d2021918650d3938c3ab242f84289daef80/x/bank/keeper/keeper_test.go#L94-L105) - A mock implementation of `AccountKeeper` is
supplied to the keeper constructor.
Example 3 [Bank keeper tests](https://github.com/cosmos/cosmos-sdk/blob/2bec9d2021918650d3938c3ab242f84289daef80/x/bank/keeper/keeper_test.go#L94-L105) - A mock implementation of `AccountKeeper` is supplied to the keeper constructor.
#### Limitations
@ -147,6 +147,9 @@ End to end tests exercise the entire system as we understand it in as close an a
to a production environment as is practical. Presently these tests are located at
[tests/e2e](https://github.com/cosmos/cosmos-sdk/tree/main/tests/e2e) and rely on [testutil/network](https://github.com/cosmos/cosmos-sdk/tree/main/testutil/network) to start up an in-process Tendermint node.
An application should be built as minimally as possible to exercise the desired functionality.
The SDK uses an application will only the required modules for the tests. The application developer is adviced to use its own application for e2e tests.
#### Limitations
In general the limitations of end to end tests are orchestration and compute cost.
@ -162,12 +165,14 @@ The scope of e2e tests has been complected with command line interface testing.
We accept these test scopes and identify the following decisions points for each.
| Scope | App Fixture | Mocks? |
| ----------- | ----------- | ------ |
| Unit | None | Yes |
| Integration | depinject | Some |
| Simulation | depinject | No |
| E2E | simapp | No |
| Scope | App Type | Mocks? |
| ----------- | ------------------- | ------ |
| Unit | None | Yes |
| Integration | integration helpers | Some |
| Simulation | minimal app | No |
| E2E | minimal app | No |
The decision above is valid for the SDK. An application developer should test their application with their full application instead of the minimal app.
### Unit Tests
@ -175,8 +180,6 @@ All modules must have mocked unit test coverage.
Illustrative tests should outnumber journeys in unit tests.
~BDD feature tests are recommended when building up illustrative and journey scenarios.~
Unit tests should outnumber integration tests.
Unit tests must not introduce additional dependencies beyond those already present in
@ -200,7 +203,7 @@ Integration tests should outnumber e2e tests.
### Simulations
Simulations shall use `depinject`. They are located under `/x/{moduleName}/simulation`.
Simulations shall use a minimal application (usually via app wiring). They are located under `/x/{moduleName}/simulation`.
### E2E Tests
@ -233,7 +236,6 @@ demonstrated in [PR#12706](https://github.com/cosmos/cosmos-sdk/pull/12706).
### Neutral
* learning curve for BDD style tests
* some discovery required for e2e transition to dockertest
## Further Discussions

View File

@ -75,7 +75,7 @@ x/{module_name}
└── README.md
```
* `client/`: The module's CLI client functionality implementation and the module's integration testing suite.
* `client/`: The module's CLI client functionality implementation and the module's CLI testing suite.
* `exported/`: The module's exported types - typically interface types. If a module relies on keepers from another module, it is expected to receive the keepers as interface contracts through the `expected_keepers.go` file (see below) in order to avoid a direct dependency on the module implementing the keepers. However, these interface contracts can define methods that operate on and/or return types that are specific to the module that is implementing the keepers and this is where `exported/` comes into play. The interface types that are defined in `exported/` use canonical types, allowing for the module to receive the keepers as interface contracts through the `expected_keepers.go` file. This pattern allows for code to remain DRY and also alleviates import cycle chaos.
* `keeper/`: The module's `Keeper` and `MsgServer` implementation.
* `module/`: The module's `AppModule` and `AppModuleBasic` implementation.

View File

@ -57,34 +57,16 @@ https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/x/gov/keeper/keeper_test.g
Integration tests are at the second level of the [test pyramid](https://martinfowler.com/articles/practical-test-pyramid.html).
In the SDK, we locate our integration tests under [`/tests/integrations`](https://github.com/cosmos/cosmos-sdk/tree/main/tests/integration).
The goal of these integration tests is to test a component with a minimal application (i.e. not `simapp`). The minimal application is defined with the help of [`depinject`](../tooling/02-depinject.md) the SDK dependency injection framework, and includes all necessary modules to test the component. With the helps of the SDK testing package, we can easily create a minimal application and start the application with a set of genesis transactions: <https://github.com/cosmos/cosmos-sdk/blob/main/testutil/sims/app_helpers.go>.
The goal of these integration tests is to test how a component interacts with other dependencies. Compared to unit tests, integration tests do not mock dependencies. Instead, they use the direct dependencies of the component. This differs as well from end-to-end tests, which test the component with a full application.
Integration tests interact with the tested module via the defined `Msg` and `Query` services. The result of the test can be verified by checking the state of the application, by checking the emitted events or the response. It is adviced to combine two of these methods to verify the result of the test.
The SDK provides small helpers for quickly setting up an integration tests. These helpers can be found at <https://github.com/cosmos/cosmos-sdk/blob/main/testutil/integration>.
### Example
Here, we will walkthrough the integration tests of the `x/distribution` module. The `x/distribution` module has, in addition to keeper unit tests, integration tests that test the `x/distribution` module with a minimal application. This is expected as you may want to test the `x/distribution` module with actual application logic, instead of only mocked dependencies.
For creating a minimal application, we use [`simtestutil.Setup`](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/testutil/sims/app_helpers.go#L95-L99) and an [`AppConfig`](../tooling/02-depinject.md) of the `x/distribution` minimal dependencies.
For instance, the `AppConfig` of `x/distribution` is defined as:
* https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/x/distribution/testutil/app_config.go
This is a stripped down version of the `simapp` `AppConfig`:
* https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/simapp/app_config.go
:::note
You can as well use the `AppConfig` `configurator` for creating an `AppConfig` [inline](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/x/slashing/app_test.go#L54-L62). There no difference between those two ways, use whichever you prefer.
:::
```go reference
https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/tests/integration/distribution/keeper/keeper_test.go#L28-L33
```
Now the types are injected and we can use them for our tests:
```go reference
https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/tests/integration/distribution/keeper/keeper_test.go#L21-L53
https://github.com/cosmos/cosmos-sdk/blob/29e22b3bdb05353555c8e0b269311bbff7b8deca/testutil/integration/example_test.go#L22-L89
```
## Deterministic and Regression tests
@ -106,6 +88,10 @@ https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/tests/integration/bank/kee
Simulations uses as well a minimal application, built with [`depinject`](../tooling/02-depinject.md):
:::note
You can as well use the `AppConfig` `configurator` for creating an `AppConfig` [inline](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/x/slashing/app_test.go#L54-L62). There is no difference between those two ways, use whichever you prefer.
:::
Following is an example for `x/gov/` simulations:
```go reference
@ -121,6 +107,7 @@ https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/x/gov/simulation/operation
End-to-end tests are at the top of the [test pyramid](https://martinfowler.com/articles/practical-test-pyramid.html).
They must test the whole application flow, from the user perspective (for instance, CLI tests). They are located under [`/tests/e2e`](https://github.com/cosmos/cosmos-sdk/tree/main/tests/e2e).
<!-- @julienrbrt: makes more sense to use an app wired app to have 0 simapp dependencies -->
For that, the SDK is using `simapp` but you should use your own application (`appd`).
Here are some examples:
@ -132,11 +119,6 @@ Here are some examples:
The SDK is in the process of creating its E2E tests, as defined in [ADR-59](https://docs.cosmos.network/main/architecture/adr-059-test-scopes.html). This page will eventually be updated with better examples.
:::
## Summary
## Learn More
| Scope | App Fixture | Mocks? |
| ----------- | ----------- | ------ |
| Unit | None | Yes |
| Integration | `depinject` | Some |
| Simulation | `depinject` | No |
| E2E | `appd` | No |
Learn more about testing scope in [ADR-59](https://docs.cosmos.network/main/architecture/adr-059-test-scopes.html).

View File

@ -7,19 +7,16 @@ import (
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"gotest.tools/v3/assert"
"github.com/cosmos/cosmos-sdk/baseapp"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/slashing/testutil"
"github.com/cosmos/cosmos-sdk/x/staking"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtestutil "github.com/cosmos/cosmos-sdk/x/staking/testutil"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
@ -28,15 +25,11 @@ import (
var InitTokens = sdk.TokensFromConsensusPower(200, sdk.DefaultPowerReduction)
type fixture struct {
ctx sdk.Context
slashingKeeper slashingkeeper.Keeper
stakingKeeper *stakingkeeper.Keeper
bankKeeper bankkeeper.Keeper
accountKeeper authkeeper.AccountKeeper
interfaceRegistry codectypes.InterfaceRegistry
addrDels []sdk.AccAddress
queryClient slashingtypes.QueryClient
msgServer slashingtypes.MsgServer
ctx sdk.Context
slashingKeeper slashingkeeper.Keeper
stakingKeeper *stakingkeeper.Keeper
bankKeeper bankkeeper.Keeper
addrDels []sdk.AccAddress
}
func initFixture(t assert.TestingT) *fixture {
@ -44,10 +37,8 @@ func initFixture(t assert.TestingT) *fixture {
app, err := simtestutil.Setup(
testutil.AppConfig,
&f.bankKeeper,
&f.accountKeeper,
&f.slashingKeeper,
&f.stakingKeeper,
&f.interfaceRegistry,
)
assert.NilError(t, err)
@ -55,24 +46,16 @@ func initFixture(t assert.TestingT) *fixture {
// TestParams set the SignedBlocksWindow to 1000 and MaxMissedBlocksPerWindow to 500
f.slashingKeeper.SetParams(ctx, testutil.TestParams())
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, ctx, 5, f.stakingKeeper.TokensFromConsensusPower(ctx, 200))
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, ctx, 5, f.stakingKeeper.TokensFromConsensusPower(f.ctx, 200))
info1 := slashingtypes.NewValidatorSigningInfo(sdk.ConsAddress(addrDels[0]), int64(4), int64(3),
time.Unix(2, 0), false, int64(10))
info2 := slashingtypes.NewValidatorSigningInfo(sdk.ConsAddress(addrDels[1]), int64(5), int64(4),
time.Unix(2, 0), false, int64(10))
info1 := slashingtypes.NewValidatorSigningInfo(sdk.ConsAddress(addrDels[0]), int64(4), int64(3), time.Unix(2, 0), false, int64(10))
info2 := slashingtypes.NewValidatorSigningInfo(sdk.ConsAddress(addrDels[1]), int64(5), int64(4), time.Unix(2, 0), false, int64(10))
f.slashingKeeper.SetValidatorSigningInfo(ctx, sdk.ConsAddress(addrDels[0]), info1)
f.slashingKeeper.SetValidatorSigningInfo(ctx, sdk.ConsAddress(addrDels[1]), info2)
queryHelper := baseapp.NewQueryServerTestHelper(ctx, f.interfaceRegistry)
slashingtypes.RegisterQueryServer(queryHelper, f.slashingKeeper)
queryClient := slashingtypes.NewQueryClient(queryHelper)
f.queryClient = queryClient
f.addrDels = addrDels
f.ctx = ctx
f.msgServer = slashingkeeper.NewMsgServerImpl(f.slashingKeeper)
return f
}
@ -81,16 +64,14 @@ func TestUnJailNotBonded(t *testing.T) {
t.Parallel()
f := initFixture(t)
ctx := f.ctx
p := f.stakingKeeper.GetParams(ctx)
p := f.stakingKeeper.GetParams(f.ctx)
p.MaxValidators = 5
f.stakingKeeper.SetParams(ctx, p)
f.stakingKeeper.SetParams(f.ctx, p)
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, ctx, 6, f.stakingKeeper.TokensFromConsensusPower(ctx, 200))
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, f.ctx, 6, f.stakingKeeper.TokensFromConsensusPower(f.ctx, 200))
valAddrs := simtestutil.ConvertAddrsToValAddrs(addrDels)
pks := simtestutil.CreateTestPubKeys(6)
tstaking := stakingtestutil.NewHelper(t, ctx, f.stakingKeeper)
tstaking := stakingtestutil.NewHelper(t, f.ctx, f.stakingKeeper)
// create max (5) validators all with the same power
for i := uint32(0); i < p.MaxValidators; i++ {
@ -98,46 +79,46 @@ func TestUnJailNotBonded(t *testing.T) {
tstaking.CreateValidatorWithValPower(addr, val, 100, true)
}
staking.EndBlocker(ctx, f.stakingKeeper)
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
staking.EndBlocker(f.ctx, f.stakingKeeper)
f.ctx = f.ctx.WithBlockHeight(f.ctx.BlockHeight() + 1)
// create a 6th validator with less power than the cliff validator (won't be bonded)
addr, val := valAddrs[5], pks[5]
amt := f.stakingKeeper.TokensFromConsensusPower(ctx, 50)
amt := f.stakingKeeper.TokensFromConsensusPower(f.ctx, 50)
msg := tstaking.CreateValidatorMsg(addr, val, amt)
msg.MinSelfDelegation = amt
res, err := tstaking.CreateValidatorWithMsg(ctx, msg)
res, err := tstaking.CreateValidatorWithMsg(f.ctx, msg)
assert.NilError(t, err)
assert.Assert(t, res != nil)
staking.EndBlocker(ctx, f.stakingKeeper)
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
staking.EndBlocker(f.ctx, f.stakingKeeper)
f.ctx = f.ctx.WithBlockHeight(f.ctx.BlockHeight() + 1)
tstaking.CheckValidator(addr, stakingtypes.Unbonded, false)
// unbond below minimum self-delegation
assert.Equal(t, p.BondDenom, tstaking.Denom)
tstaking.Undelegate(sdk.AccAddress(addr), addr, f.stakingKeeper.TokensFromConsensusPower(ctx, 1), true)
tstaking.Undelegate(sdk.AccAddress(addr), addr, f.stakingKeeper.TokensFromConsensusPower(f.ctx, 1), true)
staking.EndBlocker(ctx, f.stakingKeeper)
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
staking.EndBlocker(f.ctx, f.stakingKeeper)
f.ctx = f.ctx.WithBlockHeight(f.ctx.BlockHeight() + 1)
// verify that validator is jailed
tstaking.CheckValidator(addr, -1, true)
// verify we cannot unjail (yet)
assert.ErrorContains(t, f.slashingKeeper.Unjail(ctx, addr), "cannot be unjailed")
assert.ErrorContains(t, f.slashingKeeper.Unjail(f.ctx, addr), "cannot be unjailed")
staking.EndBlocker(ctx, f.stakingKeeper)
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
staking.EndBlocker(f.ctx, f.stakingKeeper)
f.ctx = f.ctx.WithBlockHeight(f.ctx.BlockHeight() + 1)
// bond to meet minimum self-delegation
tstaking.DelegateWithPower(sdk.AccAddress(addr), addr, 1)
staking.EndBlocker(ctx, f.stakingKeeper)
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
staking.EndBlocker(f.ctx, f.stakingKeeper)
f.ctx = f.ctx.WithBlockHeight(f.ctx.BlockHeight() + 1)
// verify we can immediately unjail
assert.NilError(t, f.slashingKeeper.Unjail(ctx, addr))
assert.NilError(t, f.slashingKeeper.Unjail(f.ctx, addr))
tstaking.CheckValidator(addr, -1, false)
}
@ -149,45 +130,43 @@ func TestHandleNewValidator(t *testing.T) {
t.Parallel()
f := initFixture(t)
ctx := f.ctx
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, ctx, 1, f.stakingKeeper.TokensFromConsensusPower(ctx, 0))
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, f.ctx, 1, f.stakingKeeper.TokensFromConsensusPower(f.ctx, 0))
valAddrs := simtestutil.ConvertAddrsToValAddrs(addrDels)
pks := simtestutil.CreateTestPubKeys(1)
addr, val := valAddrs[0], pks[0]
tstaking := stakingtestutil.NewHelper(t, ctx, f.stakingKeeper)
ctx = ctx.WithBlockHeight(f.slashingKeeper.SignedBlocksWindow(ctx) + 1)
tstaking := stakingtestutil.NewHelper(t, f.ctx, f.stakingKeeper)
f.ctx = f.ctx.WithBlockHeight(f.slashingKeeper.SignedBlocksWindow(f.ctx) + 1)
// Validator created
amt := tstaking.CreateValidatorWithValPower(addr, val, 100, true)
staking.EndBlocker(ctx, f.stakingKeeper)
staking.EndBlocker(f.ctx, f.stakingKeeper)
assert.DeepEqual(
t, f.bankKeeper.GetAllBalances(ctx, sdk.AccAddress(addr)),
sdk.NewCoins(sdk.NewCoin(f.stakingKeeper.GetParams(ctx).BondDenom, InitTokens.Sub(amt))),
t, f.bankKeeper.GetAllBalances(f.ctx, sdk.AccAddress(addr)),
sdk.NewCoins(sdk.NewCoin(f.stakingKeeper.GetParams(f.ctx).BondDenom, InitTokens.Sub(amt))),
)
assert.DeepEqual(t, amt, f.stakingKeeper.Validator(ctx, addr).GetBondedTokens())
assert.DeepEqual(t, amt, f.stakingKeeper.Validator(f.ctx, addr).GetBondedTokens())
// Now a validator, for two blocks
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), 100, true)
ctx = ctx.WithBlockHeight(f.slashingKeeper.SignedBlocksWindow(ctx) + 2)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), 100, false)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), 100, true)
f.ctx = f.ctx.WithBlockHeight(f.slashingKeeper.SignedBlocksWindow(f.ctx) + 2)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), 100, false)
info, found := f.slashingKeeper.GetValidatorSigningInfo(ctx, sdk.ConsAddress(val.Address()))
info, found := f.slashingKeeper.GetValidatorSigningInfo(f.ctx, sdk.ConsAddress(val.Address()))
assert.Assert(t, found)
assert.Equal(t, f.slashingKeeper.SignedBlocksWindow(ctx)+1, info.StartHeight)
assert.Equal(t, f.slashingKeeper.SignedBlocksWindow(f.ctx)+1, info.StartHeight)
assert.Equal(t, int64(2), info.IndexOffset)
assert.Equal(t, int64(1), info.MissedBlocksCounter)
assert.Equal(t, time.Unix(0, 0).UTC(), info.JailedUntil)
// validator should be bonded still, should not have been jailed or slashed
validator, _ := f.stakingKeeper.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val))
validator, _ := f.stakingKeeper.GetValidatorByConsAddr(f.ctx, sdk.GetConsAddress(val))
assert.Equal(t, stakingtypes.Bonded, validator.GetStatus())
bondPool := f.stakingKeeper.GetBondedPool(ctx)
expTokens := f.stakingKeeper.TokensFromConsensusPower(ctx, 100)
bondPool := f.stakingKeeper.GetBondedPool(f.ctx)
expTokens := f.stakingKeeper.TokensFromConsensusPower(f.ctx, 100)
// adding genesis validator tokens
expTokens = expTokens.Add(f.stakingKeeper.TokensFromConsensusPower(ctx, 1))
assert.Assert(t, expTokens.Equal(f.bankKeeper.GetBalance(ctx, bondPool.GetAddress(), f.stakingKeeper.BondDenom(ctx)).Amount))
expTokens = expTokens.Add(f.stakingKeeper.TokensFromConsensusPower(f.ctx, 1))
assert.Assert(t, expTokens.Equal(f.bankKeeper.GetBalance(f.ctx, bondPool.GetAddress(), f.stakingKeeper.BondDenom(f.ctx)).Amount))
}
// Test a jailed validator being "down" twice
@ -196,50 +175,47 @@ func TestHandleAlreadyJailed(t *testing.T) {
t.Parallel()
f := initFixture(t)
// initial setup
ctx := f.ctx
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, ctx, 1, f.stakingKeeper.TokensFromConsensusPower(ctx, 200))
addrDels := simtestutil.AddTestAddrsIncremental(f.bankKeeper, f.stakingKeeper, f.ctx, 1, f.stakingKeeper.TokensFromConsensusPower(f.ctx, 200))
valAddrs := simtestutil.ConvertAddrsToValAddrs(addrDels)
pks := simtestutil.CreateTestPubKeys(1)
addr, val := valAddrs[0], pks[0]
power := int64(100)
tstaking := stakingtestutil.NewHelper(t, ctx, f.stakingKeeper)
tstaking := stakingtestutil.NewHelper(t, f.ctx, f.stakingKeeper)
amt := tstaking.CreateValidatorWithValPower(addr, val, power, true)
staking.EndBlocker(ctx, f.stakingKeeper)
staking.EndBlocker(f.ctx, f.stakingKeeper)
// 1000 first blocks OK
height := int64(0)
for ; height < f.slashingKeeper.SignedBlocksWindow(ctx); height++ {
ctx = ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), power, true)
for ; height < f.slashingKeeper.SignedBlocksWindow(f.ctx); height++ {
f.ctx = f.ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), power, true)
}
// 501 blocks missed
for ; height < f.slashingKeeper.SignedBlocksWindow(ctx)+(f.slashingKeeper.SignedBlocksWindow(ctx)-f.slashingKeeper.MinSignedPerWindow(ctx))+1; height++ {
ctx = ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), power, false)
for ; height < f.slashingKeeper.SignedBlocksWindow(f.ctx)+(f.slashingKeeper.SignedBlocksWindow(f.ctx)-f.slashingKeeper.MinSignedPerWindow(f.ctx))+1; height++ {
f.ctx = f.ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), power, false)
}
// end block
staking.EndBlocker(ctx, f.stakingKeeper)
staking.EndBlocker(f.ctx, f.stakingKeeper)
// validator should have been jailed and slashed
validator, _ := f.stakingKeeper.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val))
validator, _ := f.stakingKeeper.GetValidatorByConsAddr(f.ctx, sdk.GetConsAddress(val))
assert.Equal(t, stakingtypes.Unbonding, validator.GetStatus())
// validator should have been slashed
resultingTokens := amt.Sub(f.stakingKeeper.TokensFromConsensusPower(ctx, 1))
resultingTokens := amt.Sub(f.stakingKeeper.TokensFromConsensusPower(f.ctx, 1))
assert.DeepEqual(t, resultingTokens, validator.GetTokens())
// another block missed
ctx = ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), power, false)
f.ctx = f.ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), power, false)
// validator should not have been slashed twice
validator, _ = f.stakingKeeper.GetValidatorByConsAddr(ctx, sdk.GetConsAddress(val))
validator, _ = f.stakingKeeper.GetValidatorByConsAddr(f.ctx, sdk.GetConsAddress(val))
assert.DeepEqual(t, resultingTokens, validator.GetTokens())
}
@ -250,74 +226,72 @@ func TestValidatorDippingInAndOut(t *testing.T) {
t.Parallel()
f := initFixture(t)
// initial setup
ctx := f.ctx
params := f.stakingKeeper.GetParams(ctx)
params := f.stakingKeeper.GetParams(f.ctx)
params.MaxValidators = 1
f.stakingKeeper.SetParams(ctx, params)
f.stakingKeeper.SetParams(f.ctx, params)
power := int64(100)
pks := simtestutil.CreateTestPubKeys(3)
simtestutil.AddTestAddrsFromPubKeys(f.bankKeeper, f.stakingKeeper, ctx, pks, f.stakingKeeper.TokensFromConsensusPower(ctx, 200))
simtestutil.AddTestAddrsFromPubKeys(f.bankKeeper, f.stakingKeeper, f.ctx, pks, f.stakingKeeper.TokensFromConsensusPower(f.ctx, 200))
addr, val := pks[0].Address(), pks[0]
consAddr := sdk.ConsAddress(addr)
tstaking := stakingtestutil.NewHelper(t, ctx, f.stakingKeeper)
tstaking := stakingtestutil.NewHelper(t, f.ctx, f.stakingKeeper)
valAddr := sdk.ValAddress(addr)
tstaking.CreateValidatorWithValPower(valAddr, val, power, true)
validatorUpdates := staking.EndBlocker(ctx, f.stakingKeeper)
validatorUpdates := staking.EndBlocker(f.ctx, f.stakingKeeper)
assert.Equal(t, 2, len(validatorUpdates))
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)
// 100 first blocks OK
height := int64(0)
for ; height < int64(100); height++ {
ctx = ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), power, true)
f.ctx = f.ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), power, true)
}
// kick first validator out of validator set
tstaking.CreateValidatorWithValPower(sdk.ValAddress(pks[1].Address()), pks[1], power+1, true)
validatorUpdates = staking.EndBlocker(ctx, f.stakingKeeper)
validatorUpdates = staking.EndBlocker(f.ctx, f.stakingKeeper)
assert.Equal(t, 2, len(validatorUpdates))
tstaking.CheckValidator(sdk.ValAddress(pks[1].Address()), stakingtypes.Bonded, false)
tstaking.CheckValidator(valAddr, stakingtypes.Unbonding, false)
// 600 more blocks happened
height += 600
ctx = ctx.WithBlockHeight(height)
f.ctx = f.ctx.WithBlockHeight(height)
// validator added back in
tstaking.DelegateWithPower(sdk.AccAddress(pks[2].Address()), valAddr, 50)
validatorUpdates = staking.EndBlocker(ctx, f.stakingKeeper)
validatorUpdates = staking.EndBlocker(f.ctx, f.stakingKeeper)
assert.Equal(t, 2, len(validatorUpdates))
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)
newPower := power + 50
// validator misses a block
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, false)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), newPower, false)
height++
// shouldn't be jailed/kicked yet
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)
// validator misses an additional 500 more blocks within the SignedBlockWindow (here 1000 blocks).
latest := f.slashingKeeper.SignedBlocksWindow(ctx) + height
latest := f.slashingKeeper.SignedBlocksWindow(f.ctx) + height
// misses 500 blocks + within the signing windows i.e. 700-1700
// validators misses all 1000 block of a SignedBlockWindows
for ; height < latest+1; height++ {
ctx = ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, false)
f.ctx = f.ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), newPower, false)
}
// should now be jailed & kicked
staking.EndBlocker(ctx, f.stakingKeeper)
staking.EndBlocker(f.ctx, f.stakingKeeper)
tstaking.CheckValidator(valAddr, stakingtypes.Unbonding, true)
// check all the signing information
signInfo, found := f.slashingKeeper.GetValidatorSigningInfo(ctx, consAddr)
signInfo, found := f.slashingKeeper.GetValidatorSigningInfo(f.ctx, consAddr)
assert.Assert(t, found)
assert.Equal(t, int64(700), signInfo.StartHeight)
assert.Equal(t, int64(0), signInfo.MissedBlocksCounter)
@ -325,30 +299,30 @@ func TestValidatorDippingInAndOut(t *testing.T) {
// some blocks pass
height = int64(5000)
ctx = ctx.WithBlockHeight(height)
f.ctx = f.ctx.WithBlockHeight(height)
// validator rejoins and starts signing again
f.stakingKeeper.Unjail(ctx, consAddr)
f.stakingKeeper.Unjail(f.ctx, consAddr)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, true)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), newPower, true)
// validator should not be kicked since we reset counter/array when it was jailed
staking.EndBlocker(ctx, f.stakingKeeper)
staking.EndBlocker(f.ctx, f.stakingKeeper)
tstaking.CheckValidator(valAddr, stakingtypes.Bonded, false)
// check start height is correctly set
signInfo, found = f.slashingKeeper.GetValidatorSigningInfo(ctx, consAddr)
signInfo, found = f.slashingKeeper.GetValidatorSigningInfo(f.ctx, consAddr)
assert.Assert(t, found)
assert.Equal(t, height, signInfo.StartHeight)
// validator misses 501 blocks after SignedBlockWindow period (1000 blocks)
latest = f.slashingKeeper.SignedBlocksWindow(ctx) + height
for ; height < latest+f.slashingKeeper.MinSignedPerWindow(ctx); height++ {
ctx = ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(ctx, val.Address(), newPower, false)
latest = f.slashingKeeper.SignedBlocksWindow(f.ctx) + height
for ; height < latest+f.slashingKeeper.MinSignedPerWindow(f.ctx); height++ {
f.ctx = f.ctx.WithBlockHeight(height)
f.slashingKeeper.HandleValidatorSignature(f.ctx, val.Address(), newPower, false)
}
// validator should now be jailed & kicked
staking.EndBlocker(ctx, f.stakingKeeper)
staking.EndBlocker(f.ctx, f.stakingKeeper)
tstaking.CheckValidator(valAddr, stakingtypes.Unbonding, true)
}

View File

@ -0,0 +1,2 @@
// Integration contains the integration test setup used for SDK modules.
package integration

View File

@ -0,0 +1,150 @@
package integration_test
import (
"fmt"
"io"
"cosmossdk.io/log"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/testutil/integration"
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"
authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/mint"
mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
"github.com/google/go-cmp/cmp"
)
// Example shows how to use the integration test framework to test the integration of SDK modules.
// Panics are used in this example, but in a real test case, you should use the testing.T object and assertions.
func Example() {
// in this example we are testing the integration of the following modules:
// - mint, which directly depends on auth, bank and staking
encodingCfg := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, mint.AppModuleBasic{})
keys := storetypes.NewKVStoreKeys(authtypes.StoreKey, minttypes.StoreKey)
authority := authtypes.NewModuleAddress("gov").String()
accountKeeper := authkeeper.NewAccountKeeper(
encodingCfg.Codec,
runtime.NewKVStoreService(keys[authtypes.StoreKey]),
authtypes.ProtoBaseAccount,
map[string][]string{minttypes.ModuleName: {authtypes.Minter}},
"cosmos",
authority,
)
// subspace is nil because we don't test params (which is legacy anyway)
authModule := auth.NewAppModule(encodingCfg.Codec, accountKeeper, authsims.RandomGenesisAccounts, nil)
// here bankkeeper and staking keeper is nil because we are not testing them
// subspace is nil because we don't test params (which is legacy anyway)
mintKeeper := mintkeeper.NewKeeper(encodingCfg.Codec, keys[minttypes.StoreKey], nil, accountKeeper, nil, authtypes.FeeCollectorName, authority)
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)
// register the message and query servers
authtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), authkeeper.NewMsgServerImpl(accountKeeper))
minttypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), mintkeeper.NewMsgServerImpl(mintKeeper))
minttypes.RegisterQueryServer(integrationApp.QueryHelper(), mintKeeper)
params := minttypes.DefaultParams()
params.BlocksPerYear = 10000
// now we can use the application to test a mint message
result, err := integrationApp.RunMsg(&minttypes.MsgUpdateParams{
Authority: authority,
Params: params,
})
if err != nil {
panic(err)
}
// 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 {
panic(fmt.Errorf("unexpected nil result"))
}
// we now check the result
resp := minttypes.MsgUpdateParamsResponse{}
err = encodingCfg.Codec.Unmarshal(result.Value, &resp)
if err != nil {
panic(err)
}
// we should also check the state of the application
got := mintKeeper.GetParams(integrationApp.SDKContext())
if diff := cmp.Diff(got, params); diff != "" {
panic(diff)
}
fmt.Println(got.BlocksPerYear)
// Output: 10000
}
// ExampleOneModule shows how to use the integration test framework to test the integration of a single module.
// That module has no dependency on other modules.
func Example_oneModule() {
// in this example we are testing the integration of the auth module:
encodingCfg := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{})
keys := storetypes.NewKVStoreKeys(authtypes.StoreKey)
authority := authtypes.NewModuleAddress("gov").String()
accountKeeper := authkeeper.NewAccountKeeper(
encodingCfg.Codec,
runtime.NewKVStoreService(keys[authtypes.StoreKey]),
authtypes.ProtoBaseAccount,
map[string][]string{minttypes.ModuleName: {authtypes.Minter}},
"cosmos",
authority,
)
// subspace is nil because we don't test params (which is legacy anyway)
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)
// register the message and query servers
authtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), authkeeper.NewMsgServerImpl(accountKeeper))
params := authtypes.DefaultParams()
params.MaxMemoCharacters = 1000
// now we can use the application to test a mint message
result, err := integrationApp.RunMsg(&authtypes.MsgUpdateParams{
Authority: authority,
Params: params,
})
if err != nil {
panic(err)
}
// 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 {
panic(fmt.Errorf("unexpected nil result"))
}
// we now check the result
resp := authtypes.MsgUpdateParamsResponse{}
err = encodingCfg.Codec.Unmarshal(result.Value, &resp)
if err != nil {
panic(err)
}
// we should also check the state of the application
got := accountKeeper.GetParams(integrationApp.SDKContext())
if diff := cmp.Diff(got, params); diff != "" {
panic(diff)
}
fmt.Println(got.MaxMemoCharacters)
// Output: 1000
}

View File

@ -0,0 +1,105 @@
package integration
import (
"fmt"
"github.com/cometbft/cometbft/abci/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"cosmossdk.io/log"
storetypes "cosmossdk.io/store/types"
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
)
// 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
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 {
db := dbm.NewMemDB()
interfaceRegistry := codectypes.NewInterfaceRegistry()
for _, module := range modules {
module.RegisterInterfaces(interfaceRegistry)
}
txConfig := authtx.NewTxConfig(codec.NewProtoCodec(interfaceRegistry), authtx.DefaultSignModes)
bApp := baseapp.NewBaseApp(fmt.Sprintf("integration-app-%s", nameSuffix), logger, db, txConfig.TxDecoder())
bApp.MountKVStores(keys)
bApp.SetInitChainer(func(ctx sdk.Context, req types.RequestInitChain) (types.ResponseInitChain, error) {
return types.ResponseInitChain{}, nil
})
router := baseapp.NewMsgServiceRouter()
router.SetInterfaceRegistry(interfaceRegistry)
bApp.SetMsgServiceRouter(router)
if err := bApp.LoadLatestVersion(); err != nil {
panic(fmt.Errorf("failed to load application version from store: %w", err))
}
ctx := bApp.NewContext(true, cmtproto.Header{})
return &App{
BaseApp: bApp,
logger: logger,
ctx: ctx,
queryHelper: baseapp.NewQueryServerTestHelper(ctx, interfaceRegistry),
}
}
// RunMsg allows to run a message and return the response.
// In order to run a message, the application must have a handler for it.
// These handlers are registered on the application message service router.
// 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) {
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 {
msgResponse := msgResult.MsgResponses[0]
if msgResponse == nil {
return nil, fmt.Errorf("got nil msg response %s in message result: %s", sdk.MsgTypeURL(msg), msgResult.String())
}
response = msgResponse
}
return response, nil
}
func (app *App) SDKContext() sdk.Context {
return app.ctx
}
func (app *App) QueryHelper() *baseapp.QueryServiceTestHelper {
return app.queryHelper
}

View File

@ -8,7 +8,6 @@ import (
abci "github.com/cometbft/cometbft/abci/types"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/staking/types"