Filters

# Integration Tests

Make sure you have everything you need before proceeding:

In this section, you will:

  • Add integration tests.

In the previous section, you handled wager payments and added unit tests that pass. You added mocks of the bank keeper. Mocks are useful to embody your expectations of the bank keeper's behavior and then quickly confirm that your code does what you expect. By interacting via the CLI, you also confirmed that the bank keeper behaved as you expected.

Now, it would be better to automatically check that your expectations of the bank keeper's behavior are correct. This is done with integration tests, and is the focus of this section.

# What is to be done

In order, you will:

  • Prepare your code to accept integration tests.
  • Create helper functions that will make your integration tests more succinct.
  • Add integration tests that create a full app and test proper token bank balances.

As a reminder:

  • At version 0.45.4 of the Cosmos SDK, an integration test creates a full app.
  • At version 0.47 of the SDK, an integration test creates a minimal app, and a test that creates a full app is called an end-to-end test (E2E).

Fortunately, you do not have to do this from scratch: taking inspiration from tests on the bank module (opens new window), prepare your code so as to accommodate and create a full app that will contain a bank keeper, and add new tests.

For unit tests, each function takes a t *testing.T (opens new window) object. For integration tests, each function will be a method on a test suite that inherits from testify's suite (opens new window). This has the advantage that your test suite can have as many fields as is necessary or useful. The objects that you have used and would welcome in the suite are:

Copy keeper keeper.Keeper msgServer types.MsgServer ctx sdk.Context

You can spread the suite's methods to different files, so as to keep consistent naming for your test files.

When testing, go test will find the suite because you add a regular test (opens new window) that initializes the suite and runs it. The test suite is then automatically initialized with its SetupTest (opens new window) function via its parent suite class. After that, all the methods of the test suite are run.

# Accommodate your code

Copy and adjust from the Cosmos SDK.

1

Ignite CLI created a default constructor for your App with a cosmoscmd.App (opens new window) return type, but this is not convenient. Instead of risking breaking other dependencies, add a new constructor with your App (opens new window) as the return type.

2

Use encoding.go (opens new window) taken from here (opens new window), where you:

  • Import "github.com/ignite-hq/cli/ignite/pkg/cosmoscmd".
  • Replace simappparams.EncodingConfig with cosmoscmd.EncodingConfig.
  • Replace simappparams.MakeTestEncodingConfig with appparams.MakeTestEncodingConfig.
3

Use proto.go (opens new window) taken from here (opens new window), where you:

  • Import "github.com/ignite-hq/cli/ignite/pkg/cosmoscmd".
  • Replace EncodingConfig with cosmoscmd.EncodingConfig.
4

Use test_helpers.go (opens new window) taken from here (opens new window), in which you:

  • Adjust from SimApp to App

  • Adjust from New() to NewApp()

  • Initialize your checkers genesis:

    Copy checkersGenesis := types.DefaultGenesis() genesisState[types.ModuleName] = app.AppCodec().MustMarshalJSON(checkersGenesis) app test_helpers.go View source
5

Define your test suite in a new keeper_integration_suite_test.go file in a dedicated folder tests/integration/checkers/keeper:

Copy type IntegrationTestSuite struct { suite.Suite app *checkersapp.App msgServer types.MsgServer ctx sdk.Context queryClient types.QueryClient } tests integration ... keeper keeper_integration_suite_test.go View source
6

Direct go test to it:

Copy func TestCheckersKeeperTestSuite(t *testing.T) { suite.Run(t, new(IntegrationTestSuite)) } tests integration ... keeper keeper_integration_suite_test.go View source
7

Create the suite.SetupTest function, taking inspiration from the bank tests (opens new window):

Copy func (suite *IntegrationTestSuite) SetupTest() { app := checkersapp.Setup(false) ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Now()}) app.AccountKeeper.SetParams(ctx, authtypes.DefaultParams()) app.BankKeeper.SetParams(ctx, banktypes.DefaultParams()) checkersModuleAddress = app.AccountKeeper.GetModuleAddress(types.ModuleName).String() queryHelper := baseapp.NewQueryServerTestHelper(ctx, app.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, app.CheckersKeeper) queryClient := types.NewQueryClient(queryHelper) suite.app = app suite.msgServer = keeper.NewMsgServerImpl(app.CheckersKeeper) suite.ctx = ctx suite.queryClient = queryClient } tests integration ... keeper keeper_integration_suite_test.go View source

