В результате первой попытки выкуривания бамбука на тему того, как можно писать unit-тесты для агентов, появился нижеследующий пример кода. Который, как мне думается, удовлетворяет двум важным критериям. Во-первых, он выглядит реализуемым. И, во-вторых, он позволяет достаточно декларативно записывать тесты для отдельных агентов.
Кому интересно посмотреть и пообсуждать -- милости прошу под кат.
В качестве поляны для экспериментов был взят штатный пример dining_philosophers (знаменитые обедающие философы, в одном из вариантов реализации).
И даже не весь пример, а один простенький класс оттуда: fork_t, который реализует понятие "вилка", т.е. ресурс, за который борются философы. Выглядит этот класс в слегка модифицированном варианте так:
struct msg_take { const so_5::mbox_t m_who; }; struct msg_busy : public so_5::signal_t {}; struct msg_taken : public so_5::signal_t {}; struct msg_put : public so_5::signal_t {}; class fork_t final : public so_5::agent_t { public : fork_t( context_t ctx ) : so_5::agent_t( ctx ) { this >>= st_free; st_free.event( [this]( const msg_take & evt ) { this >>= st_taken; so_5::send< msg_taken >( evt.m_who ); } ); st_taken.event( []( const msg_take & evt ) { so_5::send< msg_busy >( evt.m_who ); } ) .just_switch_to< msg_put >( st_free ); } private : state_t st_free{ this, "free" }; state_t st_taken{ this, "taken" }; }; |
Т.е. простой агент с двумя состояниями free и taken. Когда агент в состоянии free, он реагирует на сообщение msg_take: переходит в taken и отсылает подтверждение тому, кто msg_take отослал.
Когда агент в taken, то он реагирует на два сообщения. Самое простое -- это реакция на msg_put. Агент просто переходит в free.
Но если в состоянии taken приходит еще одно сообщение msg_take, то агент должен отослать msg_busy отправителю msg_take.
Вот такая тривиальная логика. И давайте попробуем посмотреть, как ее можно проверить посредством unit-теста, написанного с использованием пока еще не существующих инструментов.
Выглядеть это может вот так:
int main() { namespace tests = so_5::extra::testing; tests::wrapped_env_t sobj; auto fork = make_agent<fork_t>(sobj); auto dummy_mbox = sobj.env().create_mbox(); tests::inspect(*fork, [&](auto pre) { pre.send<msg_take>(*fork, dummy_mbox); }, [&](auto post) { post.expect_state_by_name("taken"); post.expect_message<msg_taken>(dummy_mbox); }); tests::inspect(*fork, [&](auto pre) { pre.send<msg_take>(*fork, dummy_mbox); }, [&](auto post) { post.expect_state_by_name("taken"); post.expect_message<msg_busy>(dummy_mbox); }); tests::inspect(*fork, [&](auto pre) { pre.send<msg_put>(*fork); }, [&](auto post) { post.expect_state_by_name("free"); post.expect_message<msg_put>(dummy_mbox); }); tests::inspect(*fork [&](auto pre) { pre.send<msg_put>(*fork); }, [&](auto post) { post.expect_nothing(); }); } |
Т.е. мы запускаем SObjectizer, затем создаем экземпляр агента fork и вспомогательный mbox, в который fork должен будет отсылать ответные сообщения. Ну и затем мы начинаем гонять тестовые случаи.
Вот, скажем:
tests::inspect(*fork, [&](auto pre) { pre.send<msg_take>(*fork, dummy_mbox); }, [&](auto post) { post.expect_state_by_name("taken"); post.expect_message<msg_taken>(dummy_mbox); }); |
Здесь мы говорим, что хотим проинспектировать поведение агента fork. Это указывается с помощью первого аргумента inspect().
Далее в вызов inspect передаются две лямбды. Первая лямбда выполняет воздействия на объект inspect. В нашем случае отсылается сообщение msg_take.
Во второй лямбде выполняются проверки того, как агент среагировал на наше воздействие. Мы ждем, что агент окажется в состоянии taken и отошлет сообщение msg_taken в наш вспомогательный mbox. Если эти проверки окажутся пройденными, то будет пройден и весь тестовый случай.
Остальные проверки похожи на первую, за исключением вот этой:
tests::inspect(*fork [&](auto pre) { pre.send<msg_put>(*fork); }, [&](auto post) { post.expect_nothing(); }); |
К этому времени у нас агент должен быть в состоянии free. В этом состоянии он должен просто проигнорировать очередное сообщение msg_put. Поэтому во второй лямбде мы указываем expect_nothing. Т.е. ничего не должно произойти, никакой реакции.
Вот такое пока самое первое приближение.
Не факт, что в вызове inspect первая лямбда вообще нужна. Скорее всего тестовое воздействие на агента всегда будет выражаться в отсылке единственного сообщения агенту или в какой-то mbox. Если это так, то можно будет писать что-то вроде:
tests::inspect(*fork, tests::impact<msg_take>(*fork, dummy_mbox), [&](auto post) { post.expect_state_by_name("taken"); post.expect_message<msg_taken>(dummy_mbox); }); |
Комментариев нет:
Отправить комментарий