Como escrever testes de unidade para atores? Abordagem SObjectizer

Os atores simplificam a programação multithread, evitando um estado mutável compartilhado e compartilhado. Cada ator possui seus próprios dados que não são visíveis para ninguém. Os atores interagem apenas através de mensagens assíncronas. Portanto, os horrores mais aterrorizantes do multithreading na forma de raças e impasses ao usar atores não são terríveis (embora os atores tenham seus problemas, mas isso não é sobre isso agora).

Em geral, escrever aplicativos multithread usando atores é fácil e agradável. Inclusive porque os próprios atores são escritos com facilidade e naturalidade. Você poderia até dizer que escrever o código do ator é a parte mais fácil do trabalho. Mas quando o ator é escrito, surge uma pergunta muito boa: "Como verificar a correção de seu trabalho?"

A questão é realmente muito boa. Nos perguntam regularmente quando falamos sobre atores em geral e sobre o SObjectizer em particular. E até recentemente, poderíamos responder a essa pergunta apenas em termos gerais.

Mas a versão 5.5.24 foi lançada , na qual havia suporte experimental para a possibilidade de teste de unidade dos atores. E neste artigo, tentaremos falar sobre o que é, como usá-lo e com o que foi implementado.

Como são os testes de ator?


Vamos considerar os novos recursos do SObjectizer em alguns exemplos, transmitindo o que é o quê. O código fonte dos exemplos discutidos pode ser encontrado neste repositório .

Ao longo da história, os termos "ator" e "agente" serão usados ​​de forma intercambiável. Eles designam a mesma coisa, mas no SObjectizer o termo "agente" é usado historicamente, portanto, mais "agente" será usado com mais frequência.

O exemplo mais simples com Pinger e Ponger


O exemplo dos atores Pinger e Ponger é provavelmente o exemplo mais comum para estruturas de atores. Pode-se dizer um clássico. Bem, se sim, então vamos começar com os clássicos.

Portanto, temos um agente Pinger que, no início de seu trabalho, envia uma mensagem Ping ao agente Ponger. E o agente Ponger envia de volta uma mensagem Pong. É assim que fica no código C ++:

// Types of signals to be used. struct ping final : so_5::signal_t {}; struct pong final : so_5::signal_t {}; // Pinger agent. class pinger_t final : public so_5::agent_t { so_5::mbox_t m_target; public : pinger_t( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { so_subscribe_self().event( [this](mhood_t<pong>) { so_deregister_agent_coop_normally(); } ); } void set_target( const so_5::mbox_t & to ) { m_target = to; } void so_evt_start() override { so_5::send< ping >( m_target ); } }; // Ponger agent. class ponger_t final : public so_5::agent_t { so_5::mbox_t m_target; public : ponger_t( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { so_subscribe_self().event( [this](mhood_t<ping>) { so_5::send< pong >( m_target ); } ); } void set_target( const so_5::mbox_t & to ) { m_target = to; } }; 

Nossa tarefa é escrever um teste que verifique se, ao registrar esses agentes no SObjectizer, o Ponger receberá uma mensagem Ping e o Pinger receberá uma mensagem Pong em resposta.

OK Escrevemos esse teste usando a estrutura doctest unit-test e obtemos:

 #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include <doctest/doctest.h> #include <ping_pong/agents.hpp> #include <so_5/experimental/testing.hpp> namespace tests = so_5::experimental::testing; TEST_CASE( "ping_pong" ) { tests::testing_env_t sobj; pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); }); sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>()); sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>()); sobj.scenario().run_for(std::chrono::milliseconds(100)); REQUIRE(tests::completed() == sobj.scenario().result()); } 

Parece ser fácil. Vamos ver o que acontece aqui.

Primeiro, baixamos descrições das ferramentas de suporte ao teste do agente:

 #include <so_5/experimental/testing.hpp> 

Todas essas ferramentas são descritas no namespace so_5 :: experimental :: testing, mas, para não repetir um nome tão longo, introduzimos um alias mais curto e mais conveniente:

 namespace tests = so_5::experimental::testing; 

A seguir, é apresentada uma descrição de um único caso de teste (e não precisamos mais aqui).

Dentro do caso de teste, existem vários pontos-chave.