This SetupTest function (opens new window) is like a beforeEach as found in other test libraries. With it, you always get a new app in each test, without interference between them. Do not omit it (opens new window) unless you have specific reasons to do so.

It collects your checkersModuleAddress for later use in tests that check events and balances:

Copy var ( checkersModuleAddress string ) tests integration ... keeper keeper_integration_suite_test.go View source

# Test the test suite

You can now confirm you did all this correctly by running these new keeper integration tests, although the suite has no tests. Note how the path to call has changed:

# Helpers for money checking

Your upcoming integration tests will include checks on wagers being paid, lost, and won, so your tests need to initialize some bank balances for your players. This is made easier with a few helpers, including a helper to confirm a bank balance.

  1. Make a bank genesis Balance (opens new window) type from primitives:

    Copy func makeBalance(address string, balance int64) banktypes.Balance { return banktypes.Balance{ Address: address, Coins: sdk.Coins{ sdk.Coin{ Denom: sdk.DefaultBondDenom, Amount: sdk.NewInt(balance), }, }, } } tests integration ... keeper keeper_integration_suite_test.go View source
  2. Declare default accounts and balances that will be useful for you:

    Copy import ( "github.com/alice/checkers/x/checkers/testutil" ) const ( alice = testutil.Alice bob = testutil.Bob carol = testutil.Carol ) const ( balAlice = 50000000 balBob = 20000000 balCarol = 10000000 ) tests integration ... keeper keeper_integration_suite_test.go View source
  3. Make your preferred bank genesis state:

    Copy func getBankGenesis() *banktypes.GenesisState { coins := []banktypes.Balance{ makeBalance(alice, balAlice), makeBalance(bob, balBob), makeBalance(carol, balCarol), } supply := banktypes.Supply{ Total: coins[0].Coins.Add(coins[1].Coins...).Add(coins[2].Coins...) } state := banktypes.NewGenesisState( banktypes.DefaultParams(), coins, supply.Total, []banktypes.Metadata{}) return state } tests integration ... keeper keeper_integration_suite_test.go View source
  4. Add a simple function to prepare your suite with your desired balances:

    Copy func (suite *IntegrationTestSuite) setupSuiteWithBalances() { suite.app.BankKeeper.InitGenesis(suite.ctx, getBankGenesis()) } tests integration ... keeper keeper_integration_suite_test.go View source
  5. Add a function to check balances from primitives:

    Copy func (suite *IntegrationTestSuite) RequireBankBalance(expected int, atAddress string) { sdkAdd, err := sdk.AccAddressFromBech32(atAddress) suite.Require().Nil(err, "Failed to parse address: %s", atAddress) suite.Require().Equal( int64(expected), suite.app.BankKeeper.GetBalance(suite.ctx, sdkAdd, sdk.DefaultBondDenom).Amount.Int64()) } tests integration ... keeper keeper_integration_suite_test.go View source

With the preparation done, what does an integration test method look like?

# Anatomy of an integration suite test

