Port of schuchert.wikispaces.com


FitNesse.Tutorials.TableTables

FitNesse.Tutorials.TableTables

Background

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):

|Create Daily Program Named|D5_1|On Channel|5|Starting On|3/4/2008|at|20:00|Length|30|Episodes|7|
|Create Daily Program Named|D5_2|On Channel|5|Starting On|3/4/2008|at|20:30|Length|30|Episodes|7|
|Create Daily Program Named|D5_3|On Channel|5|Starting On|3/4/2008|at|21:00|Length|30|Episodes|7|
|Create Daily Program Named|D5_4|On Channel|5|Starting On|3/4/2008|at|21:30|Length|30|Episodes|7|
|Create Daily Program Named|D6_1|On Channel|6|Starting On|3/4/2008|at|20:00|Length|30|Episodes|7|
|Create Daily Program Named|D6_2|On Channel|6|Starting On|3/4/2008|at|20:30|Length|30|Episodes|7|
|Create Daily Program Named|D6_3|On Channel|6|Starting On|3/4/2008|at|21:00|Length|30|Episodes|7|
|Create Daily Program Named|D6_4|On Channel|6|Starting On|3/4/2008|at|21:30|Length|30|Episodes|7|
...

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:

|3/4/2008|20:00  |20:30  |21:00  |21:30  |
|5       |D5_1.E1|D5_2.E1|D5_3.E1|D5_4.E1|
|6       |D6_1.E1|D6_2.E1|D6_3.E1|D6_4.E1|
|3/5/2008|20:00  |20:30  |21:00  |21:30  |
|5       |D5_1.E1|D5_2.E1|D5_3.E1|D5_4.E1|
|6       |D6_1.E1|D6_2.E1|D6_3.E1|D6_4.E1|

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:

With these changes, read this program guide:

|Table:Create One Day Program Guide|1:00|3/4/2008| 
|   |1   |2   |3   |4   |5   |6   |7   |8   |9   |10  |11  |12  |13  |14  |
|200|aaaa|BBcc|cccc|ccDD|DDee|efff|ffff|fffg|gggg|gggh|hhii|jklm|nopq|rstt|
|247|aaaa|BBBB|cccc|DDDD|eeee|FFFF|gggg|HHHH|iiii|JJJJ|kkkk|LLLL|mmmm|NNNN|
|302|aaBB|ccDD|eeFF|ggHH|iiJJ|kkLL|mmNN|ooPP|qqRR|ssTT|uuVV|wwww|wwXX|XXXy|
|501|    |    |    |    |    |aaBB|ccDD|eeFF|ggHH|iiJJ|kkLL|mmNN|ooPP|qqRR|
|556|    |__aa|BBcc|DDee|FFgg|HHii|JJkk|LLmm|NNoo|PPqq|RRss|TTuu|VVxx|xxxx|

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:

!include <DigitalVideoRecorderExamples.SetUp

|Table:Create One Day Program Guide|1:00|3/4/2008|
|   |1   |2   |3   |4   |5   |6   |7   |8   |9   |10  |11  |12  |13  |14  |
|200|aaaa|BBcc|cccc|ccDD|DDee|efff|ffff|fffg|gggg|gggh|hhii|jklm|nopq|rstt|
|247|aaaa|BBBB|cccc|DDDD|eeee|FFFF|gggg|HHHH|iiii|JJJJ|kkkk|LLLL|mmmm|NNNN|
|302|aaBB|ccDD|eeFF|ggHH|iiJJ|kkLL|mmNN|ooPP|qqRR|ssTT|uuVV|wwww|wwXX|XXXy|
|501|    |    |    |    |    |aaBB|ccDD|eeFF|ggHH|iiJJ|kkLL|mmNN|ooPP|qqRR|
|556|    |__aa|BBcc|DDee|FFgg|HHii|JJkk|LLmm|NNoo|PPqq|RRss|TTuu|VVxx|xxxx|

!|Scenario|dvrCanSimultaneouslyRecord|number|andWithThese|seasonPasses|shouldHaveTheFollowing|toDoList|
|givenDvrCanRecord|@number|
|whenICreateSeasonPasses|@seasonPasses|
|thenTheToDoListShouldContain|@toDoList|

|Script|Dvr Recording|

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.