Primeiramente, esta é a criação e o lançamento de um ambiente de teste especial para o SObjectizer:

 tests::testing_env_t sobj; 

Sem esse ambiente, a “execução de teste” para agentes não pode ser concluída, mas falaremos sobre isso um pouco mais tarde.

A classe testing_env_t é muito semelhante à classe wrap_env_t no SObjectizer. Da mesma maneira, o SObjectizer inicia no construtor e para no destruidor. Portanto, ao escrever testes, você não precisa pensar em iniciar e parar o SObjectizer.

Em seguida, precisamos criar e registrar agentes Pinger e Ponger. Nesse caso, precisamos usar esses agentes para determinar o chamado. "Cenário de teste". Portanto, armazenamos separadamente ponteiros para agentes:

 pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); }); 

E então começamos a trabalhar com o "cenário de teste".

Um caso de teste é uma peça que consiste em uma sequência direta de etapas que devem ser concluídas do começo ao fim. A frase "de uma sequência direta" significa que no SObjectizer-5.5.24 as etapas do script "funcionam" estritamente em sequência, sem ramificações ou loops.

Escrever um teste para agentes é a definição de um script de teste que precisa ser executado. I.e. todas as etapas do cenário de teste devem funcionar, desde a primeira até a última.

Portanto, em nosso caso de teste, definimos um cenário de duas etapas. A primeira etapa verifica se o agente Ponger receberá e processará a mensagem Ping:

 sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>()); 

A segunda etapa verifica se o agente Pinger recebe uma mensagem Pong:

 sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>()); 

Essas duas etapas são suficientes para o nosso caso de teste; portanto, após sua determinação, prosseguimos para a execução do script. Executamos o script e permitimos que ele funcione não mais do que 100ms:

 sobj.scenario().run_for(std::chrono::milliseconds(100)); 

Cem milissegundos deve ser mais do que suficiente para os dois agentes trocarem mensagens (mesmo se o teste for executado em uma máquina virtual muito lenta, como às vezes é o caso do Travis CI). Bem, se cometemos um erro ao escrever agentes ou descrevemos incorretamente um script de teste, aguardar a conclusão de um script incorreto por mais de 100 ms não faz sentido.

Portanto, depois de retornar de run_for (), nosso script pode ser concluído com êxito ou não. Portanto, simplesmente verificamos o resultado do script:

 REQUIRE(tests::completed() == sobj.scenario().result()); 

Se o script não foi concluído com êxito, isso levará à falha do nosso caso de teste.

Alguns esclarecimentos e acréscimos


Se executarmos esse código dentro de um SObjectizer normal:

 pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); }); 

então, muito provavelmente, os agentes Pinger e Ponger conseguiriam trocar mensagens e concluir seu trabalho antes de retornar do introdutor_coop (milagres de multithreading são tais). Mas dentro do ambiente de teste, criado graças a testing_env_t, isso não acontece, os agentes Pinger e Ponger esperam pacientemente até executarmos nosso script de teste. Como isso acontece?

O fato é que, dentro do ambiente de teste, os agentes parecem estar congelados. I.e. após o registro, eles estão presentes no SObjectizer, mas não podem processar nenhuma de suas mensagens. Portanto, mesmo que so_evt_start () não seja chamado para agentes antes da execução do script de teste.

Quando executamos o script de teste usando o método run_for (), o script de teste primeiro descongela todos os agentes congelados. E então o script começa a receber notificações do SObjectizer sobre o que acontece com os agentes. Por exemplo, que o agente Ponger recebeu a mensagem Ping e que o agente Ponger processou a mensagem, mas não a rejeitou.

Quando essas notificações começam a chegar ao script de teste, o script tenta "experimentá-las" na primeira etapa. Portanto, temos uma notificação de que o Ponger recebeu e processou o Ping - é interessante para nós ou não? Acontece que é interessante, porque a descrição da etapa diz exatamente isso: funciona quando Ponger reage ao Ping. O que vemos no código:

 .when(*ponger & tests::reacts_to<ping>()) 

OK Portanto, o primeiro passo funcionou, vá para o próximo passo.

