4. WHY TESTING?
Lots of reasons! Here are a select few
Verify functionality of
application
Catch bugs and prevent them from
coming back
Serve as documentation for
maintainability purposes
5. BUT WHAT ABOUT…?
? How do we make sure we have full test coverage?
? How can we be confident all cases are covered?
? How can we prevent bugs from happening?
29. RED – GREEN - REFACTOR
New Test New Test New Test
Red
GreenRefactor
Red
GreenRefactor
Red
GreenRefactor
30. EXAMPLE
You are given strings of different lengths. If
the number of vowels are more than 30%
of the string length then insert ‘mommy’ for
each continuous set of vowels.
his → hmommys
hear → hmommyr
✗ hear → hmommymommyr
40. TEST
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MommifierTest {
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {
String word = "str";
Mommifier mommifier = new Mommifier();
String mommifiedWord = mommifier.mommify(word);
assertEquals("str", mommifiedWord);
}
}
41. TEST
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MommifierTest {
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {
String word = "a";
Mommifier mommifier = new Mommifier();
String mommifiedWord = mommifier.mommify(word);
assertEquals("mommy", mommifiedWord);
}
}
42. TEST
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MommifierTest {
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {
String word = "a";
Mommifier mommifier = new Mommifier();
String mommifiedWord = mommifier.mommify(word);
assertEquals("mommy", mommifiedWord);
}
}
43. SOURCE
public class Mommifier {
public String mommify(String word) {
for (char character : word.toCharArray()) {
if (character == 'a'
|| character == 'e'
|| character == 'i'
|| character == 'o'
|| character == 'u'){
return "mommy";
}
}
return word;
}
}
44. TEST
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MommifierTest {
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {
String word = "a";
Mommifier mommifier = new Mommifier();
String mommifiedWord = mommifier.mommify(word);
assertEquals("mommy", mommifiedWord);
}
}
45. SOURCE
public class Mommifier {
public String mommify(String word) {
for (char character : word.toCharArray()) {
if (character == 'a'
|| character == 'e'
|| character == 'i'
|| character == 'o'
|| character == 'u'){
return "mommy";
}
}
return word;
}
}
46. SOURCE
public class Mommifier {
private static final String VOWELS = "aeiou";
private static final String REPLACEMENT_WORD = "mommy";
public String mommify(String word) {
for (Character character : word.toCharArray()) {
if (isAVowel(character)) {
return REPLACEMENT_WORD;
}
}
return word;
}
private boolean isAVowel(Character character) {
return VOWELS.contains(character.toString());
}
}
47. TEST
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MommifierTest {
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {
String word = "a";
Mommifier mommifier = new Mommifier();
String mommifiedWord = mommifier.mommify(word);
assertEquals("mommy", mommifiedWord);
}
}
48. TEST
private Mommifier mommifier;
@Before
public void setUp() {
mommifier = new Mommifier();
}
@Test
public void shouldNotMommifyEmptyString() {
assertEquals("", mommifier.mommify(""));
}
@Test
public void shouldNotMommifyWordsWithNoVowels() {
assertEquals("str", mommifier.mommify("str"));
}
@Test
public void shouldMommifyVowel() {
assertEquals("mommy", mommifier.mommify("a"));
}
49. TEST
private Mommifier mommifier;
@Before
public void setUp() {
mommifier = new Mommifier();
}
@Test
public void shouldNotMommifyEmptyString() {
assertEquals("", mommifier.mommify(""));
}
@Test
public void shouldNotMommifyWordsWithNoVowels() {
assertEquals("str", mommifier.mommify("str"));
}
@Test
public void shouldMommifyVowel() {
assertEquals("mommy", mommifier.mommify("a"));
}
50. TEST
import ...;
public class MommifierTest {
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {
assertEquals("blmommy", mommifier.mommify("bla"));
}
}
51. TEST
import ...;
public class MommifierTest {
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {
assertEquals("blmommy", mommifier.mommify("bla"));
}
}
52. SOURCE
public class Mommifier {
private static final String VOWELS = "aeiou";
private static final String REPLACEMENT_WORD = "mommy";
public String mommify(String word) {
String mommifiedWord = "";
for (Character character : word.toCharArray()) {
if (isAVowel(character)) {
mommifiedWord += REPLACEMENT_WORD;
} else {
mommifiedWord += character;
}
}
return mommifiedWord;
}
private boolean isAVowel(Character character) {
return VOWELS.contains(character.toString());
}
}
53. TEST
import ...;
public class MommifierTest {
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {
assertEquals("blmommy", mommifier.mommify("bla"));
}
}
54. TEST
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {
assertEquals("blah", mommifier.mommify("blah"));
}
55. TEST
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {
assertEquals("blah", mommifier.mommify("blah"));
}
56. SOURCE
...
private static final double THRESHOLD = 0.3;
public String mommify(String word) {
int vowelCount = 0;
String mommifiedWord = "";
for (Character character : word.toCharArray()) {
if (isAVowel(character)) {
mommifiedWord += REPLACEMENT_WORD;
vowelCount++;
} else {
mommifiedWord += character;
}
}
if((double)vowelCount/word.toCharArray().length
< THRESHOLD){
return word;
}
return mommifiedWord;
}
...
57. TEST
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {
assertEquals("blah", mommifier.mommify("blah"));
}
63. TEST
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {
assertEquals("blmommyh”, mommifier.mommify("blaah"));
}
64. TEST
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {...}
@Test
public void shouldMommifyMultipleSetsOfVowels() {
assertEquals("blmommyhmommy”, mommifier.mommify("blaha"));
}
65. TEST
private Mommifier mommifier;
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {...}
@Test
public void shouldMommifyMultipleSetsOfVowels() {
assertEquals("blmommyhmommy”, mommifier.mommify("blaha"));
}
66. TEST
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {...}
@Test
public void shouldMommifyMultipleSetsOfVowels() {...}
@Test
public void shouldMommifyCapitalVowels() {
assertEquals("BLmommy", mommifier.mommify("BLA"));
}
67. TEST
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {...}
@Test
public void shouldMommifyMultipleSetsOfVowels() {...}
@Test
public void shouldMommifyCapitalVowels() {
assertEquals("BLmommy", mommifier.mommify("BLA"));
}
68. SOURCE
public class Mommifier {
private static final String VOWELS = "aeiou";
private static final String REPLACEMENT_WORD = "mommy";
private static final double THRESHOLD = 0.3;
public String mommify(String word) {...}
private String replaceVowels(String word) {...}
private boolean vowelCountLessThanThreshold(String word){...}
private boolean isVowel(Character character) {
Character lowerCase = Character.toLowerCase(character);
return VOWELS.contains(lowerCase.toString());
}
}
69. TEST
@Before
public void setUp() {...}
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {...}
@Test
public void shouldMommifyMultipleSetsOfVowels() {...}
@Test
public void shouldMommifyCapitalVowels() {
assertEquals("BLmommy", mommifier.mommify("BLA"));
}
70. TEST
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {...}
@Test
public void shouldMommifyMultipleSetsOfVowels() {...}
@Test
public void shouldMommifyCapitalVowels() {...}
@Test(expected = NullPointerException.class)
public void shouldErrorOnNull() {
mommifier.mommify(null);
}
71. TEST
@Test
public void shouldNotMommifyEmptyString() {...}
@Test
public void shouldNotMommifyWordsWithNoVowels() {...}
@Test
public void shouldMommifyVowel() {...}
@Test
public void shouldMommifyConsonantsAndSingleVowel() {...}
@Test
public void shouldNotMommifyLessThan30PercentVowels() {...}
@Test
public void shouldMommifyContinuousVowelsOnlyOnce() {...}
@Test
public void shouldMommifyMultipleSetsOfVowels() {...}
@Test
public void shouldMommifyCapitalVowels() {...}
@Test(expected = NullPointerException.class)
public void shouldErrorOnNull() {
mommifier.mommify(null);
}
73. TEST CASES
public void shouldNotMommifyEmptyString
public void shouldNotMommifyWordsWithNoVowels
public void shouldMommifyVowel
public void shouldMommifyConsonantsAndSingleVowel
public void shouldNotMommifyLessThan30PercentVowels
public void shouldMommifyContinuousVowelsOnlyOnce
public void shouldMommifyMultipleSetsOfVowels
public void shouldMommifyCapitalVowels
public void shouldErrorOnNull
This talk will be mainly about what benefits Test Driven Development brings and includes a practical example.
Before we start on TDD, let’s quickly look at testing
Why do we test? For a number of reasons!
Testing allows us to verify the behaviour of software and ensures these are not changed unintentionally.
It allows us to track bugs. Call out the possible ones and having a test in place prevents it from coming back.
It also serves as documentation for the current state of the software.
But if we add tests after we have completed a functionality…
How do we make sure we have full test coverage?
How can we be confident all cases are covered?
How can we prevent bugs from happening in the first place?
This is where TDD comes in!
… or Test Driven Development!
But wait…
… what exactly does TDD even mean?
Test driving means…
We write the tests before implementing the code.
And then we write the minimum amount of code possible to make this single test pass – baby steps.
Then we write another test and the process repeats.
But why test first?
Let’s go back to the questions we asked previously for writing tests after:
How can we be more confident about:
Test coverage
Test cases covered
Preventing bugs
Tests written before implementing code
Means that all the code you write should be covered by a test.
Writing minimum amount of code to make tests pass means that you won’t have untested code.
This will result in higher test coverage
So yay. What about test cases?
Writing tests first and having high code coverage means the tests reflect exactly what the code does.
Having a very visible list of the current functionality of the software…
…allows you to see which cases are tested and which are missing
Finally bug prevention
TDD creates a mental shift in having more focus on test cases
When we can see all currently tested cases and try and think about which cases are missing it encourages people to think about the edge cases where the software is likely to fail
And what we expect to happen in these cases.
TDD helps address all these questions…
But that’s not all
Having a high level of test coverage means that it is much easier to debug.
Just need to go to the test case you want and debug that.
This also ensures that we are not breaking functionality when refactoring. Allowing people to be more confident to make bolder and more risky refactorings.
TDD also helps drive out the design of the software.
When you write tests first, you think about the usage of the code before writing it, making it more usable.
Writing only a minimum necessary amount of code means there are no unnecessary functionality that needs support. You ain’t gonna need it!
Now that we’ve been through why.. Let’s talk about the how
Red – Green – Refactor.
Write the test, see your code fail for the right reasons to make sure you’re testing the right thing (red)
Make the test pass (green)
Only refactor while the tests are green
Write a new test and repeat the process
Let’s go through a coding example of TDD
Thinking up different possible test cases
Let’s start with a test.
Since the implementation does not exist, it will not compile.
Make the code compile
Now the test fails for the right reason
Minimum step necessary to make it pass.
And it passes
Now we need another test case to force us to add more functionality to the code
It fails for the right reason
Again minimum amount of code to make it pass – remembering to keep the old tests passing too.
And they pass
Adding actual replacement of vowels.
It fails for the right reason
And the logic for vowel replacement is added
Tests all pass
Now that tests pass we can go ahead and refactor
Notice the logic has changed. It’s a step for us to improve our design.
Tests still pass
Since tests are also code we can refactor this too
And they still pass
Adding new tests for actual replace mixed vowels and consonants
It fails for the right reason
And we modify the code to make it pass.
And all tests including old ones still work
Now for the logic of not replacing vowels if it’s less than 30%
It fails for the right reason
we modify the code to make the tests pass
It does
We can then go and refactor the code to make it more readable and maintainable
Making sure our tests still pass
Continuous vowels
Fails for the right reason
Adding logic
Tests pass
How about multiple sets of vowels in a word?
It passes since our code already works with this scenario.
Sometimes people like to change the code to see it fail just to make sure it’s passing for the right reason and not because of some coincidence.
How about capital letters?
It fails.
Again making it pass
And it does
Null checks?
It already passes because runtime exceptions.
And we’re done! Let’s look at all the test cases we came up before.
The tests now serve as documentations of these functionality.
In terms of writing different types of tests,
I personally prefer to Test Drive from ‘Top down”
This means starting out writing higher level tests such as functional tests or integration tests and then using this to drive out your unit tests.
And having your unit tests then drive out the implementation.
Here are some books if you’re interested in reading more about TDD:
Test Driven Development by example by Kent Beck
Refactoring by Martin Fowler