Examples
Test Systems

Testing the World Extension

The commented code can be found in packages/contracts/test/ReadDemoSystem.t.sol (opens in a new tab).

The test should execute by running forge test within the packages/contracts/ folder. If you want to see additional details, you can run forge test -vvvv with 1-5 vs to increase verbosity. I generally recommend 4 or 5 for debug.

Before we deploy this system, we should run tests. We'll be using forge test in this example, to test against a live deployment. pnpm mud test is another option, that has some additional complexities, so we are not using it for now.

We've included three tests in this example, to highlight a couple different situations.

The test file can be intimidating, especially the imports. Most of these are boilerplate for tests and scripts that interact with MUD worlds.

A detailed explanation of most of the imports can be found here (opens in a new tab).

We will highlight some specific additional details in the Explanation below.

examples/ReadDemo/packages/contracts/test/ReadDemoSystem.t.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
 
import { MudTest } from "@latticexyz/world/test/MudTest.t.sol";
import { console2 } from "forge-std/Test.sol";
 
import { WorldRegistrationSystem } from "@latticexyz/world/src/modules/init/implementations/WorldRegistrationSystem.sol";
import { System } from "@latticexyz/world/src/System.sol";
 
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
 
import { ReadDemoSystem } from "../src/systems/ReadDemoSystem.sol";
 
import { IWorld } from "../src/codegen/world/IWorld.sol";
import { IWorld as IPrimodiumWorld } from "primodium/world/IWorld.sol";
 
contract ReadDemoTest is MudTest {
 
  address extensionDeployerAddress = vm.envAddress("ADDRESS_ALICE");
  address playerAddressActive = vm.envAddress("ADDRESS_PLAYER_ACTIVE");
  address playerAddressInactive = vm.envAddress("ADDRESS_PLAYER_INACTIVE");
 
  bytes14 PRIMODIUM_NAMESPACE = bytes14("Primodium");
  bytes14 namespace = bytes14("PluginExamples");
  bytes16 system = bytes16("ReadDemoSystem");
 
  function setUp() public override {
    super.setUp();
 
    worldAddress = vm.envAddress("WORLD_ADDRESS");
    StoreSwitch.setStoreAddress(worldAddress);
 
    vm.createSelectFork(vm.envString("PRIMODIUM_RPC_URL"), vm.envUint("BLOCK_NUMBER"));
    console2.log("\nForkLivePrimodium is running.");
 
    WorldRegistrationSystem world = WorldRegistrationSystem(worldAddress);
 
    ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14(namespace));
    ResourceId systemResource = WorldResourceIdLib.encode(RESOURCE_SYSTEM, namespace, system);
 
    vm.startPrank(extensionDeployerAddress);
 
    world.registerNamespace(namespaceResource);
 
    ReadDemoSystem readDemoSystem = new ReadDemoSystem();
    world.registerSystem(systemResource, readDemoSystem, true);
 
    world.registerFunctionSelector(systemResource, "readMainBaseLevel()");
 
    vm.stopPrank();
  }
 
  function test_ReadMainBaseLevel_Inactive() public {
    vm.startPrank(playerAddressInactive);
 
    uint32 baseLevel = IWorld(worldAddress).PluginExamples__readMainBaseLevel();
 
    vm.stopPrank();
    assertEq(baseLevel, 0, "The base level should be 0 for an Inactive player.");
  }
 
  function test_ReadMainBaseLevel_Active() public {
    vm.startPrank(playerAddressActive);
 
    uint32 baseLevel = IWorld(worldAddress).PluginExamples__readMainBaseLevel();
 
    vm.stopPrank();
    assertEq(baseLevel, 1, "The base level should be 1 for an Active player.");
  }
 
  function test_SpawnAndReadMainBaseLevel() public {
    vm.startPrank(playerAddressInactive);
 
    uint32 baseLevel = IWorld(worldAddress).PluginExamples__readMainBaseLevel();
 
    IPrimodiumWorld(worldAddress).Primodium__spawn();
    baseLevel = IWorld(worldAddress).PluginExamples__readMainBaseLevel();
 
    vm.stopPrank();
    assertEq(baseLevel, 1, "The base level should be 1 for a freshly spawned player.");
  }
}
Explanation
ReadDemoSystem.t.sol:15
import { ReadDemoSystem } from "../src/systems/ReadDemoSystem.sol";

We need to import our System to use it.

ReadDemoSystem.t.sol:17
import { IWorld } from "../src/codegen/world/IWorld.sol";
import { IWorld as IPrimodiumWorld } from "primodium/world/IWorld.sol";

We need the import specific features and types to our World Extension. These are found in ../src/codegen/. We also need to specific features of the Primodium world.

ReadDemoSystem.t.sol:20
contract ReadDemoTest is MudTest {

We will be using the MudTest library, which extends the standard forge Test with some additional supporting functionality.

ReadDemoSystem.t.sol:22
address extensionDeployerAddress = vm.envAddress("ADDRESS_ALICE");
address playerAddressActive = vm.envAddress("ADDRESS_PLAYER_ACTIVE");
address playerAddressInactive = vm.envAddress("ADDRESS_PLAYER_INACTIVE");

Foundry Cheatcodes allows us quick access to .env parameters using vm.envAddress, vm.Uint, etc. https://book.getfoundry.sh/cheatcodes/external (opens in a new tab)

ReadDemoSystem.t.sol:30
function setUp() public override {
  super.setUp();

We need some of the background setUp that occurs in MudTest.

ReadDemoSystem.t.sol:36
vm.createSelectFork(
  vm.envString("PRIMODIUM_RPC_URL"),
  vm.envUint("BLOCK_NUMBER")
);

For these tests, we create a local temporary copy of the live Primodium blockchain, at a specific block number. Changes to this blockchain are not sent back to the real one, so we can do thorough testing without danger of unintended consequences.

ReadDemoSystem.t.sol:39
WorldRegistrationSystem world = WorldRegistrationSystem(worldAddress);

This is our primary interface to Primodium world for the duration of the test.

ReadDemoSystem.t.sol:41
ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14(namespace));
ResourceId systemResource = WorldResourceIdLib.encode(RESOURCE_SYSTEM, namespace, system);

ResourceIds are type wrapped bytes32s. They are the primary type we use to access most game data.

encodeNamespace() and encode() handle the messy bitshifting into appropriate ResourceId formats for interfacing with Tables and Systemss.

ReadDemoSystem.t.sol:49
world.registerSystem(systemResource, readDemoSystem, true);

This line associates the derived ResourceId to a System address, and sets access permissions. https://mud.dev/world/reference/world-external#registersystem (opens in a new tab)

ReadDemoSystem.t.sol:51
world.registerFunctionSelector(systemResource, "readMainBaseLevel()");

The System may be registered, but we also need to tell the World what functions are provided by the System. This handles that. The function text is a function signature, and needs to include all the input parameters, e.g. "transfer(address,uint256)". No spaces.