Etikettarkiv: TDD

Testdriven utveckling: Del 5, Refaktorera koden

Vi har nu i del fyra fått en kod som passerar testen, men det är inte vad man skulle kalla för snygg kod. Det är något som nästa del an Test Driven Utveckling hanterar nu är det dags att göra så mycket refaktorering vi känner för. Det man skall tänka på att det är refatorering. Inte lägga in ny funktionalitet, eller ändra på test resultatet. För varje refaktotering steg kan det vara bra att köra testerna igen.

tests.cpp:

class TimeOfDay {
    public:
        TimeOfDay(int hour, int minute) {}
        int hour() { return 12; }
};

timeofday.h:

class TimeOfDay {
    public:
        TimeOfDay(int hour, int minute) {}
        int hour() { return 12; }
};

Om vi tittar på testerna och koden så ser vi att test frågan 12 och svaret 12 är duplicerat. Det vore en ganska enkel refaktoering att ta bort detta. Genom att spara hour värdet på indatat så sliper vi duplicerade värdet från enhets testerna.

class TimeOfDay {
    private:
        int m_hour;
    public:
        TimeOfDay(int hour, int minute) : m_hour(hour) {}
        int hour() { return m_hour; }
};

Kanske man skulle kunna ta bort upprepningen av konstanten 12 också i test koden.

TEST(timeofday, midnighthavehourzero) {
    int hour = 12;
    TimeOfDay test(hour, 0);
    EXPECT_EQ(hour, test.hour());
}

När jag nu sätter mig ned, tittar på koden känner jag att för detta grundläggande test har vi gjort ungefär så mycket som behövs. Jag tycker nog att testa att minuter fungerar också är en bra ide. De alternativ som kommer runt det testen blir nästa korta del.

Testdriven utveckling: Del 4, Mot grönt

Nu när vi har ett test som fallerar på det sätt vi förväntar oss. Är det dags för näste steg i den testdriva processen. Att på enklast möjliga sätt få testen att passera. Vi jobbar alltså mot grönt ljus i test systemet.

Att bara byta ut retur värdet i hour() från 0 till 12 kan se som det mest triviala sättet att få ett grönt ljus.

class TimeOfDay {
    public:
        TimeOfDay(int hour, int minute) {}
        int hour() { return 12; }
};

Så fort vi har ändrat en test är det dags att provköra.

