This is a tutorial loosely based on this writeup. That writeup describes using table table to implement test data setup to make determining expected results easier. You can read that for a slightly different take. That example was written after the fact and somewhat cleaned up. It also is not a tutorial; it is really a summary of what you’ll be doing in this tutorial.
In this tutorial, you’ll review the setup for a previous test and then build the test setup in a way that will much better relate to the domain. Unlike the original table table example, this one will seem a lot more like a plausible development effort.
Getting Started
As with the other tutorials, you can continue from the work you’ve done on the previous tutorial, or you can use the source and start at the tag: FitNesse.Tutorials.TableTables.Start.
Up to this point, you have created programs using several different styles. However, all of these styles are very different from the underlying domain. This tutorial picks up from the Scenario Tables Tutorial and looks at one final way to create a program guide, or a series of programs.
Creating Many Programs
You used the following table to populate the program schedule (this is a snippet):
What does this table represent? What follows is a different visualization of this same data to make it easier to understand. In fact, it is just two days, but you can probably imagine extending this to support multiple days:
Notice that this appears more like a program guide you might see with cable receiver or a DVR. This isn’t perfect because there are artifacts from the original table in the forms of the names used to create the programs. Even so, this appears to be a bit closer to a program guide.
Next, consider the following simplifications:
Programs only have one episode called “E1”
Only consider one day
Simplify the names. The name of the program/episode is just a sequence of the same characters, e.g. aaaabbbb, represents 2 programs, one called aaaa and the second called bbbb.
Rather than specify each starting time, make the length of the name related to the program length, e.g., 4 letters is 1 hour, 1 letter is 15 minutes.
With these changes, read this program guide:
Why do this? When I was testing the logic of selecting the correct programs with multiple season passes and a variable number of recorders, I had trouble program the schedules using the script table. What I was doing in my head was visualizing the program guide used by the DVRs I’ve personally used. It then hit me that the visualization of the program guide was essential to my use of the DVR. My tests did not reflect that so it occurred to me to make my tests reflect that as close as I could.
There is one problem with this setup. On DVRs, the length of the program is not related to the length of its name. This is an artificial simplification to make the table representation look decent. Even so, in practice this representation made writing tests easier at the expense of a more complex fixture, and that’s the right choice to make.
What you will do in the remainder of this tutorial is create a fixture to handle this new table type. Once you’ve done that, you’ll recreate some of the tests from the previous tutorial using the table table for the setup.
Creating the table
As with the previous tutorials, you’ll create these tests under their own sub-hierarchy:
Change its type to a suite page (Note, as of 4/15/09, FitNesse has been updated to automatically set the page type to a suite if it ends in “Examples”. If you build from source or you happen to have a recent release with this feature built, you might not need to set the type to suite.)
Notice that this borrows the Scenario table and script table from the SetUp page in the previous tutorial. In this tutorial, the only new table is a new Table Table to populate the program schedule.
You might need to set this page to a test page. As of 4/15/09, the FitNesse source will automatically set the test page type for pages that begin with or end with the word “Example” (in addition to the word “Test”). However, you might not have the latest release.
Finally, you have a complete test with SetUp and TearDown code. Run it and notice that the test fails. It cannot find the class “Create One Day Program Guide”, which is required by the Table Table.
Create the Fixture
Create version 1 of the fixture:
While this Fixture does not do anything yet, it is a minimal example that will get the test to run, finding all of the Fixtures. The test is still failing.
The minimal requirement for the fixture is a doTable method as shown above. Since the table takes in parameters to its constructor, this fixture also needs a matching constructor. Now that you have the basic infrastructure in place, it’s time to experiment just a little bit to see just what FitNesse really passes in to the doTable method.
Update the doTable method:
Execute this test and review the output (click on the yellow triangle with an ! in it):
Does this look familiar? This gives a hint at just what a table-table does. FitNesse simply passes in the entire table (minus the first row) in the form of a list of a list of strings:
The outside list represents the collection of rows. The first entry is actually the second row of the actual table, FitNesse does not pass in the row naming the fixture. (We’re going to ignore that row altogether in this tutorial, it’s there for documentation).
The inside list represents the individual cells within a given row. In our case, the first cell is the channel. The remaining cells represent an hour of programming.
With that basic understanding, now it is time to process an individual row. This fixture (and in general, table-table fixtures) can be complex enough to warrant unit test code. Why is that? You are trying to make a table that is easy for a non-programmer to be able to use effectively; something that is closer to the problem domain. Because the table is closer to the domain and further away from the implementation, it will require some amount of coding.
Switch to Unit Testing
Our fixture needs to be able process a series of rows, each of which represent a channel of programming. That’s where we’ll start with unit testing.
Create the First Test
This first test simply puts most of the basic API in place:
Purpose: The purpose of this test is to being defining the API by which row parsing will happen.
Here’s the minimal code to make this test pass:
Run the test, make sure it passes.
Next, add another test with one program and notice that this requires several changes:
Update: ProgramGuideRowParserTest, Add new test
Update ProgramGuidRowPaser: Add constructor and method
Run your tests, they fail.
Add missing equals() methods:
Update: TimeSlot:
Update: Program
Finally, update the production code to get this test passing:
Run your tests, they should pass.
Note: Did you notice that you just added equals() methods to Program and TimeSlot without adding unit tests to those same classes? Is this a problem? These methods are complex enough that there is certainly some risk in adding them. Also, in Java it is standard practice to write hashCode() when writing equals() just in case the object is used as a key in a Map. However, the code does not sore these objects as keys in Maps, so writing a hashCode() method, while conventional, is not really necessary.
These methods were written in response to a test, something more than a unit test, but a test none the less. Whether to add tests for the equals() method beyond what we’ve already written is not a clear yes or no decision, so I’ll leave that to the reader since this is more about working with FitNesse than unit testing (in the book version of this tutorial, however, I’ll probably take the other approach).
Next Test: Getting Program Length Correct
Add this test:
Run it and verify that it does not pass.
Next, update the production code in ProgramGuideRowParser:
Run your tests, make sure they pass.
Note: This is a somewhat refactored method. It will get longer and shorter as you work through this parsing exercise.
Next Test: Handle two 30 minute programs
Add a new test (and update the @Before method):
Note: You can remove the parser.setChannel(204) from the other two @Test methods since that duplicated method has be pushed up into the @Before method.
Run your tests, the new one will.
Now make several updates to make this next test pass (and notice that the code is getting unruly):
Update: ProgramGuideRowParser.java
Update: DateUtil.java
Run your tests, make sure they pass.
Next Test: Ignore _ in name
FitNesse removes extra spaces on either side of the cell. To represent “no program”, the example uses _. Here’s a test that verifies the production code handles _’s correctly:
After adding this test, verify that it fails.
Now, update the parser to handle this condition (note, this is after several rounds of refactoring, with much refactoring still left):
Run your tests, make sure the pass.
Next Test: A cell with all spaces handled correctly
FitNesse will take an empty cell and pass in “”, so here’s a test to make sure the production code works:
Run your tests, verify this new one fails.
Update your parser to get this test to pass:
Run your tests, make sure they pass.
Final Test: One Big Row
This algorithm is either close to complete or complete. Here’s a final test that will bring everything together:
Run your tests, make sure this new test fails (the underlying code is close but not quite there).
Here’s the update to the parse method (post-refactoring):
Make this change, verify that your tests pass.
Make sure all of your unit tests still pass.
Switch back to your browser and verify that all acceptance tests still pass (other than the one failing test).
Bringing it all together
Now that you can parse a single row, there are a few things left before your table-table fixture will be ready:
Parse a row that contains both a channel and all of the programs.
Take all of the programs from all of the rows and add them to the program schedule.
This will require several more steps, so let’s get started.
Refactor: The Name is wrong
The name of the parser class is wrong, it only parses the programs not the whole row (it does not handle the channel).
Rename the class ProgramGuideRowParser –> ProgramGuideProgramCellsParser.
Run your tests, make sure they still pass
Rename the test class ProgramGuidRowParserTest –> ProgramGuideProgramCellsParserTest
Run your tests, make sure they still pass.
Create the Real ProgramGuideRowParser
Create a new test class, ProgramGuideRowParserTest:
This requires two new classes:
Create: ProgramBuilderUtil.java
Create: ProgramGuideRowParser
Also, the new class ProgramBuilderUtil was extracted from the previous class:
Update: ProgramGuideProgramCellsParserTest.java
Run all of your unit tests, make sure everything passes.
Finally, upate the Table Fixture
All the pieces are in place, now it’s just a matter of creating the programs:
Update the CreateOneDayProgramGuide.java**
You’ll need to add one method to Schedule:
Make sure all of your unit tests pass.
Make sure all of your acceptance tests pass.
Final Cleanup
I made some false starts. For example, rather than having the ProgramGuideRowParser take in a String, it could easily have taken in a List and did all of its work based on that. Also, the DvrRecording fixture requires a range of episodes, but the way this program fixture works, it only creates one episode per program. I'm sure you could review the fixtures and also the unit tests and production code and find several more places to refactor. Before you leave this tutorial, let's finish up with some basic refactoring of the Fixtures.
Allow for a Single Episode
After a little experimentation with the DvrRecording fixture, I discovered a simple change that allows a single value rather than a range. I tried a few values and ran the tests after each time.
Make the following update to DvrRecording (note, only the return statement in each of these methods changed):
Run all of your unit tests, make sure they still pass.
Run all of your acceptance tests, make sure the still pass.
Update your acceptance test:
Run your acceptance test, make sure they all pass. Close, but not quite.
Make one more change to the DvrRecording fixture:
Run all of your unit tests and acceptance tests, they should all pass.
Change ProgramGuideRowParser to take a List instead of a String
Update ProgramGuideRowParserTest:
Update ProgramGuidRowParser:
Update CreateOneDayProgramGuide:
Run your unit tests, verify they still pass.
Run your acceptance tests, make sure they still pass.
Conclusion and Summary
Congratulations, you have finished this tutorial.
This tutorial demonstrated that you can create tables reflecting a more natural or fluent style and then write more complex fixture code to support that style.
Once you created the table, you started creating a parser using TDD to build the parser from the ground up. While this tutorial did not show you all of the intermediate steps, it certainly did cover many of the major moving parts. Once you built most of the infrastructure, you tied it into the fixture and got the test passing.
Table tables are a great way to pass anything in that you’d like. You can also pass back a completely different table. That is not demonstrated here, this tutorial simply passes back an empty list so FitNesse disregards the return result, but it certainly is possible to pass back something different from the input table.
Notice that this tutorial was different from the previous tutorials in that you spent more time working on fixture-based code. Indeed, there was very little new code added to the production side. This is consistent with table fixtures, most of the work is in mapping the table representation into some kind of logical message. When you use table fixtures, you are making it easier for acceptance test writers to write better looking tests. You are taking the burden of the responsibility to make things work.
Assuming you’ve worked through the previous tutorials, you have now used all of the basic table types in Slim. Congratulations!
Comments