Testes unitários em C++ para um programador Java!
English version here
Eu trabalhei com C++ quando iniciei no mundo da programação (entre 1997 e 2000), mas na época eu trabalhava com o Borland C++ Builder e o Microsoft Visual C++, naquela época eu ainda não tinha ouvido falar em testes unitários, depois disto eu trabalhei com Delphi, PHO, ASP, ColdFusion, …
Desde 2002 eu trabalhei a maior parte do tempo com Java, e aprendi muito neste período, muitas boas práticas, muito sobre orientação a objetos e principalmente, aprendi a amar os testes unitários.
Pouco tempo atrás eu voltei a trabalhar com C++, mas já viciado em testes unitários, e querendo aplica-los ao meu código C++ também, e este post é um exemplo bem curto de como um programador Java pode trabalhar com C++ utilizando testes unitários.
Um projeto C++ começa por um Makefile, eu estou acostumado com o ANT e não gosto da idéia de listar todos os meus arquivos fonte na configuração de build como a maior parte dos exemplos de Makefiles fazem, então eu criei um Makefile simples, mas bastante flexível para o meu projeto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | TESTDIRECTORIES := test DIRECTORIES := src SOURCES := $(foreach dir,$(DIRECTORIES),$(wildcard $(dir)/*.cpp)) TESTSOURCES := $(foreach dir,$(TESTDIRECTORIES),$(wildcard $(dir)/*.cpp)) OBJECTS := $(patsubst %.cpp,%.obj,$(SOURCES)) TESTOBJECTS := $(patsubst %.cpp,%.obj,$(TESTSOURCES)) TESTOBJECTS += $(filter-out src/main.obj,$(OBJECTS)) TARGET := example LINK := g++ CC := g++ CFLAGS := -c LFLAGS := all: $(OBJECTS) $(LINK) $(LFLAGS) -o $(TARGET) $(OBJECTS) test: $(TESTOBJECTS) $(LINK) $(LFLAGS) -lcppunit -o $(TARGET)_unit $(TESTOBJECTS) ./$(TARGET)_unit %.obj:%.cpp $(CC) $(CFLAGS) -o $*.obj $*.cpp |
Com este Makefile, todos os arquivos .cpp que estiverem no diretório src farão parte do executável gerado, mais diretórios podem ser adicionados simplesmente atualizando a variável DIRECTORIES, a mesma coisa acontece com o diretório test e a variável TESTDIRECTORIES para os testes unitários.
O truque aqui é a combinação das funções foreach e wildcard, a função pathsubst é usada para alterar as extensões de .cpp para .obj e a função filter-out é usada para remover o main.cpp dos testes unitários pois este arquivo é apenas o ponto de entrada para o executável principal.
Este Makefile é o mais próximo que eu consegui chegar da funcionalidade do ANT para programação C++, claro que ela pode ser melhorada, considerando que eu não sou um especialista em Makefiles.
Mas o Makefile não é o motivo deste post, estou escrevendo para contar para vocês sobre o CppUnit, uma ótima implementação xUnit para C++.
Eu comecei o projeto escrevendo o “executador de testes” do CppUnit:
testRunner.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <cppunit/CompilerOutputter.h> #include <cppunit/extensions/TestFactoryRegistry.h> #include <cppunit/TestResult.h> #include <cppunit/TestResultCollector.h> #include <cppunit/TestRunner.h> #include <cppunit/BriefTestProgressListener.h> int main (int argc, char* argv[]) { // informs test-listener about testresults CPPUNIT_NS :: TestResult testresult; // register listener for collecting the test-results CPPUNIT_NS :: TestResultCollector collectedresults; testresult.addListener (&collectedresults); // register listener for per-test progress output CPPUNIT_NS :: BriefTestProgressListener progress; testresult.addListener (&progress); // insert test-suite at test-runner by registry CPPUNIT_NS :: TestRunner testrunner; testrunner.addTest (CPPUNIT_NS :: TestFactoryRegistry :: getRegistry ().makeTest ()); testrunner.run (testresult); // output results in compiler-format CPPUNIT_NS :: CompilerOutputter compileroutputter (&collectedresults, std::cerr); compileroutputter.write (); // return 0 if tests were successful return collectedresults.wasSuccessful () ? 0 : 1; } |
CppUnit é mito flexível, permitindo diversos tipos de saída para os resultados dos testes, mas escreverei sobre isto em outro post, a idéia atrás deste “executador” é a utilização do registro de testes do CppUnit, o que torna a vida muito mais fácil.
O registro de testes é bem próximo ao fileset passado a task junit do ant, mas os testes se registram sozinhos.
Depois do “executador de testes” pronto, podemos começar a escrever os testes unitários.
Em C++ diferente do Java, são necessários dois arquivos para cada classe, um cabeçalho e uma implementação.
Então, vamos começar com o cabeçalho.
mainTest.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #ifndef MAINTEST_H #define MAINTEST_H #include <cppunit/TestFixture.h> #include <cppunit/extensions/HelperMacros.h> #include "../src/HelloWorld.hpp" using namespace std; class MainTest : public CPPUNIT_NS :: TestFixture { CPPUNIT_TEST_SUITE (MainTest); CPPUNIT_TEST (testHello); CPPUNIT_TEST_SUITE_END (); public: void setUp (void); void tearDown (void); void testHello (void); private: HelloWorld *hello; }; CPPUNIT_TEST_SUITE_REGISTRATION (MainTest); #endif |
Neste cabeçalho temos uma declaração de classe simples, extendendo TestFixture do namespace do CppUnit.
C++ não possui reflexão, por isto o CppUnit possui algumas macros para definir o teste, que podem ser vistas no início da declaração da classe, será necessária uma linha com CPPUNIT_TEST para cada método de teste que você declarar.
A linha: CPPUNIT_TEST_SUITE_REGISTRATION (MainTest);
Faz a mágica do auto registro dos testes.
Com isto pronto, você pode começar a implementar a classe de testes como faria em java:
mainTest.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include "mainTest.hpp" void MainTest::setUp(){ hello = new HelloWorld("Test"); } void MainTest::tearDown(){ delete hello; } void MainTest::testHello(){ string expected("Hello Test\n"); CPPUNIT_ASSERT_EQUAL(expected,hello->sayHello()); } |
Como no JUnit existem os métodos setUp e tearDown que são executados antes e depois de cada um dos testes, e o método testHello possui o código do teste (ja que só foi implementado um para este exemplo).
As asserções no CppUnit são feitas utilizando macros.
O CppUnit disponibiliza as seguintes asserções:
- CPPUNIT_ASSERT(condition)
- CPPUNIT_ASSERT_MESSAGE(message,condition)
- CPPUNIT_FAIL( message )
- CPPUNIT_ASSERT_EQUAL(expected,actual)
- CPPUNIT_ASSERT_EQUAL_MESSAGE(message,expected,actual)
- CPPUNIT_ASSERT_DOUBLES_EQUAL(expected,actual,delta)
- CPPUNIT_ASSERT_THROW( expression, ExceptionType )
Muito menos do que no JUnit, mas o suficiente para a grande maioria dos casos.
Depois do teste pronto, agora precisamos escrever o código para que os testes passem.
HelloWord.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h> #include <iostream> #ifndef MAIN_HPP #define MAINHPP class HelloWorld{ private: std::string name; public: HelloWorld(char* name); std::string sayHello(); }; #endif |
E a implementação:
HelloWord.cpp
1 2 3 4 5 6 7 8 9 10 11 | #include "HelloWorld.hpp" HelloWorld::HelloWorld(char* name){ this->name = name; } std::string HelloWorld::sayHello(){ std::string result("Hello "); result = result + name + "\n"; return result; } |
Para executar os testes, basta você executar no console:
make test
Agora que todos os testes foram escritos e estão passando, o último passo é escrever o código para inicializar a aplicação:
main.cpp
1 2 3 4 5 6 7 8 | #include "HelloWorld.hpp" int main(int argc, char** argv){ if (argc >= 2) { HelloWorld* hello = new HelloWorld(argv[1]); std::cout << hello->sayHello(); delete hello; } } |
E você tem a sua primeira aplicação test driven escrita em C++!
PS.: se você esta utilizando o Makefile que escrevi, lembre que o código da aplicação deve ficar no diretório src e o código de testes no diretório test.
PS2.: O exemplo foi testado em um linux com o CppUnit instalado pelo gerenciador de pacotes, se você quiser instalar o cppunit usando o código fonte ou for executar em outra plataforma lembre-se de atualizar o CFLAGS com os caminhos de influde corretos e o LFLAGS com o caminho da biblioteca do cppunit, se você não esta utilizando o g++ como compilador e linker, lembre-se de atualizar as variáveis CC e LINK.
Se você gostou deste post, lembre-se de assinar o RSS feed do blog, para ser notificado de novos posts!