balp@silvara:~/localsrc/tdd-blog$ ( cd build && cmake .. && make && ./testprogram )
-- Configuring done
-- Generating done
-- Build files have been written to: /home/balp/localsrc/tdd-blog/build
[ 33%] Built target gtest
[ 66%] Built target gtest_main
[100%] Built target testprogram
Running main() from gtest_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from timeofday
[ RUN      ] timeofday.midnighthavehourzero
[       OK ] timeofday.midnighthavehourzero (0 ms)
[----------] 1 test from timeofday (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 1 test.

Som vi ser passerar testen. Denna enkla lösning att bara fuska till svaret är något som är svårt för många programmerare att göra, man vill ta höjd för alla kommande problem, redan från början skriva in kod som löser massa svåra special fall. Man om man håller sig lite till man skriver en test som kör specialfallen blir det oftast en mycket bättre förståelse och testad kod.

Testdriven utveckling: Del 3, Ett inledande test

En kort tillbaka blick till hur man genomför Testdriven utveckling. Det är tre viktiga steg, Skapa en test, Rött, Få koden att klara testen, Grönt, Refaktorera koden. Nu när vi har en miljö där vi kan köra tester, är det dags att skapa vårt första test. Den första viktiga delen  är att titta på problemet. Vad skall vi försöka lösa, vilka krav finns det och så vidare. jag kommer att jobba med ett litet övning exempel. Jag kommer att skapa en enkel klass för tid.

Problemet

Skapa an klass för tid på dyngnet, upplösningen behöver vara i minuter. Klassen skall kunna få ut timmar, minuter samt vilken minut på dygnet det är. Klockan skall klara att addera och subtrahera två tidpunkter.

Första testen

Vi böjar med det enklaste faller, varför klockan 12. och kollar att det går att skapa ett objekt samt få ut timman 12. Medan vi skriver testet börjar vi också konstruera det gränssnitt vi vill ha på klasserna. Jag börjar med att testfilen, tests.cpp som vi skapade i förra delen lägga in ett test.

#include "timeofday.h"
#include "gtest/gtest.h"

TEST(timeofday, midnighthavehourzero) {
        TimeOfDay test(12, 0);
        EXPECT_EQ(12, test.hour());
}

VI har nu skapat ett enkelt gränssnitt, en funktions som returnerar timmen, testen är dock inte klar ännu. Om vi testar att kompilera vår test kod kommer vi att se att den inte bygger.

[100%] Building CXX object CMakeFiles/testprogram.dir/tests.cpp.o
/home/balp/localsrc/tdd-blog/tests.cpp:1:23: ödesdigert fel: timeofday.h: Filen eller katalogen finns inte

För att vi skall veta att testen är det vi tror att den är skall det bygga och kunna köras med det resultat vi förväntar oss. Vi måste alltså skapa oss en klass för tiden, skapa timeofday.h och skriv in följande.

class TimeOfDay {
    public:
        TimeOfDay(int hour, int minute) {}
        int hour() { return 0; }
};
VI kan nu köra våra tester och vi ser att vi har nått det förväntade resultatet.

1: Test command: /home/balp/localsrc/tdd-blog/build/testprogram
1: Test timeout computed to be: 1500
1: Running main() from gtest_main.cc
1: [==========] Running 1 test from 1 test case.
1: [———-] Global test environment set-up.
1: [———-] 1 test from timeofday
1: [ RUN ] timeofday.midnighthavehourzero
1: /home/balp/localsrc/tdd-blog/tests.cpp:6: Failure
1: Value of: test.hour()
1: Actual: 0
1: Expected: 12
1: [ FAILED ] timeofday.midnighthavehourzero (0 ms)
1: [———-] 1 test from timeofday (1 ms total)
1:
1: [———-] Global test environment tear-down
1: [==========] 1 test from 1 test case ran. (1 ms total)
1: [ PASSED ] 0 tests.
1: [ FAILED ] 1 test, listed below:
1: [ FAILED ] timeofday.midnighthavehourzero
1:
1: 1 FAILED TEST

Efter som vi nu har ett rött test fall, är det dags att lämna skapa test lägen och gå in på nästa del, att få testet att passera. Men det blir nästa blogpostning.

Koden för detta exempel finns på github.

Testdriven utveckling: Del 2, En bra byggmiljö

För att fortsätta på ämnet testdriven utveckling tänkte jag gå ned på detaljerna, först kommer jag med lite tankar och tips på hut man skriver bra tester. Kanske det svåraste steget, speciellt svårt är det om man har mycket befintlig kod och få tester. Jag rekommenderar att man skriver testerna med i ett befintligt testharnesk. För att det går oftast snabbare och de finns mycket bra funktionalitet i dem. Skriver man kod i Java är JUnit det självklara valet, i Python, unittest. För C++ finns det lite mer variation, jag har länge jobbat med ccpunit. Men det är mycket jobba att komma igång med det systemet. Det är inflexibelt och inte mycket har uppdaterats på många år. Just nu är mitt favorit system Google Test.

Byggmiljö

Det första man måste göra för att kunna testa är att sätta upp en byggmiljö, jag kör min i Ubuntu 11.10 Oneiric, för C++ behöver jag en kompilator, make, jag vill ha cmake för att det blir så mycket enklare att bygga då. Jag vill alltid ha ett fungerade versions hanterings system så som git. Skall man bygga på ubuntu/debian måste man också se till att man har alla de -dev paket som behövs installerade. Saknar man dessa får man ofta spännande problem. Bygger man på MacOS behöver man bara installera git, cmake och xcode.

Projektet

Börja med att skapa en katalog för projektet, och initiera git för versions kontroll.

balp@silvara:~/localsrc$ mkdir tdd-blog
balp@silvara:~/localsrc$ cd tdd-blog/
balp@silvara:~/localsrc/tdd-blog$ git init
Initialized empty Git repository in /home/balp/localsrc/tdd-blog/.git/

Efter detta lägger vi till Google test som ett git subreposotory. Jag har skapar en mirror av googles svn för att kunna göra det här smidigt på github.com.

balp@silvara:~/localsrc/tdd-blog$ git submodule add git://github.com/balp/googletest.git googletest
Cloning into googletest...
remote: Counting objects: 3489, done.
remote: Compressing objects: 100% (583/583), done.
remote: Total 3489 (delta 2704), reused 3457 (delta 2672)
Receiving objects: 100% (3489/3489), 1.14 MiB | 463 KiB/s, done.
Resolving deltas: 100% (2704/2704), done.

Nu har vi alla grundkomponenter på plats och vi kan sätta igång. Det första vi skall göra när vi programmerar är att skapa en test som fallerar. Låt oss skapa en tom test fil, och editera vår byggfil.

balp@silvara:~/localsrc/tdd-blog$ touch tests.cpp
balp@silvara:~/localsrc/tdd-blog$ vim CMakeLists.txt

CMakeLists.txt behöver ett minimalt cmake projekt som bygger och kör testerna. Skriv in följande i filen.

cmake_minimum_required(VERSION 2.8)
project(BlogExamples)
enable_testing()

add_subdirectory(googletest)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/googletest/include)
add_executable(testprogram tests.cpp)
target_link_libraries(testprogram gtest_main )
add_test(NAME test COMMAND $<TARGET_FILE:testprogram> )

include(CTest)

Bygg och testa med den tomma test filen, jag brukar när jag jobbar från kommando raden sätta samman byggkommandona i ett subshell.

