Testing

To allow easy testing of the generated code, Coco provides support for automatically generating GMock-compatible mock implementations of external components. These allow you to easily write tests that check that the correct function calls and signals are being produced. For example, the mock generator generates methods that make it simple to:

  • Create expectations that functions are called, or signals are received;
  • Send signals as GMock actions;
  • Indicate that the mock state machine has terminated.

Mock Generation

To enable generation of mocks, either set generator.cpp.generateMocks, or set the coco --mocks option. This will cause the code generator to emit mocks in separate files: for each generated file A.`h` or ``A.cc, the code generator will additionally generate two extra files AMock.h and AMock.cc. The mock files contain only mock-related code, and should only be compiled into testing libraries, rather than release libraries.

For each external component E, the generated mock files will contain a C++ class called EMock that subclasses E and implements all of its virtual methods, as described in External Components, by using the GMock mocking macros. Further, for each generated function of the form send_x defined in the C++ generated code for E, the generated code will contain a function called send_x_action that creates a GMock action that, when executed, will send the corresponding signal.

For example, consider the following port and external component:

port PPinger {
  function ping(x : BoundedInt<0, 5>) : Bool
  function terminate() : Nil
  outgoing signal pong(x : BoundedInt<0, 5>)
  machine M { ... }
}

The generated mock code will contain:

class PingerMock : public Pinger {
 public:
  testing::PolymorphicAction<SendMachineTerminatedAction> coco_shutdown_action() { ... }
  testing::PolymorphicAction<client_pongAction> send_client_pong_action(int x) { ... }

  MOCK_METHOD(bool, client_ping, (int), (override));
  MOCK_METHOD(void, client_terminate, (), (override));
  MOCK_METHOD(void, execute_start, (), (override));
};

This can be utilised as follows, assuming pinger_mock is an instance of PingerMock:

EXPECT_CALL(pinger_mock, client_ping(2))
    .WillOnce(DoAll(pinger_mock.send_client_pong_action(2), Return(false)));

The above would create an expectation that:

  • ping is called exactly once, passing the value 2;
  • When ping is called, will send the pong(2) signal to all subscribers of pinger_mock.
  • Will return false to its caller.

The generated mocks can also be used to create expectations on signals being received. For example, suppose we have the following external component defined:

external component Client {
  pinger : Required<PPinger>;
}

This will result in the following generated code:

class ClientMock : public Client {
 public:
  testing::PolymorphicAction<SendMachineTerminatedAction> coco_shutdown_action() { ... }

  MOCK_METHOD(void, pinger_pong, (int), (override));
  MOCK_METHOD(void, execute_start, (), (override));
};

This can be used as follows, assuming client_mock is an instance of ClientMock:

EXPECT_CALL(client_mock, pinger_pong(2)).WillOnce(client_mock.coco_shutdown_action());

This creates an expectation that:

  • The pong signal is received exactly once with the value 2;
  • When the pong signal is received, client_mock will terminate.

Full Example

The following Coco encodes a simple echo component that raises a signal with the same value as was passed to the function:

port PPinger {
  function ping(x : BoundedInt<0, 5>) : Bool
  function terminate() : Nil
  outgoing signal pong(x : BoundedInt<0, 5>)

  machine M {
    terminate() = setNextState(Terminated)
    ping(x : BoundedInt<0, 5>) = {
      pong(x);
      return x == 0;
    }
  }
}

component Echo {
  client : Provided<PPinger>;
  pinger : Required<PPinger>;
  machine M {
    client.ping(x : BoundedInt<0, 5>) = pinger.ping(x)
    client.terminate() = {
      pinger.terminate();
      setNextState(Terminated);
    }
    pinger.pong(x : BoundedInt<0, 5>) = client.pong(x)
  }
}

external component Client {
  pinger : Required<PPinger>;
}

external component Pinger {
  client : Provided<PPinger>;
}

component System {
  client : Client;
  echo : Echo;
  pinger : Pinger;
  init() = {
    connect(client.pinger, echo.client);
    connect(echo.pinger, pinger.client);
  }
}

The following code creates a test that checks that if a single ping call is made, then Client will receive a single signal back.

#include "gtest/gtest.h"
#include "coco/stream_logger.h"

#include "DocExampleMock_c++11.h"

TEST(DocExample, Echo) {
  ClientMock client_mock;
  PingerMock pinger_mock;

  System::Arguments arguments;
  arguments.client = &client_mock;
  arguments.pinger = &pinger_mock;

  System system(arguments);
  system.set_name("root");
  system.set_logger_recursively(coco::StreamMachineLogger::cout());
  system.coco_start();

  const int value = 2;

  EXPECT_CALL(pinger_mock, client_terminate()).WillOnce(pinger_mock.coco_shutdown_action());
  EXPECT_CALL(pinger_mock, client_ping(value))
      .WillOnce(DoAll(pinger_mock.send_client_pong_action(value), Return(value == 0)));
  EXPECT_CALL(client_mock, pinger_pong(value)).WillOnce(client_mock.coco_shutdown_action());

  client_mock.pinger().connected().ping(value);
  client_mock.pinger().connected().terminate();
}