Next, we need a test that uses this http://localhost:8080/FrontPage.DigitalVideoRecorderExamples.TableTableExamples.SetUp

|Scenario|A Two Recorder Dvr With These Season Passes|seasonPasses|Should Have These Episodes In To Do List|toDoList|
|Dvr Can Simultaneously Record | 2 | And With These |@seasonPasses|Should Have The Following|@toDoList|

|A Two Recorder Dvr With These Season Passes Should Have These Episodes In To Do List|
|seasonPasses                                |toDoList                               |
|cccccccc:200,FF:302                         |cccccccc:E:1-1,FF:E:1-1                |
package com.om.example.dvr.fixtures;

import java.util.Collections;
import java.util.List;

public class CreateOneDayProgramGuide {
   public CreateOneDayProgramGuide(String startTime, String date) {
   }

   public List<?> doTable(List<List<String>> table) {
      return Collections.emptyList();
   }
}

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.

   public List<?> doTable(List<List<String>> table) {
      for (List<String> row : table) {
         System.out.print("|");
         for (String cell : row)
            System.out.printf("%s|", cell);
         System.out.println();
      }
      return Collections.emptyList();
   }
|1|2|3|4|5|6|7|8|9|10|11|12|13|14|
|200|aaaa|BBcc|cccc|ccDD|DDee|efff|ffff|fffg|gggg|gggh|hhii|jklm|nopq|rstt|
|247|aaaa|BBBB|cccc|DDDD|eeee|FFFF|gggg|HHHH|iiii|JJJJ|kkkk|LLLL|mmmm|NNNN|
|302|aaBB|ccDD|eeFF|ggHH|iiJJ|kkLL|mmNN|ooPP|qqRR|ssTT|uuVV|wwww|wwXX|XXXy|
|501|||aaBB|ccDD|eeFF|ggHH|iiJJ|kkLL|mmNN|ooPP|qqRR|
|556|__aa|BBcc|DDee|FFgg|HHii|JJkk|LLmm|NNoo|PPqq|RRss|TTuu|VVxx|xxxx|

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:

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

package com.om.example.dvr.fixtures;

import static org.junit.Assert.assertEquals;

import java.util.List;

import org.junit.Test;

import com.om.example.dvr.domain.Program;

public class ProgramGuideRowParserTest {
   @Test
   public void emptyRowGeneratesNoPrograms() {
      ProgramGuideRowParser parser = new ProgramGuideRowParser();
      List<Program> result = parser.parse("");
      assertEquals(0, result.size());
   }
}

Purpose: The purpose of this test is to being defining the API by which row parsing will happen.

package com.om.example.dvr.fixtures;

import java.util.LinkedList;
import java.util.List;

import com.om.example.dvr.domain.Program;

public class ProgramGuideRowParser {

   public List<Program> parse(String string) {
      return new LinkedList<Program>();
   }
}
package com.om.example.dvr.fixtures;

import static org.junit.Assert.assertEquals;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import org.junit.Before;
import org.junit.Test;

import com.om.example.dvr.domain.Program;
import com.om.example.dvr.domain.TimeSlot;
import com.om.example.util.DateUtil;

public class ProgramGuideRowParserTest {
   private ProgramGuideRowParser parser;

   @Before
   public void init() throws ParseException {
      parser = new ProgramGuideRowParser(DateUtil.instance()
            .buildDate("4/8/2008", "1:00"));
   }

   @Test
   public void emptyRowGeneratesNoPrograms() {
      List<Program> result = parser.parse("");
      assertEquals(0, result.size());
   }

   @Test
   public void oneOneHourProgram() throws ParseException {
      parser.setChannel(204);
      List<Program> result = parser.parse("|aaaa|");

      assertEquals(1, result.size());

      Program expected = buildProgram("4/8/2008", "1:00", "aaaa", 204, 60);
      assertEquals(expected, result.get(0));
   }

   private Program buildProgram(String date, String time, String name, int channel,
         int duration) throws ParseException {
      Date expectedStartDateTime = DateUtil.instance().buildDate(date, time);
      return new Program(name, "E1", new TimeSlot(channel, expectedStartDateTime,
            duration));
   }
}

Update ProgramGuidRowPaser: Add constructor and method

   public ProgramGuideRowParser(Date buildDate) {
   }

   public void setChannel(int channel) {
   }

