See CodeUnitTestFirst (We should merge TestingFirst and CodeUnitTestFirst Disagree, since that is a long discussion and this is a short case study. Maybe renaming to CodeUnitTestFirstExamples??)
Write the tests for a class *before* the implementations.
For example, suppose I want to write a class to get a not yet existing file. I'd start with a simple test:
public class FileGetterTest{ private FileGetter fileGetter; private File file; // public void testGetFile(){ fileGetter = new FileGetter(); file = fileGetter.getFile(); assertTrue(file != null); } }The simplest possible small bit of funcionality... give mre a file, any file will do. Now, write a stub implementation:
public class FileGetter{ public File getFile(){ return null; } }And run the test. It fails. Excellent. Why, you ask? This proves that the test works, or at least that it doesn't give a false negative. Okay, fill in the implementation.
public class FileGetter{ public File getFile(){ return new File(); } }And run the test. Okay, it passes, but it's still not really useful... okay, write another small test.
public class FileGetterTest{ private FileGetter fileGetter; private File file; // public void testGetFile(){ fileGetter = new FileGetter(); file = fileGetter.getFile(); assertTrue(file != null); assertTrue(!file.isDirectory()); } }And run the test. It fails...
public class FileGetter{ public File getFile(){ return new File("somenicefilename.txt"); } }And it passes, and is kindof useful, it indeed gets a valid file... but is it unique? Well, let's find out
public class FileGetterTest{ private FileGetter fileGetter; // public void testGetFile(){ fileGetter = new FileGetter(); File file = fileGetter.getFile(); assertTrue(file != null); assertTrue(!file.isDirectory()); // File anotherFile = fileGetter.getFile(); assertTrue(!file.equals(anotherFile)); } }And it fails (seeing a pattern here?) Hmm... is this really what we want? What happens if somebody requests a file, but never uses it? Oops, nevermind... DoTheSimplestThingThatCouldPossiblyWork: so what if they never use it. We're only worrying about getting a unique file at this point. Okay, the implementation, now starting to get useful:
public class FileGetter{ private int index = 0; // public File getFile(){ index++; return new File("somenicefilename" + index + ".txt"); } }Okay, tests passed. Next, we'll we still haven't checked if the file already exists or not... on with the test:
public class FileGetterTest{ private FileGetter fileGetter; // public void testGetFile(){ fileGetter = new FileGetter(); File file = fileGetter.getFile(); assertTrue(file != null); assertTrue(!file.isDirectory()); // File anotherFile = fileGetter.getFile(); assertTrue(!file.equals(anotherFile)); } // public void testGetExistingFile(){ fileGetter = new FileGetter(); File testFile = fileGetter.getFile(); // fileGetter = new FileGetter(); testFile.createNewFile(); assertTrue(!testFile.equals( fileGetter.getFile() )); } }And... it fails.
public class FileGetter{ private int index = 0; // public File getFile(){ File nextFile; do{ nextFile = getNextFile(); }while(nextFile.exists()); return nextFile; } // public File getNextFile(){ index++; return new File("somenicefilename" + index + ".txt"); } }And, the tests pass, it pretty much does what it's supposed to do, anything else that should be tested? Well, we're assuming that a every FileGetter instance will always return the same non existing file, so we should put in a test to let us know if that assumption ever becomes wrong. Any other assumptions discovered should be guarded in the same way.
public class FileGetterTest{ private FileGetter fileGetter; // public void testGetFile(){ fileGetter = new FileGetter(); File file = fileGetter.getFile(); assertTrue(file != null); assertTrue(!file.isDirectory()); // File anotherFile = fileGetter.getFile(); assertTrue(!file.equals(anotherFile)); } // public void testGetExistingFile(){ fileGetter = new FileGetter(); File testFile = fileGetter.getFile(); // fileGetter = new FileGetter(); testFile.createNewFile(); assertTrue(!testFile.equals( fileGetter.getFile() )); } // public void testDeterministicBehaviour(){ fileGetter = new FileGetter(); File testFile = fileGetter.getFile(); // fileGetter = new FileGetter(); assertEquals(testFile, fileGetter.getFile()); } }Tests pass, okay! Any refactoring? Well, parts of each test should be refactored into a setup method, and the 'somenicefilename' bit could probably be dumped... we really don't need it according to the simple story above. So, we end up with something like this:
public class FileGetterTest{ private FileGetter fileGetter; private File testFile; // protected void setUp(){ super.setUp(); fileGetter = new FileGetter(); testFile = fileGetter.getFile(); } // public void testGetFile(){ assertTrue(testFile != null); assertTrue(!testFile.isDirectory()); // File anotherFile = fileGetter.getFile(); assertTrue(!testFile.equals(anotherFile)); } // public void testGetExistingFile(){ fileGetter = new FileGetter(); testFile.createNewFile(); assertTrue(!testFile.equals( fileGetter.getFile() )); } // public void testDeterministicBehaviour(){ fileGetter = new FileGetter(); assertEquals(testFile, fileGetter.getFile()); } } public class FileGetter{ private int index = 0; private File nextFile; // public File getFile(){ do{ setNextFile() }while(nextFile.exists()); return nextFile; } // public void setNextFile(){ index++; testFile = new File(index + ".tmp"); } }And we're left with a class which does exactly what we want it to, and a set of tests which will tell us if anything happens to change that.