Skip to content

CDCF Test Instruction

Yuecheng edited this page Aug 14, 2020 · 6 revisions

CDCF Test Instruction

CDCF Test is based on caf test framework, and key concepts are shown below:

  • Checks represent single boolean expressions.
  • Tests contain one or more checks.
  • Fixture equip tests with a fixed data environment.
  • Suite group tests together.

Demo1 Basic Structure

// Define Suite, all Tests below belons to math Suite
#define CAF_SUITE math

#include "caf/test/dsl.hpp"

// shared data field
namespace { 
	struct fixture {}; 
} // namespace 

// explicitly associate test suite with fixture data(in this case, the fixture has no actual data)
CAF_TEST_FIXTURE_SCOPE(math_tests, fixture)

// 
CAF_TEST(divide) {
CAF_CHECK(1 / 1 == 0); // Faild, CAF_CHECK faild will not stop the test
CAF_CHECK(2 / 2 == 1); // Pass
CAF_REQUIRE(3 + 3 == 5); // REQUIRE failed will stopp the test
CAF_CHECK(4 - 4 == 0); // Can not reach CHECK because the last REQUIRE failed
}

CAF_TEST_FIXTURE_SCOPE_END()

Demo2 Test One Actor

namespace {
	struct fixture { 
    caf::actor_system_config cfg; 
    caf::actor_system sys; 
    caf::scoped_actor self;
		fixture() : sys(cfg), self(sys) {}
  };
  
  caf::behavior adder() { 
    return {
      [=](int x, int y){ 
        return x + y;
      } 
    };
  }
} // namespace 

CAF_TEST_FIXTURE_SCOPE(actor_tests, fixture)

CAF_TEST(simple actor test) {
	// spawn an actor under test
	auto aut = self->spawn(adder); 
  self->request(aut, caf::infinite, 3, 4).receive(
    [=](int r) {
      // Check return value
      CAF_CHECK(r == 7);
    },
    [&](caf::error& err) {
    // Once receive error message, CAF_FALL will print error message and stop test
    CAF_FAIL(sys.render(err)); 
    });
} 

CAF_TEST_FIXTURE_SCOPE_END()

Demo3 Test Multiple Actors

CAF provides a scheduler implementation specifically tailored for writing unit tests called test_coordinator. It does not start any threads and instead gives unit tests full control over message dispatching and timeout management. And test_coordinator_fixture provides a predefined fixture with actor system(sys), testing scheduler(sched) to reduce boilerplate code.

Using this fixture unlocks three additional macros:

  • expect checks for a single message. The macro verifies the content types of the message and invokes the necessary member functions on the test coordinator. Optionally, the macro checks the receiver of the message and its content. If the expected message does not exist, the test aborts.
  • allow is similar to expect, but it does not abort the test if the expected message is missing. This macro returns true if the allowed message was delivered, false otherwise.
  • disallow aborts the test if a particular message was delivered to an actor.
namespace {
behavior ping(event_based_actor* self, actor pong_actor, int n) {
  // send the first message to trigger the rest process
  self->send(pong_actor, ping_atom_v, n);
  return {
    [=](pong_atom, int x) {
      if (x > 1)
        self->send(pong_actor, ping_atom_v, x - 1);
    },
  };
}

behavior pong() {
  return {
    [=](ping_atom, int x) { return make_result(pong_atom_v, x); },
  };
}

// test_coordinator_fixture
struct ping_pong_fixture : test_coordinator_fixture<> {
  actor pong_actor;

  ping_pong_fixture() {
    // spawn Pong actor.
    pong_actor = sys.spawn(pong);
    // In this project, caf version is 0.17.3 which the run function actually do nothing. 
    // CAF test framework still has some issues, you can ignore run.
    run();
  }
};

} // namespace

CAF_TEST_FIXTURE_SCOPE(ping_pong_tests, ping_pong_fixture)

CAF_TEST(three pings) {
  // spwan ping actor.
  auto ping_actor = sys.spawn(ping, pong_actor, 3);
  // 	run shced
  sched.run_once();
  // check ping pong actor message passing.
  expect((ping_atom, int), from(ping_actor).to(pong_actor).with(_, 3));
  expect((pong_atom, int), from(pong_actor).to(ping_actor).with(_, 3));
  expect((ping_atom, int), from(ping_actor).to(pong_actor).with(_, 2));
  expect((pong_atom, int), from(pong_actor).to(ping_actor).with(_, 2));
  expect((ping_atom, int), from(ping_actor).to(pong_actor).with(_, 1));
  expect((pong_atom, int), from(pong_actor).to(ping_actor).with(_, 1));
  // No further messages allowed.
  disallow((ping_atom, int), from(ping_actor).to(pong_actor).with(_, 1));
}

CAF_TEST_FIXTURE_SCOPE_END()

Demo4 Integrate to Yanghui Triangle - Worker Actor Test

Here Yanghui Triangle refers to a classic dynamic programming problem: Leetcode 120. trangle , so don't be confused when seeing the triangle data which is not the same as traditional yanghui triangle.

cdcf / demos / yanghui_cluster / worker_test.cc