Update: TimeSlot:

   @Override
   public boolean equals(Object other) {
      if (!(other instanceof TimeSlot))
         return false;

      TimeSlot rhs = getClass().cast(other);
      return channel == rhs.channel && durationInMinutes == rhs.durationInMinutes
            && startDateTime.equals(rhs.startDateTime);
   }

Update: Program

   @Override
   public boolean equals(Object other) {
      if (!(other instanceof Program))
         return false;

      Program rhs = getClass().cast(other);
      return programName.equals(rhs.programName) && episodeName.equals(rhs.episodeName)
            && timeSlot.equals(rhs.timeSlot);
   }
public class ProgramGuideRowParser {
   private int channel;
   private final Date buildDate;

   public ProgramGuideRowParser(Date buildDate) {
      this.buildDate = buildDate;
   }

   public void setChannel(int channel) {
      this.channel = channel;
   }

   public List<Program> parse(String programsInCells) {
      LinkedList<Program> result = new LinkedList<Program>();

      if (programsInCells.length() > 0) {
         TimeSlot timeSlot = new TimeSlot(channel, buildDate, 60);
         result.add(new Program("aaaa", "E1", timeSlot));
      }

      return result;
   }
}

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

   @Test
   public void oneThirtyMinuteProgram() throws ParseException {
      parser.setChannel(204);
      List<Program> result = parser.parse("|aa|");

      assertEquals(1, result.size());

      Program expected = buildProgram("4/8/2008", "1:00", "aa", 204, 30);
      assertEquals(expected, result.get(0));
   }
   public List<Program> parse(String programsInCells) {
      List<Program> result = new LinkedList<Program>();

      if (programsInCells.length() > 0) {
         String[] cells = programsInCells.substring(1).split("\\|");

         String currentName = cells[0];
         TimeSlot timeSlot = new TimeSlot(channel, buildDate, currentName.length() * 15);
         result.add(new Program(currentName, "E1", timeSlot));
      }

      return result;
   }

Next Test: Handle two 30 minute programs

   @Before
   public void init() throws ParseException {
      parser = new ProgramGuideRowParser(DateUtil.instance()
            .buildDate("4/8/2008", "1:00"));
      parser.setChannel(204);
   }

   // ...

   @Test
   public void twoThirtyMinuteProgramsInSameCell() throws ParseException {
      List<Program> result = parser.parse("|aabb|");
      
      assertEquals(2, result.size());
      
      Program expected0 = buildProgram("4/8/2008", "1:00", "aa", 204, 30);
      assertEquals(expected0, result.get(0));
      
      Program expected1 = buildProgram("4/8/2008", "1:30", "bb", 204, 30);
      assertEquals(expected1, result.get(1));
   }

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.

Update: ProgramGuideRowParser.java

   public List<Program> parse(String programsInCells) {
      List<Program> result = new LinkedList<Program>();

      if (programsInCells.length() > 0) {
         Date nextDate = buildDate;

         List<String> programNames = split(programsInCells);

         for (String currentName : programNames) {
            int length = currentName.length() * 15;

            TimeSlot timeSlot = new TimeSlot(channel, nextDate, length);
            result.add(new Program(currentName, "E1", timeSlot));
            nextDate = calculateNextDate(nextDate, length);
         }
      }
      return result;
   }

   private List<String> split(String programsInCells) {
      String[] cells = programsInCells.substring(1).split("\\|");

      List<String> result = new LinkedList<String>();

      int lastStartIndex = 0;
      int lastChar = cells[0].charAt(0);
      for (int i = 1; i < cells[0].length(); ++i) {
         if (cells[0].charAt(i) != lastChar) {
            String lastProgramName = cells[0].substring(lastStartIndex, i);
            result.add(lastProgramName);
            lastStartIndex = i;
            lastChar = cells[0].charAt(i);
            continue;
         }
         if (i == cells[0].length() - 1) {
            String lastProgramName = cells[0].substring(lastStartIndex);
            result.add(lastProgramName);
         }
      }

      return result;
   }

   private Date calculateNextDate(Date fromDate, int lengthInMinutes) {
      return DateUtil.instance().addMinutesTo(fromDate, lengthInMinutes);
   }

Update: DateUtil.java

   public Date addMinutesTo(Date fromDate, int minutes) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(fromDate);
      calendar.add(Calendar.MINUTE, minutes);
      return calendar.getTime();
   }