Now you must add integration tests for your keeper in new files. What does an integration test look like? Take the example of a simple unit test (opens new window) ported to the integration test suite:

  1. The method has a declaration:

    Copy func (suite *IntegrationTestSuite) TestCreate1GameHasSaved() tests integration ... keeper msg_server_create_game_test.go View source

    It is declared as a member of your test suite, and is prefixed with Test (opens new window).

  2. The setup can be done as you prefer, but just like for unit tests you ought to create a helper and use it. Here one exists already:

    Copy suite.setupSuiteWithBalances() goCtx := sdk.WrapSDKContext(suite.ctx) tests integration ... keeper msg_server_create_game_test.go View source
  3. The action is no different from a unit test's action, other than that you get the keeper or msgServer from the suite's fields:

    Copy suite.msgServer.CreateGame(goCtx, &types.MsgCreateGame{ Creator: alice, Red: bob, Black: carol, Wager: 45, }) keeper := suite.app.CheckersKeeper tests integration ... keeper msg_server_create_game_test.go View source
  4. The verification is done with suite.Require().X, but otherwise looks similar to the shorter require.X of unit tests:

    Copy systemInfo, found := keeper.GetSystemInfo(suite.ctx) suite.Require().True(found) suite.Require().EqualValues(types.SystemInfo{ NextId: 2, FifoHeadIndex: "1", FifoTailIndex: "1", }, systemInfo) tests integration ... keeper msg_server_create_game_test.go View source

    In fact, it is exactly the same require (opens new window) object.

You have added an integration test that copies an existing unit test. It demonstrates the concept but is of limited additional utility.

# Extra tests

It is time to add extra tests that check money handling by the bank. Before jumping in, as you did in play unit tests you can add a method that prepares your suite's keeper with a game ready to be played on:

Copy func (suite *IntegrationTestSuite) setupSuiteWithOneGameForPlayMove() { suite.setupSuiteWithBalances() goCtx := sdk.WrapSDKContext(suite.ctx) suite.msgServer.CreateGame(goCtx, &types.MsgCreateGame{ Creator: alice, Black: bob, Red: carol, Wager: 45, }) } tests integration ... keeper msg_server_play_move_test.go View source

You will call this function from the relevant tests.

For the tests proper, before an action that you expect to transfer money (or not), you can verify the initial position:

Copy suite.RequireBankBalance(balAlice, alice) suite.RequireBankBalance(balBob, bob) suite.RequireBankBalance(balCarol, carol) suite.RequireBankBalance(0, checkersModuleAddress) tests integration ... keeper msg_server_play_move_test.go View source

After the action you can test the new balances, for instance:

Copy suite.RequireBankBalance(balAlice, alice) suite.RequireBankBalance(balBob-45, bob) suite.RequireBankBalance(balCarol, carol) suite.RequireBankBalance(45, checkersModuleAddress) tests integration ... keeper msg_server_play_move_test.go View source

How you subdivide your tests and where you insert these balance checks is up to you. You can find examples here for:

# What happened to the events?

With the new tests, you may think that the events are compromised. For instance, the event type "transfer" normally comes with three attributes, but when the bank has made two transfers the "transfer" event ends up with 6 attributes. This is just the way events are organized: per type, with the attributes piled in.

When checking emitted events, you need to skip over the attributes you are not checking. You can easily achieve that with Go slices.

For instance, here transferEvent.Attributes[6:] discards the first six attributes:

Copy transferEvent := events[6] suite.Require().Equal(transferEvent.Type, "transfer") suite.Require().EqualValues([]sdk.Attribute{ {Key: "recipient", Value: carol}, {Key: "sender", Value: checkersModuleAddress}, {Key: "amount", Value: "90stake"}, }, transferEvent.Attributes[6:]) tests integration ... keeper end_block_server_game_test.go View source

# Debug your suite

You learned in a previous section how to launch a test in debug mode. It is still possible to do so when using a suite. Depending on the versions of your Go installation and your Visual Studio Code, you can launch it in two ways:

  1. Right-click on the arrow to the left of the suite's runner func TestCheckersKeeperTestSuite:

    In this case, you can only launch debug for all of the suite's test methods and not just a single one (as is possible with a simple test).

  2. Right-click on the arrow to the left of the separate test of the suite:

    This option may not be available. If being able to debug only a few tests at a time is important to you, a solution is to create more granular suites, for example using one or more test suites per file and falling back on the first option.

With this you have tested your wager payments in a way more realistic that unit tests and mocks.

synopsis

To summarize, this section has explored:

  • How to prepare your code so as to accommodate integration tests.
  • How an integration test is built, and what is a test suite.
  • How to add simple integration tests and helpers.
  • How to add meaningful integration tests and account for how the events are emitted.
  • How to debug your integration tests.