Em seguida, vem uma notificação de que o agente Pinger reagiu a Pong. E é exatamente isso que você precisa para o segundo passo para o trabalho:

 .when(*pinger & tests::reacts_to<pong>()) 

OK Então, o segundo passo funcionou, temos mais alguma coisa? Não. Isso significa que todo o script de teste foi concluído e você pode retornar o controle de run_for ().

Aqui, em princípio, como o script de teste funciona. De fato, tudo é um pouco mais complicado, mas abordaremos aspectos mais complexos quando considerarmos um exemplo mais complexo.

Exemplo de Filósofos


Exemplos mais complexos de agentes de teste podem ser vistos na solução da tarefa bem conhecida “Comer filósofos. Sobre os atores, esse problema pode ser resolvido de várias maneiras. A seguir, consideraremos a solução mais trivial: atores e filósofos são representados na forma de atores, pelos quais os filósofos precisam lutar. Cada filósofo pensa por um tempo, depois tenta pegar o garfo à esquerda. Se isso der certo, ele tenta pegar o garfo à direita. Se isso der certo, o filósofo come por algum tempo, após o que abaixa os garfos e começa a pensar. Se não foi possível colocar o plugue à direita (ou seja, foi tirado por outro filósofo), o filósofo devolve o plugue à esquerda e pensa por mais algum tempo. I.e. essa não é uma boa solução no sentido de que algum filósofo pode passar fome por muito tempo. Mas então é muito simples. E tem o escopo de demonstrar a capacidade de testar agentes.

Os códigos-fonte com a implementação dos agentes Fork e Philosopher podem ser encontrados aqui , no artigo não os consideraremos para economizar espaço.

Teste para garfo


O primeiro teste para agentes do Dining Philosophers será para o agente Fork.

Este agente funciona de acordo com um esquema simples. Ele tem dois estados: Livre e Tomado. Quando o agente está no estado Livre, ele responde a uma mensagem Receber. Nesse caso, o agente entra no estado de Tomada e responde com uma mensagem de resposta Tomada.

Quando o agente está no estado Tomado, ele responde à mensagem Receber de maneira diferente: o estado do agente não muda e Ocupado é enviado como uma mensagem de resposta. Também no estado Tomado, o agente responde à mensagem Colocar: o agente retorna ao estado Livre.

No estado Livre, a mensagem Colocar é ignorada.

Vamos tentar testar este por meio do seguinte caso de teste:

 TEST_CASE( "fork" ) { class pseudo_philosopher_t final : public so_5::agent_t { public: pseudo_philosopher_t(context_t ctx) : so_5::agent_t{std::move(ctx)} { so_subscribe_self() .event([](mhood_t<msg_taken>) {}) .event([](mhood_t<msg_busy>) {}); } }; tests::testing_env_t sobj; so_5::agent_t * fork{}; so_5::agent_t * philosopher{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { fork = coop.make_agent<fork_t>(); philosopher = coop.make_agent<pseudo_philosopher_t>(); }); sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>()); sobj.scenario().define_step("take_when_free") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>() & tests::store_state_name("fork"), *philosopher & tests::reacts_to<msg_taken>()); sobj.scenario().define_step("take_when_taken") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>(), *philosopher & tests::reacts_to<msg_busy>()); sobj.scenario().define_step("put_when_taken") .impact<msg_put>(*fork) .when( *fork & tests::reacts_to<msg_put>() & tests::store_state_name("fork")); sobj.scenario().run_for(std::chrono::milliseconds(100)); REQUIRE(tests::completed() == sobj.scenario().result()); REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork")); REQUIRE("free" == sobj.scenario().stored_state_name("put_when_taken", "fork")); } 

Há muito código, então vamos lidar com ele em partes, pulando os fragmentos que já devem estar claros.

A primeira coisa que precisamos aqui é substituir o verdadeiro agente filósofo. Um agente do Fork deve receber mensagens de alguém e responder a alguém. Mas não podemos usar o filósofo real neste caso de teste, porque o agente filósofo real tem sua própria lógica de comportamento, ele envia mensagens ele mesmo e essa independência interfere aqui.