Next Test: Ignore _ in name

   @Test
   public void underscoredIgnored() throws ParseException {
      List<Program> result = parser.parse("|__aa|");
      
      assertEquals(1, result.size());
      
      Program expected = buildProgram("4/8/2008", "1:30", "aa", 204, 30);
      assertEquals(expected, result.get(0));
   }
package com.om.example.dvr.fixtures;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import com.om.example.dvr.domain.Program;
import com.om.example.dvr.domain.TimeSlot;
import com.om.example.util.DateUtil;

public class ProgramGuideRowParser {

   private final Date buildDate;
   private int channel;

   public ProgramGuideRowParser(Date buildDate) {
      this.buildDate = buildDate;
   }

   public List<Program> parse(String programsInCells) {
      List<Program> result = new LinkedList<Program>();

      String programs = programsInCells.replaceAll("\\|", "");

      for (int index = startIndexOfNextProgram(0, programs); index < programs.length();) {
         Date nextDate = calculateNextDate(buildDate, index * 15);

         String nextProgram = getNextProgram(index, programs);
         if (nextProgram != null) {
            int length = nextProgram.length();
            index += length;

            result.add(buildProgram(nextDate, length * 15, nextProgram));
         }
      }

      return result;
   }

   private Program buildProgram(Date date, int duration, String name) {
      TimeSlot timeSlot = new TimeSlot(channel, date, duration);
      return new Program(name, "E1", timeSlot);
   }

   public void setChannel(int channel) {
      this.channel = channel;
   }

   private int startIndexOfNextProgram(int index, String string) {
      while (index < string.length() && !Character.isLetter(string.charAt(index)))
         ++index;
      return index;
   }

   private String getNextProgram(int index, String string) {
      int lastIndex = index;

      while (lastIndex < string.length()
            && string.charAt(lastIndex) == string.charAt(index))
         ++lastIndex;

      return string.substring(index, lastIndex);
   }

   private Date calculateNextDate(Date fromDate, int lengthInMinutes) {
      return DateUtil.instance().addMinutesTo(fromDate, lengthInMinutes);
   }
}

Next Test: A cell with all spaces handled correctly

   @Test
   public void emptyCellsHandeledCorrectly() throws ParseException {
      List<Program> result = parser.parse("||__aa|");
      
      assertEquals(1, result.size());
      
      Program expected = buildProgram("4/8/2008", "2:30", "aa", 204, 30);
      assertEquals(expected, result.get(0));
   }
   public List<Program> parse(String programsInCells) {
      List<Program> result = new LinkedList<Program>();

      String programs = buildSingleStringFromCells(programsInCells);

      // snip - unchanged
   }

   private String buildSingleStringFromCells(String programsInCells) {
      String programs = programsInCells.replaceAll("\\|\\|", "|    |");
      programs = programs.replaceAll("\\|", "");
      return programs;
   }

Final Test: One Big Row

This algorithm is either close to complete or complete. Here’s a final test that will bring everything together:

   @Test
   public void verifyComplexParse() throws ParseException {
      List<Program> result = parser.parse("||__aa|BBcc||FFgg|__  |");
      assertEquals(5, result.size());

      Program expectedLastProgram = buildProgram("4/8/2008", "5:30", "gg", 204, 30);
      assertEquals(expectedLastProgram, result.get(4));
   }
package com.om.example.dvr.fixtures;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import com.om.example.dvr.domain.Program;
import com.om.example.dvr.domain.TimeSlot;
import com.om.example.util.DateUtil;

public class ProgramGuideRowParser {

   private static final int MINS_PER_CHAR = 15;
   private final Date buildDate;
   private int channel;

   public ProgramGuideRowParser(Date buildDate) {
      this.buildDate = buildDate;
   }

   public List<Program> parse(String programsInCells) {
      List<Program> result = new LinkedList<Program>();

      String programs = buildSingleStringFromCells(programsInCells);

      int index = 0;
      while ((index = startIndexOfNextProgram(index, programs)) < programs.length()) {
         String nextProgramName = calculateNextProgramName(index, programs);
         index += addNextProgram(nextProgramName, index, result);
      }

      return result;
   }

   public void setChannel(int channel) {
      this.channel = channel;
   }

