Mostrando postagens com marcador Mock Objects. Mostrar todas as postagens
Mostrando postagens com marcador Mock Objects. Mostrar todas as postagens

quarta-feira, 17 de novembro de 2010

Bons frameworks C++ para criação automática de mocks

Frameworks para criação automática de objetos substitutos (“mocks”) em C++ foram evoluindo num passo muito mais lento que os para Java, C# ou mesmo Ruby. A dificuldade de criar uma boa arquitetura para simulação da criação automática sem perder a portatilidade entre compiladores e também a falta de desenvolvedores para melhorar os frameworks contribuiram para esse atraso.

Ainda há poucos frameworks altamente portáteis e, menos ainda, estáveis, mas alguns funcionam bem para a maioria dos casos. Há projetos bem estáveis e portáteis, como Google Mock ou MockPP, mas que não abrangem a criação automática de objetos substitutos. Neles, é preciso criar uma classe substituta (implementando a interface que se deseja simular) e usar macros para gerar os métodos.

Os frameworks de geração automática em C++ geralmente exploram o modelo de representação interna (layout ABI) de classes usado em um determinado compilador. Combinado ao uso de templates e algumas técnicas como Fast Delegates, é feito a geração do código na hora da declaração. Como o uso da representação interna varia de compilador para compilador (se bem que em muitos a representação é parecida), alguns projetos deixam de adicionar a compatibilidade com determinados compiladores - até pelo fato de faltar desenvolvedores interessados (ou habilitados) em fazer a adaptação.

Exemplo base

Para exemplificar o uso dos frameworks, tomarei por base o simples código abaixo:

class Piada
{
public:
Piada(const string &autor, const string &texto)
: _autor( autor ), _texto( texto )
{
}
// ... gets e sets
private:
// ... ctor, dtor, ops...
string _autor;
string _texto;
};


// Piada retirada de: http://confiar.atspace.com/curtas_boas.htm
const Piada PIADA_TOSCA( "Desconhecido",
"Você conhece a piada do fotógrafo ? Ainda não foi revelada." );


class RepositorioPiada
{
public:
virtual ~RepositorioPiada() {};
// ...
virtual bool Existe() const = 0;
virtual void Adicionar(const Piada &piada) = 0;
virtual void Alterar(const Piada &piada) = 0;
// ...
};

class ServicoPiada
{
public:
ServicoPiada(RepositorioPiada &repositorio)
: _repositorio( repositorio )
{
}
//...
void Salvar(const Piada &piada)
{
if ( _repositorio.existe( piada ) )
_repositorio.alterar( piada );
else
_repositorio.adicionar( piada );
}
private:
//...
RepositorioPiada _repositorio;
};


Na maioria dos exemplos será feito o uso do UnitTest++, com o qual todos são compatíveis.


Isolator ++


Começando por uma opção paga, lançada mês passado, o Isolator ++ possui uma característica extremamente rara em relação aos concorrentes: permite criar substitutos para classes concretas, métodos não virtuais e métodos estáticos. Além disso, funciona em conjunto como alguns frameworks de teste populares: Google Test, UnitTest++, Yaffut, CPPUnit e Boost::Test.


Porém, ele tem algumas limitações consideráveis, na versão avaliada durante esse post: só funciona com o Visual Studio (2008 e 2010) e métodos abstratos (ex: virtual void FazAlgo() = 0;) devem ser declarados com a macro PURE do Visual Studio (ex: virtual void FazAlgo() = PURE;), algo que eles pretendem resolver na próxima versão. Assim, o Isolator ++ tem grandes vantagens e desvantagens importantes que limitam sua abrangência. Por ser um produto novo e pago, deve evoluir rapidamente, o que torna seu futuro promissor.