Portanto, zombamos , ou seja, em vez do verdadeiro filósofo, apresentaremos um substituto para ele: um agente vazio que não envia nada, mas recebe apenas mensagens enviadas, sem nenhum processamento útil. Este é o pseudo-filósofo implementado no código:

 class pseudo_philosopher_t final : public so_5::agent_t { public: pseudo_philosopher_t(context_t ctx) : so_5::agent_t{std::move(ctx)} { so_subscribe_self() .event([](mhood_t<msg_taken>) {}) .event([](mhood_t<msg_busy>) {}); } }; 

Em seguida, criamos uma colaboração do agente Fork e do agente PseudoPhiloospher e começamos a determinar o conteúdo do nosso caso de teste.

A primeira etapa do script é verificar se o Fork, estando no estado Livre (e este é seu estado inicial), não responde à mensagem Put. Veja como esta verificação é escrita:

 sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>()); 

A primeira coisa que chama a atenção é a construção do impacto.

Ela é necessária porque nosso agente Fork não faz nada, ele apenas reage às mensagens recebidas. Portanto, alguém deve enviar uma mensagem ao agente. Mas quem?

Mas a própria etapa do script envia impacto. De fato, o impacto é um análogo da função de envio usual (e o formato é o mesmo).

Bem, a própria etapa do script enviará uma mensagem através do impacto. Mas quando ele fará isso?

E ele fará isso quando chegar a sua vez. I.e. se a etapa no script for a primeira, o impacto será executado imediatamente após a inserção de run_for. Se a etapa no script não for a primeira, o impacto será executado assim que a etapa anterior tiver funcionado e o script prosseguirá para processar a próxima etapa.

A segunda coisa que precisamos discutir aqui é chamada ignora. Essa função auxiliar diz que a etapa é acionada quando o agente falha ao processar a mensagem. I.e. nesse caso, o agente Fork deve se recusar a processar a mensagem Put.

Vamos considerar mais uma etapa do cenário de teste com mais detalhes:

 sobj.scenario().define_step("take_when_free") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>() & tests::store_state_name("fork"), *philosopher & tests::reacts_to<msg_taken>()); 

Primeiro, aqui vemos quando tudo em vez de quando. Isso ocorre porque, para disparar uma etapa, precisamos preencher várias condições ao mesmo tempo. O agente do garfo precisa lidar com o Take. E o filósofo precisa lidar com a resposta Tomada. Portanto, escrevemos when_all, não when. A propósito, também há quando_qualquer, mas não nos encontraremos com ele nos exemplos considerados hoje.

Em segundo lugar, também precisamos verificar o fato de que, após o processamento do Take, o agente do Fork estará no estado Tomado. Fazemos a verificação da seguinte maneira: primeiro, indicamos que, assim que o agente Fork terminar o processamento do Take, o nome do seu estado atual deve ser salvo usando a tag "fork". Essa construção apenas preserva o nome do estado do agente:

 & tests::store_state_name("fork") 

E então, quando o script é concluído com êxito, verificamos este nome salvo:
 REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork")); 

I.e. pedimos o script: forneça o nome que foi salvo com a tag fork da etapa chamada take_when_free e, em seguida, compare o nome com o valor esperado.

Aqui, talvez, esteja tudo o que pode ser observado no caso de teste do agente Fork. Se os leitores tiverem alguma dúvida, pergunte nos comentários, responderemos com prazer.

Teste de script bem-sucedido para filósofo


Para o agente filósofo, consideraremos apenas um caso de teste - para o caso em que o filósofo pode pegar os garfos e comer.