   private String buildSingleStringFromCells(String programsInCells) {
      String programs = programsInCells.replaceAll("\\|\\|", "|    |");
      programs = programs.replaceAll("\\|", "");
      return programs;
   }

   private Program buildProgram(int index, int duration, String name) {
      Date nextDate = calculateNextDate(buildDate, index * MINS_PER_CHAR);
      TimeSlot timeSlot = new TimeSlot(channel, nextDate, duration);
      return new Program(name, "E1", timeSlot);
   }

   private int addNextProgram(String programName, int index, List<Program> result) {
      if (programName != null) {
         int length = programName.length();
         result.add(buildProgram(index, length * MINS_PER_CHAR, programName));
         return length;
      }
      return 0;
   }

   private int startIndexOfNextProgram(int index, String string) {
      while (index < string.length() && !Character.isLetter(string.charAt(index)))
         ++index;
      return index;
   }

   private String calculateNextProgramName(int index, String string) {
      int lastIndex = index;

      while (lastIndex < string.length() && charactesSameAt(string, index, lastIndex))
         ++lastIndex;

      return string.substring(index, lastIndex);
   }

   private boolean charactesSameAt(String string, int startIndex, int endIndex) {
      return string.charAt(endIndex) == string.charAt(startIndex);
   }

   private Date calculateNextDate(Date fromDate, int lengthInMinutes) {
      return DateUtil.instance().addMinutesTo(fromDate, lengthInMinutes);
   }
}

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:

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).

Create the Real ProgramGuideRowParser

package com.om.example.dvr.fixtures;

import static org.junit.Assert.assertEquals;

import java.text.ParseException;
import java.util.List;

import org.junit.Test;

import com.om.example.dvr.domain.Program;

public class ProgramGuideRowParserTest {
   @Test
   public void verifyChannelSetCorrectly() throws ParseException {
      ProgramGuideRowParser parser = new ProgramGuideRowParser("4/5/2008", "9:00");
      List<Program> programs = parser.parse("|123|aaaa|");

      assertEquals(1, programs.size());
      Program expected = ProgramBuilderUtil.buildProgram("4/5/2008", "9:00", "aaaa", 123,
            60);
      assertEquals(expected, programs.get(0));
   }
}

Create: ProgramBuilderUtil.java

package com.om.example.dvr.fixtures;

import java.text.ParseException;
import java.util.Date;

import com.om.example.dvr.domain.Program;
import com.om.example.dvr.domain.TimeSlot;
import com.om.example.util.DateUtil;

public class ProgramBuilderUtil {
   public static Program buildProgram(String date, String time, String name, int channel,
         int duration) throws ParseException {
      Date expectedStartDateTime = DateUtil.instance().buildDate(date, time);
      return new Program(name, "E1", new TimeSlot(channel, expectedStartDateTime,
            duration));
   }
}

Create: ProgramGuideRowParser

package com.om.example.dvr.fixtures;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import com.om.example.dvr.domain.Program;
import com.om.example.util.DateUtil;

public class ProgramGuideRowParser {
   ProgramGuideProgramCellsParser parser;

   public ProgramGuideRowParser(String date, String startTime) throws ParseException {
      Date startDateTime = DateUtil.instance().buildDate(date, startTime);
      parser = new ProgramGuideProgramCellsParser(startDateTime);
   }

   public List<Program> parse(String row) {
      int channel = getChannelFrom(row);
      parser.setChannel(channel);
      return parser.parse(justProgramCellsFrom(row));
   }

   private String justProgramCellsFrom(String row) {
      return row.replaceAll("^\\|\\d+\\|", "");
   }

   private int getChannelFrom(String row) {
      String justTime = row.substring(1).replaceAll("\\|.*", "");
      return Integer.parseInt(justTime);
   }
}

Update: ProgramGuideProgramCellsParserTest.java

   private Program buildProgram(String date, String time, String name, int channel,
         int duration) throws ParseException {
      return ProgramBuilderUtil.buildProgram(date, time, name, channel, duration);
   }

Finally, upate the Table Fixture

All the pieces are in place, now it’s just a matter of creating the programs:

package com.om.example.dvr.fixtures;

import java.text.ParseException;
import java.util.Collections;
import java.util.List;

import com.om.example.dvr.domain.Program;

public class CreateOneDayProgramGuide {
   private ProgramGuideRowParser parser;

