Using TestKit

Last updated on 16th July 2024

Creating a test and adding agents

Tests take place in a controlled environment, allowing you to isolate agent behaviour and verify expected functionality. All tests take place in a TestKit; In the same way, our models run in AgentBasedModel our tests run in a TestKit.

For the following section, we will be adding tests to the model created in the tutorial section. Don't worry if you have not compleated the tutorial; examples will be self-contained, for the interested reader you can find this tutorial here.

Given all our tests require a TestKit it would be handy if we did not have to keep setting up the TestKit for each test we wanted to run. JUnit allows you to annotate methods with @Before. Code annotated in this fashion will be executed before each test.

Creating your first TestKit (Java)

public class BankTest {

    private TestKit<MortgageModel.Globals> testKit;
    @Before
    public void init(){
         testKit = TestKit.create(MortgageModel.Globals.class);
    }
}

Notice that we have passed the user-defined globals object to TestKit.create(), which we discussed in the Global State. If our model uses the default GlobalState this argument can be omitted.

To carry out meaningful tests we have to add agents to the TestKit. We can add agents to a TestKit in a similar manner to adding agents to an AgentBasedModel. Agents added to the same Testkit do not have knowledge of each other; however, you may want to add multiple agents to a single TestKit to test different cases of the same functionality on different agents in one test.

Adding agents (Java)

@Test
public void creatingAgent(){
  Bank bank1 = testKit.addAgent(Bank.class);       
  Bank bank2 = testKit.addAgent(Bank.class);
}

Of course, this is not much of a test, we have merely added an agent to our TestKit. Improving upon this, we can also create agents with data injectors. Because we are in a test environment, we can then assert that agents are created as expected.

Adding agents with data injector (Java)

@Test    
public void agentShouldBeInitilisedWithInjectedArgs(){
  Bank bank = testKit.addAgent(Bank.class, b -> {
    b.debt = 9;
  });
  assertEquals(bank.debt, 9);
}

Making variables visible for testing

If you have issues regarding variables and methods being visible in your unit tests, check your test packaging mirrors your code packages.

Testing simple actions

As mentioned, we test actions in isolation. After creating a TestKit and adding agents, we are ready to test. There are three ways to test an action has executed as expected. One is to examine how the action has mutated an agent. Another involves inspecting the return type (TestResult) of our testAction method. Finally, TestKit can hold accumulators which we can mutate; these need to be initialized for testing actions that mutate or use the value of accumulators.

Testing agents mutations

Suppose we have a Household agent that has an age associated with it and an Action that increments its age by one.

Simple 'birthday' action (Java)

public static Action<Household> birthday =
      Action.create(Household.class, h -> h.age += 1);

Simple 'birthday' test (Java)

@Test
public void birthdayShouldIncrementAgeByOne() {
  Household household = testKit.addAgent(Household.class, h -> h.age = 49);

  testKit.testAction(household, Household.birthday);

  assertEquals(household.age, 50);
}

Inspecting the 'result' of an action.

As well as being able to test for mutations on agents we can also use the returned TestResult object to check the following:

  • If an agent has been stopped.
  • How actions affect accumulators.
  • Agents that were spawned.
  • Messages that may have been sent in an action.

We will address the first two of these now and the final point after discussing how messaging works in the test environment.

Testing that an agent has been stopped

Suppose the complexity of our birthday Action is extended slightly, to stop an agent if it reaches a certain age.

Simple 'birthday' action with 'death' (Java)

public static Action<Household> birthday =
      Action.create(
          Household.class,
          h -> {
            if (h.age >= 99) {
              h.stop();
            } else {
              h.age += 1;
            }
          });

We can use the result of testAction to check if the agent has been stopped. Notice that each action has a separate testResult, this highlights the isolated nature of actions in the TestKit and makes it easy to reason about them.

Simple 'birthday' test with 'death' (Java)

@Test
public void oldAgentsShouldStop() {
  Household household1 = testKit.addAgent(Household.class, h -> h.age = 49);
  Household household2 = testKit.addAgent(Household.class, h -> h.age = 99);

  TestResult testResult1 = testKit.testAction(household1, Household.birthday);
  TestResult testResult2 = testKit.testAction(household2, Household.birthday);

  assertFalse(testResult1.wasStopped());
  assertTrue(testResult2.wasStopped());
}

A broken clock is right twice a day

It is useful to test all the possible outcomes of an action to make sure you do not get any false positive results.

Testing the use of accumulators in actions

We can also create long accumulators in a test kit. These will need to be created to test actions that use the value of an existing accumulator, or action that add to accumulators. For example suppose we wanted to keep track of how many agents were dying in the model, and we were using an accumulator to gather this information from our model. We could test that our action is actually updating the accumulator.

// Action that updates accumulator on death
public static Action<Household> birthday =
      Action.create(
          Household.class,
          h -> {
            if (h.age >= 99) {
              h.getLongAccumulator("deaths").add(1);
              h.stop();
            } else {
              h.age += 1;
            }
          });