Este caso de teste terá a seguinte aparência:

 TEST_CASE( "philosopher (takes both forks)" ) { tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } }; so_5::agent_t * philosopher{}; so_5::agent_t * left_fork{}; so_5::agent_t * right_fork{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { left_fork = coop.make_agent<fork_t>(); right_fork = coop.make_agent<fork_t>(); philosopher = coop.make_agent<philosopher_t>( "philosopher", left_fork->so_direct_mbox(), right_fork->so_direct_mbox()); }); auto scenario = sobj.scenario(); scenario.define_step("stop_thinking") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_thinking>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); scenario.define_step("take_left") .when( *left_fork & tests::reacts_to<msg_take>() ); scenario.define_step("left_taken") .when( *philosopher & tests::reacts_to<msg_taken>() & tests::store_state_name("philosopher") ); scenario.define_step("take_right") .when( *right_fork & tests::reacts_to<msg_take>() ); scenario.define_step("right_taken") .when( *philosopher & tests::reacts_to<msg_taken>() & tests::store_state_name("philosopher") ); scenario.define_step("stop_eating") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_eating>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); scenario.define_step("return_forks") .when_all( *left_fork & tests::reacts_to<msg_put>(), *right_fork & tests::reacts_to<msg_put>() ); scenario.run_for(std::chrono::seconds(1)); REQUIRE(tests::completed() == scenario.result()); REQUIRE("wait_left" == scenario.stored_state_name("stop_thinking", "philosopher")); REQUIRE("wait_right" == scenario.stored_state_name("left_taken", "philosopher")); REQUIRE("eating" == scenario.stored_state_name("right_taken", "philosopher")); REQUIRE("thinking" == scenario.stored_state_name("stop_eating", "philosopher")); } 

Bastante volumoso, mas trivial. Primeiro, verifique se o filósofo terminou de pensar e começou a se preparar para a comida. Depois, verificamos que ele tentou pegar o garfo esquerdo. Em seguida, ele deve tentar pegar o garfo certo. Então ele deve comer e interromper esta atividade. Então ele deve colocar os dois garfos.

Em geral, tudo é simples. Mas você deve se concentrar em duas coisas.

Primeiro, a classe testing_env_t, como seu protótipo, wrap_env_t, permite personalizar o ambiente do SObjectizer. Usaremos isso para ativar o mecanismo de rastreamento de entrega de mensagens:

 tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } }; 

Esse mecanismo permite que você "visualize" o processo de entrega de mensagens, o que ajuda na investigação do comportamento do agente (já conversamos sobre isso em mais detalhes ).

Em segundo lugar, o agente Philosopher realiza uma série de ações não imediatamente, mas depois de algum tempo. Portanto, começando a funcionar, o agente deve enviar uma mensagem StopThinking pendente. Portanto, essa mensagem deve chegar ao agente após alguns milissegundos. O que indicamos definindo a restrição necessária para uma determinada etapa:

 scenario.define_step("stop_thinking") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_thinking>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); 

I.e. aqui dizemos que não estamos interessados ​​em nenhuma reação do agente filósofo ao StopThinking, mas apenas no que ocorreu antes de 250ms após o início do processamento desta etapa.

Uma restrição do tipo not_before informa ao script que todos os eventos que ocorrem antes que o tempo limite especificado expire devem ser ignorados.

Há também uma restrição no formulário not_after, que funciona ao contrário: somente os eventos que ocorrem até o tempo limite especificado expirar são levados em consideração.

As restrições not_before e not_after podem ser combinadas, por exemplo:

 .constraints( tests::not_before(std::chrono::milliseconds(250)), tests::not_after(std::chrono::milliseconds(1250))) 

mas, nesse caso, o SObjectizer não verifica a consistência dos valores fornecidos.

Como você conseguiu implementar isso?


Eu gostaria de dizer algumas palavras sobre como tudo começou a funcionar. Afinal, em geral, fomos confrontados com uma grande questão ideológica: "Como testar agentes em princípio?" e uma pergunta menor, já técnica: "Como implementar isso?"

E se sobre a ideologia de teste foi possível sair da sua mente, então sobre a implementação a situação foi mais complicada. Era necessário encontrar uma solução que, em primeiro lugar, não exigisse uma alteração radical dos interiores do SObjectizer. E, em segundo lugar, deveria ser uma solução que pudesse ser implementada no curto prazo previsível e, muito desejável.

Como resultado do difícil processo de fumar bambu, foi encontrada uma solução. Para isso, foi necessário, de fato, fazer apenas uma pequena inovação no comportamento regular do SObjectizer. E a base da solução é o mecanismo de envelope de mensagem, que foi adicionado na versão 5.5.23 e sobre o qual já falamos .

Dentro do ambiente de teste, cada mensagem enviada é embrulhada em um envelope especial. Quando um envelope com uma mensagem é entregue ao agente para processamento (ou, inversamente, rejeitado pelo agente), o cenário de teste fica ciente disso. Graças aos envelopes, o script de teste sabe o que está acontecendo e pode determinar os momentos em que o script "funciona".