   public CreateOneDayProgramGuide(String startTime, String date) throws ParseException {
      parser = new ProgramGuideRowParser(date, startTime);
   }

   public List<?> doTable(List<List<String>> table) {
      table.remove(0);

      for (List<String> row : table)
         handleOneRow(row);

      return Collections.emptyList();
   }

   private void handleOneRow(List<String> row) {
      String string = rowAsString(row);

      List<Program> programs = parser.parse(string);

      for (Program program : programs) 
         AddProgramsToSchedule.getSchedule().addProgram(program);
   }

   private String rowAsString(List<String> row) {
      StringBuffer buffer = new StringBuffer();

      buffer.append("|");
      for (String current : row)
         buffer.append(String.format("%s|", current));

      return buffer.toString();
   }
}
   public void addProgram(Program program) {
      if (conflictsWithOtherTimeSlots(program.timeSlot))
         throw new ConflictingProgramException();

      scheduledPrograms.add(program);
   }

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.

   private int extractLowerRangeFrom(String episodeSet) {
      // snip

      return Integer.MAX_VALUE;
   }

   private int extractUpperRangeFrom(String episodeSet) {
      // snip

      return 1;
	}
|A Two Recorder Dvr With These Season Passes Should Have These Episodes In To Do List|
|seasonPasses                                |toDoList                               |
|cccccccc:200,FF:302                         |cccccccc:E:1,FF:E:1                    |
   private int extractUpperRangeFrom(String episodeSet) {
      String[] values = episodeSet.split(":");
      if (values.length > 2 && values[2].indexOf('-') > 0) {
         String highRange = episodeSet.split(":")[2].split("-")[1];
         return Integer.parseInt(highRange);
      }
      return 1;
   }

Change ProgramGuideRowParser to take a List instead of a String

package com.om.example.dvr.fixtures;

import static org.junit.Assert.assertEquals;

import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;

import org.junit.Test;

import com.om.example.dvr.domain.Program;

public class ProgramGuideRowParserTest {
   @Test
   public void verifyChannelSetCorrectly() throws ParseException {
      ProgramGuideRowParser parser = new ProgramGuideRowParser("4/5/2008", "9:00");
      List<Program> programs = parser.parse(buildList("123", "aaaa"));

      assertEquals(1, programs.size());
      Program expected = ProgramBuilderUtil.buildProgram("4/5/2008", "9:00", "aaaa", 123,
            60);
      assertEquals(expected, programs.get(0));
   }

   private List<String> buildList(String... strings) {
      List<String> result = new LinkedList<String>();

      for (String current : strings)
         result.add(current);

      return result;
   }
}
package com.om.example.dvr.fixtures;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import com.om.example.dvr.domain.Program;
import com.om.example.util.DateUtil;

public class ProgramGuideRowParser {
   ProgramGuideProgramCellsParser parser;

   public ProgramGuideRowParser(String date, String startTime) throws ParseException {
      Date startDateTime = DateUtil.instance().buildDate(date, startTime);
      parser = new ProgramGuideProgramCellsParser(startDateTime);
   }

   public List<Program> parse(List<String> cells) {
      int channel = Integer.parseInt(cells.get(0));
      cells.remove(0);
      parser.setChannel(channel);
      return parser.parse(rowAsString(cells));
   }

   private String rowAsString(List<String> row) {
      StringBuffer buffer = new StringBuffer();

      buffer.append("|");
      for (String current : row)
         buffer.append(String.format("%s|", current));

      return buffer.toString();
   }
}
package com.om.example.dvr.fixtures;

import java.text.ParseException;
import java.util.Collections;
import java.util.List;

import com.om.example.dvr.domain.Program;

public class CreateOneDayProgramGuide {
   private ProgramGuideRowParser parser;

   public CreateOneDayProgramGuide(String startTime, String date) throws ParseException {
      parser = new ProgramGuideRowParser(date, startTime);
   }

   public List<?> doTable(List<List<String>> table) {
      table.remove(0);

      for (List<String> row : table)
         handleOneRow(row);

      return Collections.emptyList();
   }

   private void handleOneRow(List<String> row) {
      List<Program> programs = parser.parse(row);

      for (Program program : programs)
         AddProgramsToSchedule.getSchedule().addProgram(program);
   }
}

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

" Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.