TEST_F(TesteServicoPiada, DeveSalvarUmaPiadaNova)
{
RepositorioPiada *rp = FAKE<RepositorioPiada>();
WHEN_CALLED(rp->Existe( PIADA_TOSCA )).ReturnVal( false );

ServicoPiada servico( rp );
servico.Salvar( PIADA_TOSCA );

ASSERT_WAS_CALLED( rp->Adicionar( PIADA_TOSCA ) );
ISOLATOR_CLEANUP();
}

Acima um teste realizado com o Isolator ++. Repare o uso de FAKE para criação do objeto substituto, WHEN_CALLED para criação da expectativa, ASSERT_WAS_CALLED para verificar a chamada de um método e ISOLATOR_CLEANUP para a destruição dos objetos do Isolator ++.


AMOP


AMOP (Automatic Mock Object for C++) é um projeto de código aberto que conheci há uns dois anos (logo assim que surgiu) e teve poucas atualizações de lá pra cá, o que o fez permanecer com alguns bugs inconvenientes. Ele possui suporte ao Visual Studio, ao GCC e ao  C++ Builder 2009 – sendo o suporte a este último uma contribuição minha ao projeto – e funciona em conjunto com UnitTest++.

TEST_F(TesteServicoPiada, DeveSalvarUmaPiadaNova)
{
MockObject< RepositorioPiada > mock;

mock.call( &RepositorioPiada::Existe )
.expect( PIADA_TOSCA )
.returning( false );

mock.call( &RepositorioPiada::Adicionar )
.expect( PIADA_TOSCA );

ServicoPiada servico( mock );
servico.Salvar( PIADA_TOSCA );

mock.verify();
}

Acima um teste realizado com o AMOP. Repare uso de MockObject parametrizado para a criação do objeto substituto, do método call para criação da expectativa e do método verify para a verificação das expectativas criadas. O método verify não necessita ser chamado, pois é chamado automaticamente na destruição do objeto mock, que ocorre no fim do escopo do teste.


MockitoPP


O MockitoPP é a versão C++ do Mockito, projeto originalmente escrito em Java. De código aberto, é compatível com GCC e Visual Studio e pode ser usado com Google Test, o Hamcrest ou a Boost (regex). Venho usando exporadicamente há cerca de um ano e sinto que a versão atual apresenta poucos problemas (no GCC, era preciso colocar o operador < em algumas classes).

TEST(TesteServicoPiada, DeveSalvarUmaPiadaNova)
{
mock_object< RepositorioPiada > mock;

mock( &RepositorioPiada::Existe )
.when( PIADA_TOSCA )
.thenReturn( false );

mock( &RepositorioPiada::Adicionar )
.when( PIADA_TOSCA )
.thenReturn();

RepositorioPiada &rp = mock.getInstance();

ServicoPiada servico( mock );
servico.Salvar( PIADA_TOSCA );

ASSERT_TRUE( mock.verify( &RepositorioPiada::Existe ).exactly( 1 ) );
ASSERT_TRUE( mock.verify( &RepositorioPiada::Adicionar ).exactly( 1 ) );
}

Acima um teste realizado com MockitoPP. Repare o uso mock_object parametrizado para a criação do objeto substituto, do método when para criação da expectativa e do método verify para a verificação das expectativas criadas. Como pode ser visto, sua sintaxe é bem parecida com a AMOP, porém tendo mais opções de verifição de comportamento (veja a documentação no site).


HippoMocks


O HippoMocks é outro projeto de código aberto, compatível com GCC, Visual Studio e Comeau. O framework de testes usado pelo HippoMocks é o Yaffut, que reune as qualidades dos outros frameworks de teste (veja os detalhes na página do Yaffut). No que visto há cerca de um ano, tem boa estabilidade, sendo ligeiramente melhor que a do MockitoPP.

FUNC( DeveSalvarUmaPiadaNova )
{
MockRepository mocks;
RepositorioPiada *mock = mocks.InterfaceMock< RepositorioPiada >();

mocks.ExpectCall( mock, RepositorioPiada::Existe )
.With( PIADA_TOSCA )
.Return( false );

mocks.ExpectCall( mock, RepositorioPiada::Adicionar )
.With( PIADA_TOSCA );

ServicoPiada servico( mock );
servico.Salvar( PIADA_TOSCA );
}

