SlideShare a Scribd company logo
1 of 47
Download to read offline
Unbearable Test Smells
Steven Mak
steven@odd-e.com
www.odd-e.com
twitter: stevenmak
1
Monday, 15 November 2010
Who am I?
2
Name: Steven Mak
Agile Coach at Odd-e
Lives in Hong Kong
Agile/Scrum, TDD Coaching
I love coding - Java, C/C++,
PHP, Perl, C#, VB, and some
weird languages
Monday, 15 November 2010
Copy and Paste Code
Long test codes are copied and pasted somewhere else with only a few
lines changing
3
Monday, 15 November 2010
DRY
Donʼt Repeat Yourself!
Donʼt Repeat Yourself!
Donʼt Repeat Yourself!
Donʼt Repeat Yourself!
Donʼt Repeat Yourself!
Donʼt Repeat Yourself!
Donʼt Repeat Yourself!
Donʼt Repeat Yourself!
4
Monday, 15 November 2010
Not knowing the fixtures
Some initialisation and clean up codes that are repeated in each tests...
5
Monday, 15 November 2010
What is fixture?
TEST_GROUP (TEST_thisObject)
{
void setup() {
}
void teardown() {
}
};
6
Monday, 15 November 2010
Duplication causing fragile
tests
Where is the duplication?
EXPECT_LOG(“ABC error”);
7
Monday, 15 November 2010
Duplication causing fragile
tests
Where is the duplication?
EXPECT_LOG(“ABC error”);
So there is a line in code that
prints this log message
8
Monday, 15 November 2010
Duplication causing fragile
tests
Put it under centralise header file:
#define ABC_ERROR_WITH_EC “ABC error”
The test will then look like:
EXPECT_LOG(ABC_ERROR);
9
Monday, 15 November 2010
Over-Optimism?
Tests that forgot to cover exceptional cases or just covered the easiest
condition
if (aaa() || bbb() || ccc() {
...
} else {
...
}
10
Monday, 15 November 2010
Tests donʼt have assertions
TEST(TEST_GROUP, TEST_THIS)
{
runThisFunctionLaLaLa();
}
11
Monday, 15 November 2010
12
What does it mean by 80% Unit
Test Coverage?
Monday, 15 November 2010
Why xUnits donʼt have
CHECK_NOT_EQUAL?
What is the problem with:
CHECK(TRUE, xxx != 3);
13
Monday, 15 November 2010
Why xUnits donʼt have
CHECK_NOT_EQUAL?
What is the problem with:
CHECK(TRUE, xxx != 3);
Is there any good reason why you
cannot know the output value?
So, tell me what it is then. 14
Monday, 15 November 2010
OK, fine, so I use CHECK with a
specific output value, what now?
What is the problem with:
CHECK(TRUE, xxx == 4);
15
Monday, 15 November 2010
OK, fine, so I use CHECK with a
specific output value, what now?
What is the problem with:
CHECK(TRUE, xxx == 4);
In most xUnits, we have
LONGS_EQUAL telling you the actual
value when it goes wrong instead of a
“false”
16
Monday, 15 November 2010
17
Do you know your xUnit harness?
Monday, 15 November 2010
Further example

 try {

 
 readConfigurationFile();

 
 assertTrue(true);

 } catch (IOException e) {

 
 assertTrue(false);

 
 e.printStackTrace();

 }
These are the places you know your
team does not know the test harness.
18
Monday, 15 November 2010
19
Some xUnit harness
Java: JUnit
.Net: NUnit
C/C++: CppUTest
PHP: PHPUnit
Monday, 15 November 2010
Whatʼs wrong?
What is the problem with:
TEST(TEST_AIH, FAIL_BAD_PARAM)
20
Monday, 15 November 2010
Names donʼt really tell
What is the problem with:
TEST(TEST_AIH, FAIL_BAD_PARAM)
Be more precise about how it
triggered the failure
21
Monday, 15 November 2010
What names tell us?
• Who
- Name of the SUT class
- Name of the method or feature being exercised
• Input
- Important characteristics of any input values
- Anything relevant about the state
• Output
- The outputs expected
- The expected post-exercise state
22
Monday, 15 November 2010
Conditional Test Logic?
// verify Vancouver is in the list
actual = null;
i = flightsFromCalgary.iterator();
while (i.hasNext()) {

 FlightDto flightDto = (FlightDto) i.next();

 if (flightDto.getFlightNumber().equals(

 
 expectedCalgaryToVan.getFlightNumber()))

 {

 
 actual = flightDto;

 
 assertEquals("Flight from Calgary to Vancouver",

 
 
 expectedCalgaryToVan,

 
 
 flightDto);

 
 break;

 }
}
23
Monday, 15 November 2010
Tests that crash 50% of the
time?!!
24
Monday, 15 November 2010
public void testFlightMileage_asKm2() throws Exception {

 // set up fixture

 // exercise constructor

 Flight newFlight = new Flight(validFlightNumber);

 // verify constructed object

 assertEquals(validFlightNumber, newFlight.number);

 assertEquals("", newFlight.airlineCode);

 assertNull(newFlight.airline);

 // set up mileage

 newFlight.setMileage(1122);

 // exercise mileage translator

 int actualKilometres = newFlight.getMileageAsKm();

 // verify results

 int expectedKilometres = 1810;

 assertEquals( expectedKilometres, actualKilometres);

 // now try it with a canceled flight

 newFlight.cancel();

 try {

 
 newFlight.getMileageAsKm();

 
 fail("Expected exception");

 } catch (InvalidRequestException e) {

 
 assertEquals( "Cannot get cancelled flight mileage",

 
 e.getMessage());

 }
}
25
Testing everything at a time
Monday, 15 November 2010
Testing everything at a time
public void testFlightMileage_asKm2() throws Exception {

 // set up fixture

 // exercise constructor

 Flight newFlight = new Flight(validFlightNumber);

 // verify constructed object

 assertEquals(validFlightNumber, newFlight.number);

 assertEquals("", newFlight.airlineCode);

 assertNull(newFlight.airline);

 // set up mileage

 newFlight.setMileage(1122);

 // exercise mileage translator

 int actualKilometres = newFlight.getMileageAsKm();

 // verify results

 int expectedKilometres = 1810;

 assertEquals( expectedKilometres, actualKilometres);

 // now try it with a canceled flight

 newFlight.cancel();

 try {

 
 newFlight.getMileageAsKm();

 
 fail("Expected exception");

 } catch (InvalidRequestException e) {

 
 assertEquals( "Cannot get cancelled flight mileage",

 
 e.getMessage());

 }
}
26
Comments as
deodorant
Monday, 15 November 2010
Testing everything at a time
public void testFlightMileage_asKm2() throws Exception {

 // set up fixture

 // exercise constructor

 Flight newFlight = new Flight(validFlightNumber);

 // verify constructed object

 assertEquals(validFlightNumber, newFlight.number);

 assertEquals("", newFlight.airlineCode);

 assertNull(newFlight.airline);

 // set up mileage

 newFlight.setMileage(1122);

 // exercise mileage translator

 int actualKilometres = newFlight.getMileageAsKm();

 // verify results

 int expectedKilometres = 1810;

 assertEquals( expectedKilometres, actualKilometres);

 // now try it with a canceled flight

 newFlight.cancel();

 try {

 
 newFlight.getMileageAsKm();

 
 fail("Expected exception");

 } catch (InvalidRequestException e) {

 
 assertEquals( "Cannot get cancelled flight mileage",

 
 e.getMessage());

 }
}
27
Duplications with
application logic?
Monday, 15 November 2010
Inappropriate dependencies
• Test setup depending on other tests files
• A test file depending on another test file
• Stub functions depending on other tests
extern int reg_ecx; // in the stub program
int reg_exc; // in SUT
28
Monday, 15 November 2010
What can we do?
29
Monday, 15 November 2010
Try: one test group per file
But why canʼt? is it because of... ?
30
Monday, 15 November 2010
Test initialisation hard to read
and shared among test groups
in the same test file
• Fixtures
• Test Data Builder
• Parameterised Creation
• make-it-easy
31
Monday, 15 November 2010
Dont forget fixtures
TEST_GROUP (TEST_thisObject)
{

 void setup()

 {

 }

 void teardown()

 {

 }
};
32
Monday, 15 November 2010
Test Data Builder
eth_data_buf
->setControl(2)
->withParameterA(3)
->build();
33
Monday, 15 November 2010
Parameterised Creation
@Before
public void setUp() throws Exception {

 alice = new Person();

 alice.setId(1L);

 alice.setFirstname("Alice");

 alice.setLastname("Adams");

 alice.setSsn("111111");

 billy = new Person();

 billy.setId(2L);

 billy.setFirstname("Billy");

 billy.setLastname("Burke");

 billy.setSsn("222222");

 clark = new Person();

 clark.setId(3L);

 clark.setFirstname("Clark");

 clark.setLastname("Cable");

 clark.setSsn("333333");

 alice.isInLoveWith(billy);
}
34
Monday, 15 November 2010
Parameterised Creation
public class ParameterizedCreationMethodExample {

 private Person alice, billy, clark;

 @Before

 public void setUp() throws Exception {

 
 clark = createPerson("Clark", "Cable");

 
 billy = createPerson("Billy", "Burke");

 
 alice = createPerson("Alice", "Adams");

 
 alice.isInLoveWith(billy);

 }

 private Person createPerson(String firstName, String lastName) {

 
 Person person = new Person();

 
 person.setFirstname(firstName);

 
 person.setLastname(lastName);

 
 person.setId(UniqueNumber.next());

 
 person.setSsn(String.valueOf(UniqueNumber.next()));

 
 return person;

 }

 @Test

 public void aliceShouldAcceptWhenProposedToByBilly()

 
 throws Exception {

 
 billy.proposeTo(alice);

 
 assertTrue(alice.isEngagedWith(billy));

 }
}
35
Monday, 15 November 2010
make-it-easy
36
http://code.google.com/p/make-it-easy/
Maker<Apple> appleWith2Leaves = an(Apple, with(2, leaves));
Maker<Apple> ripeApple = appleWith2Leaves.but(with(ripeness, 0.9));
Maker<Apple> unripeApple = appleWith2Leaves.but(with(ripeness, 0.125));
       
Apple apple1 = make(ripeApple);
Apple apple2 = make(unripeApple);
       
Banana defaultBanana = make(a(Banana));
Banana straightBanana = make(a(Banana, with(curve, 0.0)));
Banana squishyBanana = make(a(Banana, with(ripeness, 1.0)));
Monday, 15 November 2010
Try: One assertion per test
37
Monday, 15 November 2010
Customised Assertions
#define CHECK_OBJ(a,b) CHECK_OBJ(a,b, __FILE__,__FILE__)
void CHECK_OBJ(struct* yourObj, struct* myObj, const char* file, int line)
{

 if (structs are not equal) {

 
 SimpleString errorMessage = StringFromFormat(

 
 
 “My struct: %d, %p, %s”, myObj->d, myObj->p, myObj->s);

 
 FAIL_LOCATION(errorMessage.asCharString(), file, line);

 }
}
38
Monday, 15 November 2010
At least: One concept per test
39
Monday, 15 November 2010
Hamcrest
• Framework for writing declarative match criteria
40http://code.google.com/p/hamcrest/
String s = "yes we have no bananas today";
Matcher<String> containsBananas = new StringContains("bananas");
Matcher<String> containsMangoes = new StringContains("mangoes");
assertTrue(containsBananas.matches(s));
assertFalse(containsMangoes.matches(s));
assertThat(s, containsString("bananas"));
assertThat(s, not(containsString("mangoes"));
Or even better
Monday, 15 November 2010
Meaningful Assertion Messages
41
• Donʼt repeat what the built-in test framework
outputs to the console (e.g. name of the test
method)
• Donʼt repeat what the test name explains
• If you donʼt have anything good to say, you donʼt
have to say anything
• Write what should have happened, or what failed
to happen, and possibly mention when it should
have happened
Monday, 15 November 2010
Itʼs Design Smell!!!
42
Monday, 15 November 2010
Extra Constructor
43
public class LogFileMerge {

 private URL logFileA, logFileB;

 public LogFileMerge() {

 
 this(new URL("http://server1/system.log"),

 
 new URL("http://server2/system.log"));

 }

 LogFileMerge(URL a, URL b) {

 
 this.logFileA = a;

 
 this.logFileB = b;

 }
}
Monday, 15 November 2010
Test-Specific SubClass
44
public class CreditCardProcessing {

 public boolean isValid(String cardnumber) {

 
 return validationCodeMatches(cardnumber)

 
 
 && cardIsActive(cardnumber);

 }

 protected boolean validationCodeMatches(String cardnumber) {

 
 // validation logic omitted for brevity...

 }

 protected boolean cardIsActive(String cardnumber) {

 
 // access to merchant system's web service

 
 // omitted for brevity...

 }
}
Monday, 15 November 2010
Still not testable?
45
• Do you follow good design principles?
Monday, 15 November 2010
Thinking
46
Test code is not second class citizen
Good design principles apply:
• Responsibility
• Dependency
• Low Coupling
• High Cohesion
• Indirection
• Protected Variations
Watch out for organisational dysfunction!
Monday, 15 November 2010
References
• Practical TDD and ATDD for Java Developers - Lasse Koskela
• Growing OO Software, guided by tests - Steve Freeman
• xUnit Test Patterns - Gerard Meszaros
47
Steven Mak
steven@odd-e.com
www.odd-e.com
twitter: stevenmak
Monday, 15 November 2010

More Related Content

What's hot

Introduction to web programming for java and c# programmers by @drpicox
Introduction to web programming for java and c# programmers by @drpicoxIntroduction to web programming for java and c# programmers by @drpicox
Introduction to web programming for java and c# programmers by @drpicoxDavid Rodenas
 
Software Testing - Invited Lecture at UNSW Sydney
Software Testing - Invited Lecture at UNSW SydneySoftware Testing - Invited Lecture at UNSW Sydney
Software Testing - Invited Lecture at UNSW Sydneyjulien.ponge
 
ReactJS for Programmers
ReactJS for ProgrammersReactJS for Programmers
ReactJS for ProgrammersDavid Rodenas
 
Kotlin : Advanced Tricks - Ubiratan Soares
Kotlin : Advanced Tricks - Ubiratan SoaresKotlin : Advanced Tricks - Ubiratan Soares
Kotlin : Advanced Tricks - Ubiratan SoaresiMasters
 
Proxies are Awesome!
Proxies are Awesome!Proxies are Awesome!
Proxies are Awesome!Brendan Eich
 
Using Reflections and Automatic Code Generation
Using Reflections and Automatic Code GenerationUsing Reflections and Automatic Code Generation
Using Reflections and Automatic Code GenerationIvan Dolgushin
 
0003 es5 핵심 정리
0003 es5 핵심 정리0003 es5 핵심 정리
0003 es5 핵심 정리욱래 김
 
Construire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradleConstruire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradleThierry Wasylczenko
 
TDC2016SP - Trilha .NET
TDC2016SP - Trilha .NETTDC2016SP - Trilha .NET
TDC2016SP - Trilha .NETtdc-globalcode
 
JavaScript Survival Guide
JavaScript Survival GuideJavaScript Survival Guide
JavaScript Survival GuideGiordano Scalzo
 
G*におけるソフトウェアテスト・シーズンIII
G*におけるソフトウェアテスト・シーズンIIIG*におけるソフトウェアテスト・シーズンIII
G*におけるソフトウェアテスト・シーズンIIITakuma Watabiki
 
Spock Testing Framework - The Next Generation
Spock Testing Framework - The Next GenerationSpock Testing Framework - The Next Generation
Spock Testing Framework - The Next GenerationBTI360
 
Redux for ReactJS Programmers
Redux for ReactJS ProgrammersRedux for ReactJS Programmers
Redux for ReactJS ProgrammersDavid Rodenas
 
2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good Tests2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good TestsTomek Kaczanowski
 
ES3-2020-06 Test Driven Development (TDD)
ES3-2020-06 Test Driven Development (TDD)ES3-2020-06 Test Driven Development (TDD)
ES3-2020-06 Test Driven Development (TDD)David Rodenas
 
ES6 PPT FOR 2016
ES6 PPT FOR 2016ES6 PPT FOR 2016
ES6 PPT FOR 2016Manoj Kumar
 

What's hot (20)

Introduction to web programming for java and c# programmers by @drpicox
Introduction to web programming for java and c# programmers by @drpicoxIntroduction to web programming for java and c# programmers by @drpicox
Introduction to web programming for java and c# programmers by @drpicox
 
Software Testing - Invited Lecture at UNSW Sydney
Software Testing - Invited Lecture at UNSW SydneySoftware Testing - Invited Lecture at UNSW Sydney
Software Testing - Invited Lecture at UNSW Sydney
 
ReactJS for Programmers
ReactJS for ProgrammersReactJS for Programmers
ReactJS for Programmers
 
Kotlin : Advanced Tricks - Ubiratan Soares
Kotlin : Advanced Tricks - Ubiratan SoaresKotlin : Advanced Tricks - Ubiratan Soares
Kotlin : Advanced Tricks - Ubiratan Soares
 
Proxies are Awesome!
Proxies are Awesome!Proxies are Awesome!
Proxies are Awesome!
 
Using Reflections and Automatic Code Generation
Using Reflections and Automatic Code GenerationUsing Reflections and Automatic Code Generation
Using Reflections and Automatic Code Generation
 
EcmaScript 6
EcmaScript 6 EcmaScript 6
EcmaScript 6
 
0003 es5 핵심 정리
0003 es5 핵심 정리0003 es5 핵심 정리
0003 es5 핵심 정리
 
Easy Button
Easy ButtonEasy Button
Easy Button
 
Construire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradleConstruire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradle
 
TDC2016SP - Trilha .NET
TDC2016SP - Trilha .NETTDC2016SP - Trilha .NET
TDC2016SP - Trilha .NET
 
JavaScript Survival Guide
JavaScript Survival GuideJavaScript Survival Guide
JavaScript Survival Guide
 
G*におけるソフトウェアテスト・シーズンIII
G*におけるソフトウェアテスト・シーズンIIIG*におけるソフトウェアテスト・シーズンIII
G*におけるソフトウェアテスト・シーズンIII
 
Spock Testing Framework - The Next Generation
Spock Testing Framework - The Next GenerationSpock Testing Framework - The Next Generation
Spock Testing Framework - The Next Generation
 
Redux for ReactJS Programmers
Redux for ReactJS ProgrammersRedux for ReactJS Programmers
Redux for ReactJS Programmers
 
2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good Tests2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good Tests
 
#JavaFX.forReal() - ElsassJUG
#JavaFX.forReal() - ElsassJUG#JavaFX.forReal() - ElsassJUG
#JavaFX.forReal() - ElsassJUG
 
ES6 in Real Life
ES6 in Real LifeES6 in Real Life
ES6 in Real Life
 
ES3-2020-06 Test Driven Development (TDD)
ES3-2020-06 Test Driven Development (TDD)ES3-2020-06 Test Driven Development (TDD)
ES3-2020-06 Test Driven Development (TDD)
 
ES6 PPT FOR 2016
ES6 PPT FOR 2016ES6 PPT FOR 2016
ES6 PPT FOR 2016
 

Similar to Unbearable Test Code Smell

Tests unitaires mock_kesako_20130516
Tests unitaires mock_kesako_20130516Tests unitaires mock_kesako_20130516
Tests unitaires mock_kesako_20130516SOAT
 
33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good Tests33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good TestsTomek Kaczanowski
 
Conf soat tests_unitaires_Mockito_jUnit_170113
Conf soat tests_unitaires_Mockito_jUnit_170113Conf soat tests_unitaires_Mockito_jUnit_170113
Conf soat tests_unitaires_Mockito_jUnit_170113SOAT
 
Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017 Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017 Codemotion
 
Developer Test - Things to Know
Developer Test - Things to KnowDeveloper Test - Things to Know
Developer Test - Things to Knowvilniusjug
 
Dat testing - An introduction to Java and Android Testing
Dat testing - An introduction to Java and Android TestingDat testing - An introduction to Java and Android Testing
Dat testing - An introduction to Java and Android TestingSaúl Díaz González
 
Imagine a world without mocks
Imagine a world without mocksImagine a world without mocks
Imagine a world without mockskenbot
 
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022Luciano Mammino
 
Workshop 5: JavaScript testing
Workshop 5: JavaScript testingWorkshop 5: JavaScript testing
Workshop 5: JavaScript testingVisual Engineering
 
Improving the java type system
Improving the java type systemImproving the java type system
Improving the java type systemJoão Loff
 
Javascript: the important bits
Javascript: the important bitsJavascript: the important bits
Javascript: the important bitsChris Saylor
 
Whats new in_csharp4
Whats new in_csharp4Whats new in_csharp4
Whats new in_csharp4Abed Bukhari
 
Cool JVM Tools to Help You Test
Cool JVM Tools to Help You TestCool JVM Tools to Help You Test
Cool JVM Tools to Help You TestSchalk Cronjé
 
Kotlin Overview (PT-BR)
Kotlin Overview (PT-BR)Kotlin Overview (PT-BR)
Kotlin Overview (PT-BR)ThomasHorta
 

Similar to Unbearable Test Code Smell (20)

Tests unitaires mock_kesako_20130516
Tests unitaires mock_kesako_20130516Tests unitaires mock_kesako_20130516
Tests unitaires mock_kesako_20130516
 
33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good Tests33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good Tests
 
Conf soat tests_unitaires_Mockito_jUnit_170113
Conf soat tests_unitaires_Mockito_jUnit_170113Conf soat tests_unitaires_Mockito_jUnit_170113
Conf soat tests_unitaires_Mockito_jUnit_170113
 
Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017 Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017
Davide Cerbo - Kotlin: forse è la volta buona - Codemotion Milan 2017
 
A Test of Strength
A Test of StrengthA Test of Strength
A Test of Strength
 
Developer Test - Things to Know
Developer Test - Things to KnowDeveloper Test - Things to Know
Developer Test - Things to Know
 
Dat testing - An introduction to Java and Android Testing
Dat testing - An introduction to Java and Android TestingDat testing - An introduction to Java and Android Testing
Dat testing - An introduction to Java and Android Testing
 
Imagine a world without mocks
Imagine a world without mocksImagine a world without mocks
Imagine a world without mocks
 
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
 
Kotlin Generation
Kotlin GenerationKotlin Generation
Kotlin Generation
 
Workshop 5: JavaScript testing
Workshop 5: JavaScript testingWorkshop 5: JavaScript testing
Workshop 5: JavaScript testing
 
Improving the java type system
Improving the java type systemImproving the java type system
Improving the java type system
 
Javascript: the important bits
Javascript: the important bitsJavascript: the important bits
Javascript: the important bits
 
Whats new in_csharp4
Whats new in_csharp4Whats new in_csharp4
Whats new in_csharp4
 
Writing Good Tests
Writing Good TestsWriting Good Tests
Writing Good Tests
 
Kotlin
KotlinKotlin
Kotlin
 
C# labprograms
C# labprogramsC# labprograms
C# labprograms
 
Cool JVM Tools to Help You Test
Cool JVM Tools to Help You TestCool JVM Tools to Help You Test
Cool JVM Tools to Help You Test
 
Kotlin Overview (PT-BR)
Kotlin Overview (PT-BR)Kotlin Overview (PT-BR)
Kotlin Overview (PT-BR)
 
Google guava
Google guavaGoogle guava
Google guava
 

More from Steven Mak

Continuous Security Testing
Continuous Security TestingContinuous Security Testing
Continuous Security TestingSteven Mak
 
Quality comes free with open source testing tools
Quality comes free with open source testing toolsQuality comes free with open source testing tools
Quality comes free with open source testing toolsSteven Mak
 
Adopting technical practices 2013
Adopting technical practices 2013Adopting technical practices 2013
Adopting technical practices 2013Steven Mak
 
100 doors kata solution
100 doors kata solution100 doors kata solution
100 doors kata solutionSteven Mak
 
Bossless companies
Bossless companiesBossless companies
Bossless companiesSteven Mak
 
Is this how you hate unit testing?
Is this how you hate unit testing?Is this how you hate unit testing?
Is this how you hate unit testing?Steven Mak
 
Driving Quality with TDD
Driving Quality with TDDDriving Quality with TDD
Driving Quality with TDDSteven Mak
 
Sustainable TDD
Sustainable TDDSustainable TDD
Sustainable TDDSteven Mak
 
Introduction to Acceptance Test Driven Development
Introduction to Acceptance Test Driven DevelopmentIntroduction to Acceptance Test Driven Development
Introduction to Acceptance Test Driven DevelopmentSteven Mak
 
Essential practices and thinking tools for Agile Adoption
Essential practices and thinking tools for Agile AdoptionEssential practices and thinking tools for Agile Adoption
Essential practices and thinking tools for Agile AdoptionSteven Mak
 
ATDD in Practice
ATDD in PracticeATDD in Practice
ATDD in PracticeSteven Mak
 

More from Steven Mak (11)

Continuous Security Testing
Continuous Security TestingContinuous Security Testing
Continuous Security Testing
 
Quality comes free with open source testing tools
Quality comes free with open source testing toolsQuality comes free with open source testing tools
Quality comes free with open source testing tools
 
Adopting technical practices 2013
Adopting technical practices 2013Adopting technical practices 2013
Adopting technical practices 2013
 
100 doors kata solution
100 doors kata solution100 doors kata solution
100 doors kata solution
 
Bossless companies
Bossless companiesBossless companies
Bossless companies
 
Is this how you hate unit testing?
Is this how you hate unit testing?Is this how you hate unit testing?
Is this how you hate unit testing?
 
Driving Quality with TDD
Driving Quality with TDDDriving Quality with TDD
Driving Quality with TDD
 
Sustainable TDD
Sustainable TDDSustainable TDD
Sustainable TDD
 
Introduction to Acceptance Test Driven Development
Introduction to Acceptance Test Driven DevelopmentIntroduction to Acceptance Test Driven Development
Introduction to Acceptance Test Driven Development
 
Essential practices and thinking tools for Agile Adoption
Essential practices and thinking tools for Agile AdoptionEssential practices and thinking tools for Agile Adoption
Essential practices and thinking tools for Agile Adoption
 
ATDD in Practice
ATDD in PracticeATDD in Practice
ATDD in Practice
 

Recently uploaded

guest bathroom white and blue ssssssssss
guest bathroom white and blue ssssssssssguest bathroom white and blue ssssssssss
guest bathroom white and blue ssssssssssNadaMohammed714321
 
General Knowledge Quiz Game C++ CODE.pptx
General Knowledge Quiz Game C++ CODE.pptxGeneral Knowledge Quiz Game C++ CODE.pptx
General Knowledge Quiz Game C++ CODE.pptxmarckustrevion
 
Top 10 Modern Web Design Trends for 2025
Top 10 Modern Web Design Trends for 2025Top 10 Modern Web Design Trends for 2025
Top 10 Modern Web Design Trends for 2025Rndexperts
 
Pearl Disrtrict urban analyusis study pptx
Pearl Disrtrict urban analyusis study pptxPearl Disrtrict urban analyusis study pptx
Pearl Disrtrict urban analyusis study pptxDanielTamiru4
 
Map of St. Louis Parks
Map of St. Louis Parks                              Map of St. Louis Parks
Map of St. Louis Parks CharlottePulte
 
The spirit of digital place - game worlds and architectural phenomenology
The spirit of digital place - game worlds and architectural phenomenologyThe spirit of digital place - game worlds and architectural phenomenology
The spirit of digital place - game worlds and architectural phenomenologyChristopher Totten
 
Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...
Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...
Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...Associazione Digital Days
 
Interior Design for Office a cura di RMG Project Studio
Interior Design for Office a cura di RMG Project StudioInterior Design for Office a cura di RMG Project Studio
Interior Design for Office a cura di RMG Project StudioRMG Project Studio
 
simpson-lee_house_dt20ajshsjsjsjsjj15.pdf
simpson-lee_house_dt20ajshsjsjsjsjj15.pdfsimpson-lee_house_dt20ajshsjsjsjsjj15.pdf
simpson-lee_house_dt20ajshsjsjsjsjj15.pdfLucyBonelli
 
Karim apartment ideas 01 ppppppppppppppp
Karim apartment ideas 01 pppppppppppppppKarim apartment ideas 01 ppppppppppppppp
Karim apartment ideas 01 pppppppppppppppNadaMohammed714321
 
Iconic Global Solution - web design, Digital Marketing services
Iconic Global Solution - web design, Digital Marketing servicesIconic Global Solution - web design, Digital Marketing services
Iconic Global Solution - web design, Digital Marketing servicesIconic global solution
 
Niintendo Wii Presentation Template.pptx
Niintendo Wii Presentation Template.pptxNiintendo Wii Presentation Template.pptx
Niintendo Wii Presentation Template.pptxKevinYaelJimnezSanti
 
guest bathroom white and bluesssssssssss
guest bathroom white and bluesssssssssssguest bathroom white and bluesssssssssss
guest bathroom white and bluesssssssssssNadaMohammed714321
 
Making and Unmaking of Chandigarh - A City of Two Plans2-4-24.ppt
Making and Unmaking of Chandigarh - A City of Two Plans2-4-24.pptMaking and Unmaking of Chandigarh - A City of Two Plans2-4-24.ppt
Making and Unmaking of Chandigarh - A City of Two Plans2-4-24.pptJIT KUMAR GUPTA
 
怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道
怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道
怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道yrolcks
 
Piece by Piece Magazine
Piece by Piece Magazine                      Piece by Piece Magazine
Piece by Piece Magazine CharlottePulte
 
10 must-have Chrome extensions for designers
10 must-have Chrome extensions for designers10 must-have Chrome extensions for designers
10 must-have Chrome extensions for designersPixeldarts
 
Karim apartment ideas 02 ppppppppppppppp
Karim apartment ideas 02 pppppppppppppppKarim apartment ideas 02 ppppppppppppppp
Karim apartment ideas 02 pppppppppppppppNadaMohammed714321
 
cda.pptx critical discourse analysis ppt
cda.pptx critical discourse analysis pptcda.pptx critical discourse analysis ppt
cda.pptx critical discourse analysis pptMaryamAfzal41
 
How to Empower the future of UX Design with Gen AI
How to Empower the future of UX Design with Gen AIHow to Empower the future of UX Design with Gen AI
How to Empower the future of UX Design with Gen AIyuj
 

Recently uploaded (20)

guest bathroom white and blue ssssssssss
guest bathroom white and blue ssssssssssguest bathroom white and blue ssssssssss
guest bathroom white and blue ssssssssss
 
General Knowledge Quiz Game C++ CODE.pptx
General Knowledge Quiz Game C++ CODE.pptxGeneral Knowledge Quiz Game C++ CODE.pptx
General Knowledge Quiz Game C++ CODE.pptx
 
Top 10 Modern Web Design Trends for 2025
Top 10 Modern Web Design Trends for 2025Top 10 Modern Web Design Trends for 2025
Top 10 Modern Web Design Trends for 2025
 
Pearl Disrtrict urban analyusis study pptx
Pearl Disrtrict urban analyusis study pptxPearl Disrtrict urban analyusis study pptx
Pearl Disrtrict urban analyusis study pptx
 
Map of St. Louis Parks
Map of St. Louis Parks                              Map of St. Louis Parks
Map of St. Louis Parks
 
The spirit of digital place - game worlds and architectural phenomenology
The spirit of digital place - game worlds and architectural phenomenologyThe spirit of digital place - game worlds and architectural phenomenology
The spirit of digital place - game worlds and architectural phenomenology
 
Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...
Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...
Giulio Michelon, Founder di @Belka – “Oltre le Stime: Sviluppare una Mentalit...
 
Interior Design for Office a cura di RMG Project Studio
Interior Design for Office a cura di RMG Project StudioInterior Design for Office a cura di RMG Project Studio
Interior Design for Office a cura di RMG Project Studio
 
simpson-lee_house_dt20ajshsjsjsjsjj15.pdf
simpson-lee_house_dt20ajshsjsjsjsjj15.pdfsimpson-lee_house_dt20ajshsjsjsjsjj15.pdf
simpson-lee_house_dt20ajshsjsjsjsjj15.pdf
 
Karim apartment ideas 01 ppppppppppppppp
Karim apartment ideas 01 pppppppppppppppKarim apartment ideas 01 ppppppppppppppp
Karim apartment ideas 01 ppppppppppppppp
 
Iconic Global Solution - web design, Digital Marketing services
Iconic Global Solution - web design, Digital Marketing servicesIconic Global Solution - web design, Digital Marketing services
Iconic Global Solution - web design, Digital Marketing services
 
Niintendo Wii Presentation Template.pptx
Niintendo Wii Presentation Template.pptxNiintendo Wii Presentation Template.pptx
Niintendo Wii Presentation Template.pptx
 
guest bathroom white and bluesssssssssss
guest bathroom white and bluesssssssssssguest bathroom white and bluesssssssssss
guest bathroom white and bluesssssssssss
 
Making and Unmaking of Chandigarh - A City of Two Plans2-4-24.ppt
Making and Unmaking of Chandigarh - A City of Two Plans2-4-24.pptMaking and Unmaking of Chandigarh - A City of Two Plans2-4-24.ppt
Making and Unmaking of Chandigarh - A City of Two Plans2-4-24.ppt
 
怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道
怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道
怎么办理英国Newcastle毕业证纽卡斯尔大学学位证书一手渠道
 
Piece by Piece Magazine
Piece by Piece Magazine                      Piece by Piece Magazine
Piece by Piece Magazine
 
10 must-have Chrome extensions for designers
10 must-have Chrome extensions for designers10 must-have Chrome extensions for designers
10 must-have Chrome extensions for designers
 
Karim apartment ideas 02 ppppppppppppppp
Karim apartment ideas 02 pppppppppppppppKarim apartment ideas 02 ppppppppppppppp
Karim apartment ideas 02 ppppppppppppppp
 
cda.pptx critical discourse analysis ppt
cda.pptx critical discourse analysis pptcda.pptx critical discourse analysis ppt
cda.pptx critical discourse analysis ppt
 
How to Empower the future of UX Design with Gen AI
How to Empower the future of UX Design with Gen AIHow to Empower the future of UX Design with Gen AI
How to Empower the future of UX Design with Gen AI
 

Unbearable Test Code Smell

  • 1. Unbearable Test Smells Steven Mak steven@odd-e.com www.odd-e.com twitter: stevenmak 1 Monday, 15 November 2010
  • 2. Who am I? 2 Name: Steven Mak Agile Coach at Odd-e Lives in Hong Kong Agile/Scrum, TDD Coaching I love coding - Java, C/C++, PHP, Perl, C#, VB, and some weird languages Monday, 15 November 2010
  • 3. Copy and Paste Code Long test codes are copied and pasted somewhere else with only a few lines changing 3 Monday, 15 November 2010
  • 4. DRY Donʼt Repeat Yourself! Donʼt Repeat Yourself! Donʼt Repeat Yourself! Donʼt Repeat Yourself! Donʼt Repeat Yourself! Donʼt Repeat Yourself! Donʼt Repeat Yourself! Donʼt Repeat Yourself! 4 Monday, 15 November 2010
  • 5. Not knowing the fixtures Some initialisation and clean up codes that are repeated in each tests... 5 Monday, 15 November 2010
  • 6. What is fixture? TEST_GROUP (TEST_thisObject) { void setup() { } void teardown() { } }; 6 Monday, 15 November 2010
  • 7. Duplication causing fragile tests Where is the duplication? EXPECT_LOG(“ABC error”); 7 Monday, 15 November 2010
  • 8. Duplication causing fragile tests Where is the duplication? EXPECT_LOG(“ABC error”); So there is a line in code that prints this log message 8 Monday, 15 November 2010
  • 9. Duplication causing fragile tests Put it under centralise header file: #define ABC_ERROR_WITH_EC “ABC error” The test will then look like: EXPECT_LOG(ABC_ERROR); 9 Monday, 15 November 2010
  • 10. Over-Optimism? Tests that forgot to cover exceptional cases or just covered the easiest condition if (aaa() || bbb() || ccc() { ... } else { ... } 10 Monday, 15 November 2010
  • 11. Tests donʼt have assertions TEST(TEST_GROUP, TEST_THIS) { runThisFunctionLaLaLa(); } 11 Monday, 15 November 2010
  • 12. 12 What does it mean by 80% Unit Test Coverage? Monday, 15 November 2010
  • 13. Why xUnits donʼt have CHECK_NOT_EQUAL? What is the problem with: CHECK(TRUE, xxx != 3); 13 Monday, 15 November 2010
  • 14. Why xUnits donʼt have CHECK_NOT_EQUAL? What is the problem with: CHECK(TRUE, xxx != 3); Is there any good reason why you cannot know the output value? So, tell me what it is then. 14 Monday, 15 November 2010
  • 15. OK, fine, so I use CHECK with a specific output value, what now? What is the problem with: CHECK(TRUE, xxx == 4); 15 Monday, 15 November 2010
  • 16. OK, fine, so I use CHECK with a specific output value, what now? What is the problem with: CHECK(TRUE, xxx == 4); In most xUnits, we have LONGS_EQUAL telling you the actual value when it goes wrong instead of a “false” 16 Monday, 15 November 2010
  • 17. 17 Do you know your xUnit harness? Monday, 15 November 2010
  • 18. Further example try { readConfigurationFile(); assertTrue(true); } catch (IOException e) { assertTrue(false); e.printStackTrace(); } These are the places you know your team does not know the test harness. 18 Monday, 15 November 2010
  • 19. 19 Some xUnit harness Java: JUnit .Net: NUnit C/C++: CppUTest PHP: PHPUnit Monday, 15 November 2010
  • 20. Whatʼs wrong? What is the problem with: TEST(TEST_AIH, FAIL_BAD_PARAM) 20 Monday, 15 November 2010
  • 21. Names donʼt really tell What is the problem with: TEST(TEST_AIH, FAIL_BAD_PARAM) Be more precise about how it triggered the failure 21 Monday, 15 November 2010
  • 22. What names tell us? • Who - Name of the SUT class - Name of the method or feature being exercised • Input - Important characteristics of any input values - Anything relevant about the state • Output - The outputs expected - The expected post-exercise state 22 Monday, 15 November 2010
  • 23. Conditional Test Logic? // verify Vancouver is in the list actual = null; i = flightsFromCalgary.iterator(); while (i.hasNext()) { FlightDto flightDto = (FlightDto) i.next(); if (flightDto.getFlightNumber().equals( expectedCalgaryToVan.getFlightNumber())) { actual = flightDto; assertEquals("Flight from Calgary to Vancouver", expectedCalgaryToVan, flightDto); break; } } 23 Monday, 15 November 2010
  • 24. Tests that crash 50% of the time?!! 24 Monday, 15 November 2010
  • 25. public void testFlightMileage_asKm2() throws Exception { // set up fixture // exercise constructor Flight newFlight = new Flight(validFlightNumber); // verify constructed object assertEquals(validFlightNumber, newFlight.number); assertEquals("", newFlight.airlineCode); assertNull(newFlight.airline); // set up mileage newFlight.setMileage(1122); // exercise mileage translator int actualKilometres = newFlight.getMileageAsKm(); // verify results int expectedKilometres = 1810; assertEquals( expectedKilometres, actualKilometres); // now try it with a canceled flight newFlight.cancel(); try { newFlight.getMileageAsKm(); fail("Expected exception"); } catch (InvalidRequestException e) { assertEquals( "Cannot get cancelled flight mileage", e.getMessage()); } } 25 Testing everything at a time Monday, 15 November 2010
  • 26. Testing everything at a time public void testFlightMileage_asKm2() throws Exception { // set up fixture // exercise constructor Flight newFlight = new Flight(validFlightNumber); // verify constructed object assertEquals(validFlightNumber, newFlight.number); assertEquals("", newFlight.airlineCode); assertNull(newFlight.airline); // set up mileage newFlight.setMileage(1122); // exercise mileage translator int actualKilometres = newFlight.getMileageAsKm(); // verify results int expectedKilometres = 1810; assertEquals( expectedKilometres, actualKilometres); // now try it with a canceled flight newFlight.cancel(); try { newFlight.getMileageAsKm(); fail("Expected exception"); } catch (InvalidRequestException e) { assertEquals( "Cannot get cancelled flight mileage", e.getMessage()); } } 26 Comments as deodorant Monday, 15 November 2010
  • 27. Testing everything at a time public void testFlightMileage_asKm2() throws Exception { // set up fixture // exercise constructor Flight newFlight = new Flight(validFlightNumber); // verify constructed object assertEquals(validFlightNumber, newFlight.number); assertEquals("", newFlight.airlineCode); assertNull(newFlight.airline); // set up mileage newFlight.setMileage(1122); // exercise mileage translator int actualKilometres = newFlight.getMileageAsKm(); // verify results int expectedKilometres = 1810; assertEquals( expectedKilometres, actualKilometres); // now try it with a canceled flight newFlight.cancel(); try { newFlight.getMileageAsKm(); fail("Expected exception"); } catch (InvalidRequestException e) { assertEquals( "Cannot get cancelled flight mileage", e.getMessage()); } } 27 Duplications with application logic? Monday, 15 November 2010
  • 28. Inappropriate dependencies • Test setup depending on other tests files • A test file depending on another test file • Stub functions depending on other tests extern int reg_ecx; // in the stub program int reg_exc; // in SUT 28 Monday, 15 November 2010
  • 29. What can we do? 29 Monday, 15 November 2010
  • 30. Try: one test group per file But why canʼt? is it because of... ? 30 Monday, 15 November 2010
  • 31. Test initialisation hard to read and shared among test groups in the same test file • Fixtures • Test Data Builder • Parameterised Creation • make-it-easy 31 Monday, 15 November 2010
  • 32. Dont forget fixtures TEST_GROUP (TEST_thisObject) { void setup() { } void teardown() { } }; 32 Monday, 15 November 2010
  • 34. Parameterised Creation @Before public void setUp() throws Exception { alice = new Person(); alice.setId(1L); alice.setFirstname("Alice"); alice.setLastname("Adams"); alice.setSsn("111111"); billy = new Person(); billy.setId(2L); billy.setFirstname("Billy"); billy.setLastname("Burke"); billy.setSsn("222222"); clark = new Person(); clark.setId(3L); clark.setFirstname("Clark"); clark.setLastname("Cable"); clark.setSsn("333333"); alice.isInLoveWith(billy); } 34 Monday, 15 November 2010
  • 35. Parameterised Creation public class ParameterizedCreationMethodExample { private Person alice, billy, clark; @Before public void setUp() throws Exception { clark = createPerson("Clark", "Cable"); billy = createPerson("Billy", "Burke"); alice = createPerson("Alice", "Adams"); alice.isInLoveWith(billy); } private Person createPerson(String firstName, String lastName) { Person person = new Person(); person.setFirstname(firstName); person.setLastname(lastName); person.setId(UniqueNumber.next()); person.setSsn(String.valueOf(UniqueNumber.next())); return person; } @Test public void aliceShouldAcceptWhenProposedToByBilly() throws Exception { billy.proposeTo(alice); assertTrue(alice.isEngagedWith(billy)); } } 35 Monday, 15 November 2010
  • 36. make-it-easy 36 http://code.google.com/p/make-it-easy/ Maker<Apple> appleWith2Leaves = an(Apple, with(2, leaves)); Maker<Apple> ripeApple = appleWith2Leaves.but(with(ripeness, 0.9)); Maker<Apple> unripeApple = appleWith2Leaves.but(with(ripeness, 0.125));         Apple apple1 = make(ripeApple); Apple apple2 = make(unripeApple);         Banana defaultBanana = make(a(Banana)); Banana straightBanana = make(a(Banana, with(curve, 0.0))); Banana squishyBanana = make(a(Banana, with(ripeness, 1.0))); Monday, 15 November 2010
  • 37. Try: One assertion per test 37 Monday, 15 November 2010
  • 38. Customised Assertions #define CHECK_OBJ(a,b) CHECK_OBJ(a,b, __FILE__,__FILE__) void CHECK_OBJ(struct* yourObj, struct* myObj, const char* file, int line) { if (structs are not equal) { SimpleString errorMessage = StringFromFormat( “My struct: %d, %p, %s”, myObj->d, myObj->p, myObj->s); FAIL_LOCATION(errorMessage.asCharString(), file, line); } } 38 Monday, 15 November 2010
  • 39. At least: One concept per test 39 Monday, 15 November 2010
  • 40. Hamcrest • Framework for writing declarative match criteria 40http://code.google.com/p/hamcrest/ String s = "yes we have no bananas today"; Matcher<String> containsBananas = new StringContains("bananas"); Matcher<String> containsMangoes = new StringContains("mangoes"); assertTrue(containsBananas.matches(s)); assertFalse(containsMangoes.matches(s)); assertThat(s, containsString("bananas")); assertThat(s, not(containsString("mangoes")); Or even better Monday, 15 November 2010
  • 41. Meaningful Assertion Messages 41 • Donʼt repeat what the built-in test framework outputs to the console (e.g. name of the test method) • Donʼt repeat what the test name explains • If you donʼt have anything good to say, you donʼt have to say anything • Write what should have happened, or what failed to happen, and possibly mention when it should have happened Monday, 15 November 2010
  • 43. Extra Constructor 43 public class LogFileMerge { private URL logFileA, logFileB; public LogFileMerge() { this(new URL("http://server1/system.log"), new URL("http://server2/system.log")); } LogFileMerge(URL a, URL b) { this.logFileA = a; this.logFileB = b; } } Monday, 15 November 2010
  • 44. Test-Specific SubClass 44 public class CreditCardProcessing { public boolean isValid(String cardnumber) { return validationCodeMatches(cardnumber) && cardIsActive(cardnumber); } protected boolean validationCodeMatches(String cardnumber) { // validation logic omitted for brevity... } protected boolean cardIsActive(String cardnumber) { // access to merchant system's web service // omitted for brevity... } } Monday, 15 November 2010
  • 45. Still not testable? 45 • Do you follow good design principles? Monday, 15 November 2010
  • 46. Thinking 46 Test code is not second class citizen Good design principles apply: • Responsibility • Dependency • Low Coupling • High Cohesion • Indirection • Protected Variations Watch out for organisational dysfunction! Monday, 15 November 2010
  • 47. References • Practical TDD and ATDD for Java Developers - Lasse Koskela • Growing OO Software, guided by tests - Steve Freeman • xUnit Test Patterns - Gerard Meszaros 47 Steven Mak steven@odd-e.com www.odd-e.com twitter: stevenmak Monday, 15 November 2010