Mas como fazer o SObjectizer envolver cada mensagem em um envelope especial?

Essa foi uma pergunta interessante. Ele decidiu da seguinte maneira: um conceito como event_queue_hook foi inventado. Este é um objeto especial com dois métodos - on_bind e on_unbind.

Quando um agente é vinculado a um despachante específico, o despachante emite um event_queue do agente para o agente. Por meio dessa event_queue, as solicitações do agente entram na fila necessária e ficam disponíveis para o expedidor para processamento. Quando um agente é executado dentro de um SObjectizer, ele possui um ponteiro para event_queue. Quando um agente é removido de um SObjectizer, seu ponteiro para event_queue é anulado.

Portanto, a partir da versão 5.5.24, o agente, após o recebimento do event_queue, deve chamar o método on_bind do event_queue_hook. Onde o agente deve passar o ponteiro recebido para event_queue. E event_queue_hook pode retornar o mesmo ponteiro ou outro ponteiro em resposta. E o agente deve usar o valor retornado.

Quando um agente é removido de um SObjectizer, ele deve chamar on_unbind em event_queue_hook. Em on_unbind, o agente transmite o valor retornado pelo método on_bind.

Toda essa cozinha é executada dentro do SObjectizer e o usuário não vê nada disso. E, em princípio, você pode não saber nada disso. Mas o ambiente de teste do SObjectizer, o mesmo testing_env_t, explora exatamente event_queue_hook. Dentro testing_env_t, uma implementação especial de event_queue_hook é criada.Essa implementação em on_bind agrupa cada event_queue em um objeto proxy especial. E esse objeto proxy já coloca as mensagens enviadas ao agente em um envelope especial.

Mas isso não é tudo.Você deve se lembrar que, em um ambiente de teste, os agentes devem ser congelados. Isso também é implementado através dos objetos proxy mencionados. Enquanto o script de teste não estiver em execução, o objeto proxy armazena as mensagens enviadas para o agente em casa. Mas quando o script é executado, o objeto proxy transfere todas as mensagens acumuladas anteriormente para a fila de mensagens atual do agente.

Conclusão


Em conclusão, quero dizer duas coisas.

Primeiro, implementamos nossa visão de como os agentes podem ser testados no SObjectizer. Minha opinião, porque não existem tantos bons exemplos por aí. Nós olhamos para Akka.Testing . Mas o Akka e o SObjectizer são muito diferentes para portar as abordagens que funcionam no Akka para o SObjectizer. E o C ++ não é Scala / Java, no qual algumas coisas relacionadas à introspecção podem ser feitas devido à reflexão. Então eu tive que criar uma abordagem que fosse do SObjectizer.

Na versão 5.5.24, a primeira implementação experimental ficou disponível. Certamente você pode fazer melhor. Mas como entender o que será útil e o que são fantasias inúteis? Infelizmente nada. Você precisa tomar e tentar, ver o que acontece na prática.

Por isso, criamos uma versão mínima que você pode usar e experimentar. O que nos propomos a fazer por todos: experimente, experimente e compartilhe suas impressões conosco. O que você gostou, o que você não gostou? Talvez algo esteja faltando?

Em segundo lugar, as palavras ditas no início de 2017 tornaram-se ainda mais relevantes :
… , , , . - — . . . : , .

, , , — , .

Portanto, meu conselho para aqueles que procuram uma estrutura de ator pronta: preste atenção não apenas à originalidade das idéias e à beleza dos exemplos. Veja também todos os tipos de coisas auxiliares que ajudarão você a descobrir o que está acontecendo no seu aplicativo: por exemplo, para descobrir quantos atores estão dentro agora, quais são os tamanhos de fila deles, se a mensagem não chegar ao destinatário, então para onde ele vai ... Se a estrutura fornece algo assim, será mais fácil para você. Caso contrário, você terá mais trabalho.
Tudo isso é ainda mais importante quando se trata de testar atores. Portanto, ao escolher uma estrutura de ator para si mesmo, preste atenção no que está nela e no que não está. Por exemplo, já temos em nosso kit de ferramentas para simplificar os testes :)

Source: https://habr.com/ru/post/pt435606/


All Articles