My friend, Cees and I were thinking about a test for candidate software engineers. We came up with a fiendishly simple problem. We asked them to write software that would take a simple list of personal data, stored as comma-separated-values, and transform them into a HTML table. However, we warned, shortly in the future, we would be getting different forms of data and the design had to account for that. What would the candidate do? Would they know how to use Java’s classes? Would they drive the design out test-first? And if they did, would they use mocks? Would they use a framework for automating functional tests? If not, why not? Would they separate the parsing of the person data from any person object they created? Or would they combine the parsing, formatting and outputting all in the same program?
The Problem
It took me five days to solve the problem fully, to come up with a robust design. But, it took me only five minutes to write an end-to-end test that passed. The point I want to make, just in case it’s not obvious, is that it took me not one but two orders of magnitude longer to solve the problem than it took to ‘get it working’. Going from ‘getting it working‘ to a proper system, I think, is where all the time on a software project goes. It also accounts for all the head scratching - ‘but it was working last week’.
I decided to post this blog because I would like other people to have a go at solving the problem and I wanted to share my experiences. In the next few weeks, I hope to post more about how I solved this problem, about how I worked outwards from the initial solution to the work of beauty that now sits on my hard drive. Finally, I used, for the first time, WindowLicker, an open source framework, and I wanted to give it (and its creators) a shout out.
Input Data
Name,Address,Postcode,Phone,Credit Limit,Birthday
"Johnson, John",Voorstraat 32,3122gg,020 3849381,10000,01/01/1987
"
Anderson, Paul",Dorpsplein 3A,4532 AA,030 3458986,109093,03/12/1965
Output Data
<table border="1">
<tr>
<td id='name'>Name</td><td id='address'>Address</td><td id='postcode'>Post Code</td><td id='phone'>Phone Number</td><td id='credit_limit'>Credit Limit</td><td id='birthday'>D.O.B.</td></tr>
<tr><td id=1.1>Johnson, John</td><td id=1.2>Voorstraat 32</td><td id=1.3>3122gg</td><td id=1.4>020 3849381</td><td id=1.5>10000</td><td id=1.6>01/01/1987</td>
</tr>
<tr>
<td id=2.1>Anderson, Paul</td><td id=2.2>Dorpsplein 3A</td><td id=2.3>4532 AA</td><td id=2.4>030 3458986</td><td id=2.5>109093</td><td id=2.6>03/12/1965</td>
</tr>
</table>
Rendered Output Data
| Name |
Address |
Post Code |
Phone Number |
Credit Limit |
D.O.B. |
| Johnson, John |
Voorstraat 32 |
3122gg |
020 3849381 |
10000 |
01/01/1987 |
| Anderson, Paul |
Dorpsplein 3A |
4532 AA |
030 3458986 |
109093 |
03/12/1965 |
I always start my projects off with a test driver and an end-to-end test. This blog is about the end-to-end test. The next blog is about the driver. It’s important to know why these two play together.
Test Driver
A test driver is a simple tool for pushing data into the system under test. On a real project, the test driver is useful to the programmer and testers, both of whom want real-time feedback from the system under test, and to the project managers and stakeholders as one way to gauge progress. Any bugs found using the driver are always captured as automated tests.
Test drivers come in a number of forms. At the beginning of our careers, we often use a main method and enter data via the command prompt. As we develop, and start to use test-driven-development, we might rely on unit tests as our drivers. When I work in finance, developing models (sometimes in C# or MatLab), a perfect test-driver is Microsoft’s Excel.
A test driver allows the testers to optimise their work and the optimised work of testers in turn drives the development of the system. A programmer wants to code, wants to get ‘it’ working. An engineer wants to enable themselves and the people around them. It is the latter (and good project managers) who want test drivers to be written; they know that without them the actual time to complete the project will be three or four orders of magnitude longer.
End-To-End Test
There can be no object-oriented development without automated testing. Roughly, we do get it working, that’s what takes five minutes, but we then grow our design outwards. Every time we move a class, pull out some duplicate code, or tidy up a loop, we are designing. Every time we design, we need to know that we haven’t broken the system. An automated end-to-end test allows us to do this, it provides a safety net. In our first attempts at writing the end-to-end test, we really are testing; we are learning about the system we are developing, we are considering, for a given set of inputs, what the outputs should or could be. The system and the end-to-end test grow together (and in the background, the test driver does, too). This is known as the co-evolution of a problem and solution (where the problem is captured in the test and updated as we find out more information):
It is widely accepted that creative design is not a matter of first fixing the problem and then searching for a satisfactory solution concept; instead it seems more to be a matter of developing and refining together both the formulation of the problem and ideas for its solution, with constant iterations of analysis, synthesis and evaluation processes between the two “spaces” - problem and solution. (Cross & Dorst, 1999.)
As the system stabilises, as what we want it to do becomes clear and is captured in the code, the evolution of the end-to-end test (may) slow down. At this point, our design may be a long way from complete, but the software is functional. The end-to-end test becomes an end-to-end check, it verifies what we already know. It confirms existing beliefs. (Checking is a process of ‘confirmation, verification and validation’. Testing is a process of ‘exploration, discovery, investigation, and learning’.)
There we have it. My stall is quite clear: there is no object-oriented design without automated checks. There is no object-oriented design without exploratory testing (which is easier to do with decent tools that in the old days we called test drivers).
End-to-End Test - The Code
The first thing I did was extend one of WindowLicker’s classes, the ‘AbstractWebTest’.
public class EndToEnd extends AbstractWebTest {
In this test, the output from the system is going to be stored, on my desktop, in the file ‘original_table.html’. Before each test, I use WindowLicker to pop open a browser.
private static final String HTML_FILE = "/Users/jamiedobson/desktop/original_table.html";
private static final String INPUT_DATA = "data/Workbook_two_entries.csv";
@Before
public void open() throws MalformedURLException {
openFile(new File(HTML_FILE));
}
The tests use a combination of classes from the Selenium and Hamcrest frameworks, which make it easy for me to write my assertions.
@Test
public void headers() {
browser.element(By.id("name")).assertText(equalTo("Name"));
browser.element(By.id("address")).assertText(equalTo("Address"));
browser.element(By.id("postcode")).assertText(equalTo("Post Code"));
browser.element(By.id("phone")).assertText(equalTo("Phone Number"));
browser.element(By.id("credit_limit")).assertText(equalTo("Credit Limit"));
browser.element(By.id("birthday")).assertText(equalTo("D.O.B."));
}
@Test
public void contents() {
browser.element(By.id("1.1")).assertText(equalTo("Johnson, John"));
browser.element(By.id("1.2")).assertText(equalTo("Voorstraat 32"));
browser.element(By.id("1.3")).assertText(equalTo("3122gg"));
browser.element(By.id("1.4")).assertText(equalTo("020 3849381"));
browser.element(By.id("1.5")).assertText(equalTo("10000"));
browser.element(By.id("1.6")).assertText(equalTo("01/01/1987"));
browser.element(By.id("2.1")).assertText(equalTo("Anderson, Paul"));
browser.element(By.id("2.2")).assertText(equalTo("Dorpsplein 3A"));
browser.element(By.id("2.3")).assertText(equalTo("4532 AA"));
browser.element(By.id("2.4")).assertText(equalTo("030 3458986"));
browser.element(By.id("2.5")).assertText(equalTo("109093"));
browser.element(By.id("2.6")).assertText(equalTo("03/12/1965"));
}
That’s it for the tests. Now we can look at the (rather ugly) code that I wrote to solve this problem. Before each test run, the HTML file is built. The solution is bound to the input and output structures. Inside the code, the concept of a person is hidden. There is no error checking. It only works for the given data set. It is awful, as malleable as charcoal, is utterly resistant to change.
@BeforeClass
public static void setUpFixture() throws IOException, ParseException {
assertTargetFileDoesNotExist();
buildHtmlFile();
}
private static void buildHtmlFile() throws IOException {
String footer = "</body><html>";
String header = "<html><body><table border="1"><tr><td id='name'>Name</td>"
+ "<td id='address'>Address</td><td id='postcode'>Post Code</td>"
+ "<td id='phone'>Phone Number</td><td id='credit_limit'>Credit Limit</td>"
+ "<td id='birthday'>D.O.B.</td></tr>";
Writer writer = new FileWriter(new File(HTML_FILE));
writer.write(header);
int count = 1;
Scanner data = new Scanner(new File(INPUT_DATA));
data.nextLine();
while (data.hasNext()) {
writer.write("<tr>");
Scanner lineScanner = new Scanner(data.nextLine());
lineScanner.findInLine(""");
lineScanner.useDelimiter(",");
String surname = lineScanner.next().replaceAll(""", "");
String firstName = lineScanner.next().replaceAll(""", "").trim();
writer.write("<td id='" + count + ".1'>" + surname + ", " + firstName);
writer.write("<td id='" + count + ".2'>" + lineScanner.next() + "</td>");
writer.write("<td id='" + count + ".3'>" + lineScanner.next() + "</td>");
writer.write("<td id='" + count + ".4'>" + lineScanner.next() + "</td>");
writer.write("<td id='" + count + ".5'>" + lineScanner.next() + "</td>");
writer.write("<td id='" + count + ".6'>" + lineScanner.next() + "</td>");
writer.write("</tr>");
count++;
}
writer.write(footer);
writer.close();
}
}
ConclusionIt took me a few minutes to get this code up and running. For the tests, I piggy backed on a number of open source frameworks, and for the implementation, used Java’s Scanners and Writers. This end-to-end test and simple implementation, along with the test driver which we’ll come to next time, are the three starting points for the design process. From these points, I can work outwards, and in relative safety, tweaking the design as I go along.
By thinking about exploratory testing and end-to-end tests and checks together, I hope that it becomes obvious that they are complementary. The extreme view, that automated checks/tests cannot be augmented with tests drivers is, well, extreme. And a bit daft. The end-to-end tests/checks and test drivers evolve together and are equally important.
Further ReadingSteve and Nat Pryce wrote
Growing Object-Oriented Systems Guided by Tests and I, at least I think, subscribe to most of what they say. It’s a good book, not for the faint hearted, but at the moment the best text on test-driven-development for adults. It’s the Kaner school of thought who practice exploratory testing. Some really good stuff can be found in
Lessons Learned in Software Testing. Binder’s
Testing Object-Oriented Systems is good, rudimentary, essential. Binder is, in my opinion, the starting point for any budding engineers (Freeman and Pryce the end point). You can have a look at my
Triangle example, which I stole from Binder who stole it from Myers.
ToolsI enjoyed using
WindowLicker. The documentation was rubbish but the code simple and the tests well written. I was able to write tests for a Swing application and the end-to-end test easily.
Hamcrest is interesting, it uses (their words not mine) ‘sugar’ syntax that makes assertions more readable. Finally, WindowLicker uses some
Selenium classes. In regards to building object-oriented systems, Freeman and Pryce say we want to ‘achieve more with less code’ p. 66. This, actually, is a measure of how well designed your system is; you are either on top of the code or it is on top of you. With the current state of Java testing tools, you really can do more with less.
Downloads
The code and input test data can be found below. The Java, for firewall purposes, is stored as ‘EndToEnd.txt’. You will need to checkout
WindowLicker and download
Apache Commons IO.
Comments
In this case you've tied an id attribute to your html in order to be able to refer to specific table entries. This seems to only exist to support the test - real code would not have these ID attributes.
However, you're not even really testing GUI stuff here, just making sure fields exist in an HTML file. You could probably remove the ID stuff by using xpath expressions to pick out the table entries.
Also, you should probably link to the WindowLicker project.
Thanks for that. The hyperlink above takes you to the WindowLicker source, this link takes you to the project page: code.google.com/p/windowlicker/.
I shouldn't have named the blog post WindowLicker, since the point I was trying to make was that there is a huge difference between 'getting it working' and 'getting it designed'. I was trying to show the relationship between automated end-to-end checks/tests and test drivers.
I did use WindowLicker to test a Java application, and it was alright. I am hoping that I can write about that later.
'Real' code may not produce tables with IDs, but 'real' good code will be designed so it can be tested. Remember, I was trying to show how quick it was to get this test passing, not show an example of good design, that's the next blog....
I strongly think you should read the code into a model, then run tests on the model as opposed to the output. But perhaps you plan to do this over the next few blogs.
'It took me five days to solve the problem fully, to come up with a robust design. But, it took me only five minutes to write an end-to-end test that passed. The point I want to make, just in case it’s not obvious, is that it took me not one but two orders of magnitude longer to solve the problem than it took to ‘get it working’. Going from ‘getting it working‘ to a proper system, I think, is where all the time on a software project goes. It also accounts for all the head scratching - ‘but it was working last week’.'
I wrote this blog as an example of easy it was to get a test passing. But it did, and it's not the code here, take me 'five days to solve the problem fully'. Did you read this blog or is it, in Scotland, National Wind Jamie Up Day? ';-)
I read it and intentionally never commented on the actual code, just the test, which is flawed in my opinion!
I agree with the thrust of your argument about the effort goes into doing things properly and robustly, but I'm not quite sure what you intend to do next. Are you going to come up with test cases that fail then solve them? Then at the end show how different the final tests are from the first one? In which case, fair enough, but I still think you started with the wrong test ;)
RSS feed for comments to this post