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:
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.
Não, eu não quero que vocês gerem o código dos testes, isto iria apenas criar testes inúteis!
Mas eu acho muito chato ter certeza de que todos os possíveis passos de um arquivo txt com a user story estão presentes nos passos definidos no arquivo .rb
Claro que isto não esta considerando reutilização de passos através de Mixins ou outras técnicas semelhantes, mas este script me ajudou bastante ja, então resolvi compartilhar ele para quem estiver interessado
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 | name=ARGV[0] all = [] last = "" f = File.new(name) f.each_line do |l| l.strip! l,command,params = *(/^(When|Then|Given|And) (.*)/.match l) if l command = last if command == "And" last = command params = params.gsub /"/, '\"' all << "#{last} \"#{params}\" do\n pending\n end" end end all.uniq!.sort! sym_name = name[File.dirname(name).length+1..-5] puts %Q{require 'stories/helper' steps_for(:#{sym_name}) do } all.each { |l| puts " #{l}"} puts %Q{end with_steps_for :#{sym_name} do run "\#{dir = File.dirname(__FILE__)}/#{sym_name}.txt" end } |
eu salvei este código em um arquivo txt_to_steps.rb, e para utilizar basta executar:
ruby txt_to_steps.rb
Um exemplo de user story (utilizando o formato do RSpec) seria este texto:
Story: new user As a company employee I want to register in the CRM So that I can see and manage company contacts Scenario: user with no access to the system Given the username user1 And the password mypassword When the login form is submited Then the login form should be shown again Scenario: user registration Given the username user1 And the password mypassword And the email user1@company.com When the registration form is submited And there is no other user with the same e-mail or email Then the registration should be OK And the user should be redirected to / Scenario: repeated user registration Given the username user1 And the password mypassword And the email user1@company.com When the registration form is submited And there is already another user with the same name or email Then the registration should fail And the registration form should apear again Scenario: existing user login Given the username user1 And the password mypassword When the login form is submited Then the user should be redirected to /
O txt_to_steps pode ser melhorado para tentar identificar alguns padrões, mas como esta agora ja me poupou bastante trabalho ![]()
Sei que não é o código ruby mais limpo que vocês ja leram, mas para algo escrito em 5 minutos até que ficou legal
Se ajudar mais alguem, a única exigência é deixar um comentário aqui dizendo o que poderia ser melhorado no script
PS.: sei que o blog anda meio parado demais, mas é por um bom motivo, acho que daqui a um mes aproximadamente volta tudo ao normal e eu posso contar aqui o motivo deste tempo quase sem posts