Acima um teste realizado com HippoMocks. Repare que o objeto substituto é criado através do método InterfaceMock, da classe MockRepository. As espectativas são criadas usando ExpectCall e, ao final, elas são verificadas automaticamente.


Comparativo


O comparativo a seguir é subjetivo, mas mostra uma percepção prática dos frameworks em relação aos critérios julgados relevantes na escolha. A nota varia entre 1 e 4, sendo 1=Ruim, 2=Razoável, 3=Bom e 4=Ótimo.


comparacao


Pelo fato da estabilidade, pessoalmente, dou preferência ao uso de HippoMocks. Apesar disto, gosto de poder usar os matchers do Hamcrest, que já vem “nativos” no MockitoPP.


Ficaram de fora da análise alguns frameworks que ainda não experimentei, mas parecem interessantes: M0cxx0r e MockItNow. Caso você conheça algum outro, por favor deixe seu comentário.

sábado, 13 de dezembro de 2008

Google Mock

A Google liberou seu C++ Mock Framework sob a nova licensa BSD, complementando seu Google C++ Testing Framework (ou somente Google Test). Ao que me parece, eles fizeram um trabalho bem completo, deixando o framework flexível e fácil de usar.

O Google Mock é baseado no EasyMock, no jMock e no Hamcrest, sendo construído para C++ sob o Google Test. Para utilizá-lo você precisará, além do Google Test que já vem incluso, da std::tr1::tuple, encontrada em algumas implementações de novos compiladores e na boost. Por enquanto ele está acoplado ao Google Test, mas há planos de retirar esta dependência.

Já vinha utilizando o MockPP e vendo o Google Mock tenho a impressão de seu uso ser um pouco mais simples. Assim que tiver um tempo (coisa rara), vou analisá-lo melhor e fazer uma breve comparação, talvez postando-a aqui no blog.

Mais informações sobre o Google Mock aqui.

quinta-feira, 21 de agosto de 2008

TDD na Prática - Parte II: Verificação e Confiabilidade

Como visto na Parte I, para criar código (que seja facilmente) "testável" é preferível escrever código que o testa primeiro.

Exercitar o código verificando seus limites e estados auxilia na descoberta de como o mesmo deve se comportar e traz um maior controle sobre seu funcionamento. Conseqüentemente, culmina em um maior nível de confiabilidade.

Tornar freqüente a verificação do código, de forma com que a cada alteração do mesmo ele seja novamente verificado, garante a manutenção deste nível de confiabilidade.

Verificar o código continuamente e acrescentar novas verificações à medida em que forem necessárias, além de re-executar todos os testes (teste de regressão), pode ser um processo doloroso se não for automatizado. Assim, existem diversas ferramentas que suavizam o processo, permitindo nos concentrar no que é importante.

Como o uso das ferramentas, em poucos (mili)segundos executamos todas as verificações e temos um maior grau de certeza que:

1) O novo código criado passa nos testes;

2) Após uma alteração no código, ele continua passando nos testes;

3) A alteração numa parte do código não fez outra parte do código parar de funcionar (compilar), nem deixar de funcionar como esperado (mudou o comportamento), pois ela também continua passando nos testes;

Para garantir 1) e 2) temos o Teste Unitário e para 3) o Teste de Integração e o Teste de Regressão.

Diversos frameworks de testes foram criados com esta finalidade, para cada linguagem. Em especial, os baseados na SUnit (como JUnit, CppUnit, DUnit, NUnit, etc.) são os preferidos por serem simples, poderosos e fáceis de adaptar a qualquer ambiente de desenvolvimento ou sistema operacional.

TDD, xUnit e xMock