@Test
public void deadAgentShouldUpdateAccumulator() {
  Household household1 = testKit.addAgent(Household.class, h -> h.age = 100);

  long initialValue = 0;
  testKit.createLongAccumulator("deaths", initialValue);

  TestResult result1 = testKit.testAction(household1, Household.birthday);

  assertEquals(initialValue + 1, result1.getLongAccumulator("deaths").value());
}

It is important to note that updating an accumulator from within a tested action does not update the testKit accumulator, instead it returns a new updated accumulator with the TestResult that can be tested. This is so that multiple tests can be run using the same testKit without affecting the results of each other.

It is also possible to get the value of an accumulator

Initialising accumulators

Long accumulators have a count associated with them used for calculating the mean. We can initialise accumulators with an initial value and initial count; this might be necessary to initialize when the action being tested uses the current count/mean of the accumulator.

Testing agent spawning

Suppose our households gave birth to new households based on some predicate, and adds a link from the new household back to the parent household.

public static Action<Household> birthday =
      Action.create(
          Household.class,
          h -> {
            if (h.age == 35) {
              h.spawn(Household.class, newH -> newH.addLink(h.getID(), ParentLink.class))
            }
          });

We can use the result of testAction to check if the agent has been spawned, and get details about these spawned agents.

@Test
public void agentsShouldCreateNewAgents() {
  Household household1 = testKit.addAgent(Household.class, h -> h.age = 35);
  Household household2 = testKit.addAgent(Household.class, h -> h.age = 99);

  TestResult testResult1 = testKit.testAction(household1, Household.birthday);
  TestResult testResult2 = testKit.testAction(household2, Household.birthday);

  assertTrue(testResult1.getSpawnedAgentsOfType(Household.class).size(), 1);
  assertTrue(testResult1.getSpawnedAgentsOfType(Household.class).size(), 0);

  Household newHousehold = testResult1.getSpawnedAgentsOfType(Household.class).get(0);
  assertTrue(newHousehold.getLinksTo(household1.getID()).size(), 1)
}

Sending and receiving messages

Sending messages before executing an action

We have already mentioned how agents are tested in isolation. Given an integral component of the Simudyne architecture is message passing you may wonder how the test kit handles this. To test how actions process messages we must send agents messages before running an action. This is in contrast to our AgentBasedModel where message passing is between agents. This allows us to construct the exact message we wish to test on an agent.

Sending a message to an agent (Java)

@Test
public void sendPayementMessagesTest(){
  Bank bank = testKit.addAgent(Bank.class);
  testKit.send(Messages.Payment.class, payment -> {
    payment.repayment=9;
    payment.amount=11;
  }).to(bank.getID());
}

We can think of this as putting a message directly into the inbox of the agent. On executing an action our bank agent will have this message to process. Let's take a look at a simple example where our bank processes payments by merely incrementing assets by the payment amount. For completeness, the Action is given below.

Processing a message in an action (Java)

public static Action<Bank> simplePaymentIncrement =
      new Action<>(
          Bank.class,
          bank ->
              bank.getMessagesOfType(Messages.Payment.class)
                  .forEach(payment -> bank.assets += payment.amount));

We can use the test kit to test this action is processing in the expected way.

Testing actions (Java)

@Test
public void sendPaymentMessagesTest() {
 Bank bank =
     testKit.addAgent(
         Bank.class,
         b -> {
           b.assets = 90;
         });

 testKit
     .send(
         Messages.Payment.class,
         payment -> {
           payment.repayment = 10;
           payment.amount = 10;
         })
     .to(bank.getID());

 testKit.testAction(bank, Bank.simplePaymentIncremement);

 assertEquals(bank.assets, 100);
}

If you find yourself sending multiple messages to the same agent, you can distinguish between senders by including the id of the sender. This is done by passing an extra parameter to the send() method; we will look at an example of this in the next section.

Intercepting messages sent in Actions

Processing received messages are only half the story. Agents must also be able to send messages, which we will want to test. Messages sent in an agents action are captured by the testResult object.

For illustrative purposes, let's create an Action in the bank that sends an ApplicationSuccessful message to an agent with an id of one.

Simple action that sends a message to a fixed id (Java)

public static Action<Bank> sendApplicationSuccessfulMessage =
      new Action<>(Bank.class, bank -> bank.send(Messages.ApplicationSuccessful.class).to(1L));

We can use this action in testKit and test two things. Firstly, that a message is being sent, and secondly, that it is being sent to the correct agent.

Testing messages sent in an action (Java)

@Test
 public void captureSentMessage() {
   Bank bank = testKit.addAgent(Bank.class);
   TestResult result = testKit.testAction(bank, Bank.sendApplicationSuccessfulMessage);
   List<Messages.ApplicationSuccessful> messages =
       result.getMessagesOfType(Messages.ApplicationSuccessful.class);
   assertEquals(1, messages.size());
   assertEquals(1, messages.get(0).getTo());
 }