// test yanghui triangle worker actor whose ability is to do add and compare calculation 
using namespace caf;

namespace {
  struct fixture {
    caf::actor_system_config cfg;
    caf::actor_system sys;
    caf::scoped_actor self;
    fixture() : sys(cfg), self(sys) {
    }
  };
} // namespace

CAF_TEST_FIXTURE_SCOPE(actor_tests, fixture)

CAF_TEST(simple actor test) {
  // spawn yanghui worker actor under test
  auto actor = sys.spawn<typed_calculator>();

  // test add behavior
  self->request(actor, caf::infinite, 3, 4).receive(
      [=](int r) {
        CAF_CHECK(r == 7);
      },
      [&](caf::error& err) {
        CAF_FAIL(sys.render(err));
      });

  std::vector<int> numbers = {0,1,2,3,4,5,6,7,8,9};
  NumberCompareData send_data;
  send_data.numbers = numbers;

  // test compare behavior, check if the return value is the minimums of input data
  self->request(actor, caf::infinite, send_data).receive(
      [=](int r) {
        CAF_CHECK(r == 0);
      },
      [&](caf::error& err) {
        CAF_FAIL(sys.render(err));
      });
}

CAF_TEST_FIXTURE_SCOPE_END()

Demo5 Integrate to Yanghui Triangle - Yanghui Actor Test

cdcf / demos / yanghui_cluster / yanghui_actor_test.cc

namespace {

struct fixture {
  caf::actor_system_config cfg;
  caf::actor_system sys;
  caf::scoped_actor self;
  fixture() : sys(cfg), self(sys) {
    // nop
  }
};
}  // namespace

std::vector<std::vector<int>> kYanghuiTestData = {
    {5},
    {7, 8},
    {2, 1, 4},
    {4, 2, 6, 1},
    {2, 7, 3, 4, 5},
    {2, 3, 7, 6, 8, 3},
    {2, 3, 4, 4, 2, 7, 7},
    {8, 4, 4, 3, 4, 5, 6, 1},
    {1, 2, 8, 5, 6, 2, 9, 1, 1},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 1},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9, 9},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9, 9, 1},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9, 9, 1, 6},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9, 9, 1, 6, 8},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9, 9, 1, 6, 8, 7},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9, 9, 1, 6, 8, 7, 7},
    {1, 2, 8, 5, 6, 2, 9, 1, 1, 8, 9, 9, 1, 6, 8, 7, 7, 6}};

CAF_TEST_FIXTURE_SCOPE(yanghui_tests, fixture)

CAF_TEST(divide) {
  CounterInterface* counter = new FakeCounter();
  // yanghui actor main function is to dispatch tasks, it will call a interface to do the real calculation
  // which in this case, is FakeCounter.
  auto yanghui_actor = sys.spawn(yanghui, counter);
  
  self->request(yanghui_actor, caf::infinite, kYanghuiTestData)
      .receive([=](int r) { CAF_CHECK(r == 42); },
               [&](caf::error& err) { CAF_FAIL(sys.render(err)); });
}

CAF_TEST_FIXTURE_SCOPE_END()

cdcf / demos / yanghui_cluster / fake_counter.h

// FakeCounter 
class FakeCounter : public CounterInterface{
 public:
  int AddNumber(int a, int b, int& result) override;
  int Compare(std::vector<int> numbers, int& min) override;
};

cdcf / demos / yanghui_cluster / fake_counter.cc

int FakeCounter::AddNumber(int a, int b, int& result) {
  result = a + b;
  return 0;
}

int FakeCounter::Compare(std::vector<int> numbers, int& min) {
  min = INT_MAX;
  for (auto one_number : numbers){
    if (min > one_number){
      min = one_number;
    }
  }
  return 0;
}

Demo6 Integrate to Yanghui Triangle - Multiple Actor Test

Because of some limitations of caf test framework, this demo can not integrate to yanghui demo. Instead, we create a simple yanghui demo which can be used in test framework. Simple yanghui code can be found in yanghui_simple_actor.h, simple_counter.h. Here we only discuss test code.

cdcf / demos / yanghui_cluster / yanghui_test.cc

namespace {

caf::behavior test_init_actor(caf::event_based_actor* self) { return {}; };

caf::behavior test_end_actor(caf::event_based_actor* self) {
  return {[](int result) {
    std::cout << "test complete, get result:" << result << std::endl;
  }};
};

/*
*  note: in complex scenario, test_coordinator_fixture<> need a config parameter(yanghui_config.h),
*  config defined customized actor type: add_actor_type("calculator", calculator_fun),if you did not import config,
*  it will use default config, in some complex scenario, test can not spawn the right object, thus test will stoped 
*  by timeout because can not receive message.
*/
struct yanghui_fixture : test_coordinator_fixture<config> {
  actor counter_;
  actor yanghui_count_path_;
  actor yanghui_get_min_;
  actor test_end_actor_;