A maioria dos desenvolvedores que adotam TDD, adotam também uma framework xUnit para execução dos testes. Não é diferente comigo, nem com este blog. Para cada linguagem há uma variedade de opções, com diversas vantagens e desvantagens a se ponderar. Por exemplo, para C++ existe também a Boost.Test, a CppUnitLite, a NanoCppUnit, a Unit++, a CxxTest e por aí vai. Tenho preferência pelas versões com maior adoção (como a CppUnit), apesar de que para determinadas aplicações, certos frameworks trazem vantagens interessantes.

Outra ferramenta importante, recomendada para usuários que já possuam alguma experiência em testes, é a de simulação de comportamento, para os chamados Objetos Substitutos, ou Mock Objects. Frameworks como jMock, MockPP, NMock, etc. etc. etc. trazem maneiras muito úteis de fazer construções que testem o comportamento esperado de objetos.

Enquanto os frameworks xUnit tem foco na verificação de estados de um objeto, os xMock tem foco na verificação do comportamento dos objetos, na interação entre os mesmos. É sempre possível simular os testes sem o auxílio de um framework, mas seu uso traz uma certa padronização na maneira com a qual os testes são construídos.

Com todas as facilidades e opções disponíveis, a adoção de um framework de testes passa a ser de extrema importância na implementação de TDD. Neste blog, você verá alguns deles em ação e saberá como a maioria deles funciona.

quarta-feira, 20 de agosto de 2008

Interação e Substituição de Objetos

Ter uma visão clara de como os objetos interagem é um fator fundamental no desenvolvimento de software orientado a objetos. É até mais importante do que saber como cada objeto funciona isoladamente.

Pensando que um software O. O. é uma espécie de teia de objetos que se interconectam, fazendo de sua interação o fator gerador do comportamento do software, mudar seu comportamento será como plugar novos objetos no lugar dos que estavam, bastando para isso que o plug esteja no formato esperado.

Simular este tipo de comportamento no software requer que tomemos algumas medidas de análise bem pensadas, estudadas, para que tudo se encaixe da maneira apropriada.

Tipos de Substituição

A substituição dos objetos pode não servir apenas para mudar o comportamento do software. Ela pode servir, principalmente, para analisar o comportamento do mesmo.

Se ao invés de plugar um objeto que precise desempenhar determinada tarefa você plugar outro que registre ou exponha como se deu a interação dos outros objetos com ele, você terá uma excelente ferramenta para análise comportamental.

Caso o objeto precise desempenhar tarefas necessitando da colaboração de outros, sendo requerida a passagem de mensagens para os mesmos, seu objeto pode simulá-la sem que seja necessário qualquer esforço adicional.

Você pode substituir o comportamento do objeto (plug) real pelo comportamento que você deseja, apenas para investigar o comportamento dos objetos (plugues) em que ele estará conectado.

Objeto Substituto

Como disse, para que o objeto substituto possa ocupar o espaço do original, basta que ele apresente o mesmo formato. Este formato do objeto pode ser descrito também como a interface do objeto, ou seja, a maneira com a qual é feita a interação com ele.

Mantendo a mesma interface, os objetos interconectados a ele não precisam ser modificados, podendo lhe referir sempre da mesma maneira.

Mock Object, Proxy e outros nomes

Ao objeto substituto são dados diversos nomes, como Mock Object (geralmente como é referenciado em TDD), Proxy (Padrão GoF), Surrogate, etc. Apesar de haverem diversas variações na maneira como são usados (por exemplo, como virtual proxies, remote proxies, smart references, etc.), todos se baseiam no sentido da substituição do objeto original.

Em suma

A substituição dos objetos para análise, teste ou mudança do comportamento é um dos conceitos mais significativos para o entendimento da criação de software testável e de objetos reutilizáveis. Entender as variações possíveis e os benefícios e problemas trazidos por cada implementação traz uma grande maturidade ao desenvolvedor e potencializa o crescimento e a manutenção de software, levando à um custo de alteração mais baixo e a melhoria da "testabilidade" do mesmo.