( rm -rf build/ ; mkdir build && cd build && cmake .. && make all test )

Fungerar detta som det skall så kommer man att se något i denna stil.

-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Found PythonInterp: /usr/bin/python2.7
-- Looking for include files CMAKE_HAVE_PTHREAD_H
-- Looking for include files CMAKE_HAVE_PTHREAD_H - found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Configuring done
-- Generating done
-- Build files have been written to: /home/balp/localsrc/tdd-blog/build
Scanning dependencies of target gtest
[ 33%] Building CXX object googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
Linking CXX static library libgtest.a
[ 33%] Built target gtest
Scanning dependencies of target gtest_main
[ 66%] Building CXX object googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
Linking CXX static library libgtest_main.a
[ 66%] Built target gtest_main
Scanning dependencies of target testprogram
[100%] Building CXX object CMakeFiles/testprogram.dir/tests.cpp.o
Linking CXX executable testprogram
[100%] Built target testprogram
UpdateCTestConfiguration from :/home/balp/localsrc/tdd-blog/build/DartConfiguration.tcl
Parse Config file:/home/balp/localsrc/tdd-blog/build/DartConfiguration.tcl
UpdateCTestConfiguration from :/home/balp/localsrc/tdd-blog/build/DartConfiguration.tcl
Parse Config file:/home/balp/localsrc/tdd-blog/build/DartConfiguration.tcl
Test project /home/balp/localsrc/tdd-blog/build
Constructing a list of tests
Done constructing a list of tests
Checking test dependency graph...
Checking test dependency graph end
test 1
 Start 1: test
1: Test command: /home/balp/localsrc/tdd-blog/build/testprogram
1: Test timeout computed to be: 1500
1: Running main() from gtest_main.cc
1: [==========] Running 0 tests from 0 test cases.
1: [==========] 0 tests from 0 test cases ran. (0 ms total)
1: [ PASSED ] 0 tests.
1/1 Test #1: test ............................. Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.04 sec

Vi har nu ett bra startläge att börja skriva tester.

Testdriven Utveckling, Test Driven Design (TDD) på svenska

Jag hoppas att alla har hört talas om Test Driven Utveckling, eller Test-Driven Development (TDD) på engelska. TDD kommer som en del av arbetssättet man har inom Extreme Programing och är en av grundstenarna, en metod som alla efterföljande metoder tar med sig. TDD är ett sätt att bygga och designa en kod modul. Skall man ta steget upp och designa större delar av program finns det en de andra saker man bör ta hänsyn till det finns andra adapterade metoder så som Acceptance Test-Driven Design (ATDD). Dessa kommer jag att lämna för nu och titta på de grundläggande metoderna i Test Driven Utveckling.

I boken Test-Driven Development by Example, beskriver Kent Beck metoden ingående baserat på två exempel program. Boken är som en kodnings-kata i skriven form. Jag kommer att skriva ned stegen i metoden som jag ser dem.

Skapa en test

Det första steget är att skapa en test. Vi skall nu försköka skriva en test för det nya kravet vi skall implementera, målet är att testet skall en del av kravet. Vi skall titta på krav och önskemål runt funktionen. Meningen är att när vi kör testet skall det fallera på ett förutsägbart sätt. Om testen inte fallerar så är antingen funktionen redan implementerade eller så testar inte testen det den skall. I detta steget tänker vi för oss rött, vårt mål är att få våra modul tester att fallera på ett förutsägbart sätt. Skälv klart så kör vi testerna för att se till att vi faktiskt har lyckats med att skriva en test som gör vad vi tror. Att köra tester, att kompilera koden så att den inte har syntax fel. Det är sådant som dator är bra mycket bättre än oss på. Alltså bör vi låta den göra detta.

Skriv kod

Se till att koden passerar testen, skriv den enklast tänkbara koden som kan klara testet. I detta steg skall vi bryta lite mot vad som traditionellt ansetts vara god programmeringssed. Jobba mer som om de vore en film. Det är helt ok att ”fuska till det fungerar”.  Är det enklaste koden att returnera en konstant, gör det.  Vårt mål här är att enklast möjligt få testerna gröna. Skriv kod, och kör, till det blir grönt.

Refaktorera koden

Eller omstrukturera koden. Nu tar vi itu med att snygga upp koden från de två tidigare stegen. Det är viktigt att komma ihåg att vi nu ser testerna som en del an koden. Så det skall inte finnas upprepad kod någonstans. Det kan vara ok med konstanter i test koden. Tex. skall vi testa att addition mallen 3 och 2 ger fem som resultat, är det helt ok att ha alla tre konstanterna med i testen. Man bör dock inte ha dem upprepade i additions koden längre. När man inte längre kommer på rena enkla refaktoreringar går man till bara till första steget. Att skapa en ny test. När man inte kan komma på nya tester att skapa är funktionen klar.