  yanghui_fixture() {
    auto worker = sys.spawn(simple_counter);
    test_end_actor_ = sys.spawn(test_end_actor);
    std::cout << "spawn test_end_actor_:" << test_end_actor_.id() << std::endl;
    counter_ = caf::actor_cast<caf::actor>(worker);
    std::cout << "spawn worker_:" << counter_.id() << std::endl;
    yanghui_get_min_ =
        sys.spawn(yanghui_get_final_result, counter_, test_end_actor_);
    std::cout << "spawn yanghui_get_min_:" << yanghui_get_min_.id()
              << std::endl;
    yanghui_count_path_ =
        sys.spawn(yanghui_count_path, counter_, yanghui_get_min_);
    std::cout << "spawn yanghui_count_path_:" << yanghui_count_path_.id()
              << std::endl;

    /* 
    *  run() is discussd in demo 3
    */ 
    //run();
  }
};

}  // namespace

std::vector<std::vector<int>> kYanghuiTestData = {{5}, {7, 8}, {2, 1, 4}};

CAF_TEST_FIXTURE_SCOPE(yanghui_trangle, yanghui_fixture)

CAF_TEST(yanghui interact) {
  // The test needs an entrance to trigger the yanghui task, to spawn an actor that sends the yanghui task
  auto test_init = sys.spawn(test_init_actor);
  auto sender = caf::actor_cast<caf::event_based_actor*>(test_init);
  sender->send(yanghui_count_path_, kYanghuiTestData);
  
  /*
  * Be sure that this code should be written after the trigger request, because in fact, when the request
  * is sent, the entire task has already started and finished,  sched.run_once() is to trace history log 
  * to obtain the request process.
  */
  sched.run_once();

  /*
  * The test code, the expect order here, must be written in strict accordance with the order of the 
  * request. It cannot be misplaced or missed any intermediate messages, otherwise the test the 
  * matching failure will be reported. For example, the message sequence for the entire cluster to 
  * complete a task is A B C D E, and the order of expect can be A B C D E, it is also possible to 
  * measure only the first few messages such as A B C, but not A D E or A C B D E.
  * One exception is:
  *          Request A1 >>
  *        /
  *   root                        <<  Response a1
  *        \ Request A2 >>                        \																			
  *                                                 worker																 
  *                                               /
  *		                  <<  Response a2
  *
  * Root sends A2 immediately after sending A1. and root doesn’t care whether Response a1 returns 
  * after sending A1 immediately or not. In this case, expect order can be expect A1 a1 A2 a2 or 
  * A1 A2 a1 a2 but can not be A1 A2 a2 a1 etc.
  */
  expect((std::vector<std::vector<int>>),
         from(test_init).to(yanghui_count_path_).with(kYanghuiTestData));
  expect((int), from(yanghui_count_path_).to(yanghui_count_path_).with(1));
  expect((int, int, int), from(yanghui_count_path_).to(counter_).with(5, 7, 0));
  expect((int, int), from(counter_).to(yanghui_count_path_).with(12, 0));
  expect((int, int, int), from(yanghui_count_path_).to(counter_).with(5, 8, 1));
  expect((int, int), from(counter_).to(yanghui_count_path_).with(13, 1));
  expect((int), from(yanghui_count_path_).to(yanghui_count_path_).with(2));
  expect((int, int, int),
         from(yanghui_count_path_).to(counter_).with(12, 2, 0));
  expect((int, int), from(counter_).to(yanghui_count_path_).with(14, 0));
  expect((int, int, int, int),
         from(yanghui_count_path_).to(counter_).with(12, 13, 1, 1));
  expect((int, int), from(counter_).to(yanghui_count_path_).with(13, 1));
  expect((int, int, int),
         from(yanghui_count_path_).to(counter_).with(13, 4, 2));
  expect((int, int), from(counter_).to(yanghui_count_path_).with(17, 2));
  expect((int), from(yanghui_count_path_).to(yanghui_count_path_).with(3));
  expect((std::vector<int>), from(yanghui_count_path_)
                                 .to(yanghui_get_min_)
                                 .with(std::vector<int>{14, 13, 17}));
  /*
  *  note: expect( ... with( )) uses == to check equals so when use custom type, you need overload ==
  */
  NumberCompareData number_compare;
  number_compare.numbers = {14, 13, 17};
  number_compare.index = 0;
  expect((NumberCompareData),
         from(yanghui_get_min_).to(counter_).with(number_compare));
  expect((int, int), from(counter_).to(yanghui_get_min_).with(13, 0));
  expect(
      (std::vector<int>),
      from(yanghui_get_min_).to(yanghui_get_min_).with(std::vector<int>{13}));

  // Final results
  expect((int), from(yanghui_get_min_).to(test_end_actor_).with(13));
}

CAF_TEST_FIXTURE_SCOPE_END()

Common test failure reports

REQUIRED: sched_.prioritize(whom) An error occurred in from( sender ) to( receiver ), and no message matching such a sender could be found. It is recommended to check whether there is any missing intermediate message, or whether the sender writes correctly.

Message does not match expected pattern parameter mode does not match

No prompt, display the test result directly. E.g. checks: 112 (111/1) One of the test failed. This error is annoying, because it does not tell you which particular Check failed. It is recommended to manually comment out part of the Checks, run it, and locate it again.

Clone this wiki locally