Port of schuchert.wikispaces.com


FitNesse.Tutorials.2

FitNesse.Tutorials.2

Introduction

A Query table is a means of performing a single query and verifying the results. A typical test might use Slim Decision Tables to insert a large data set and then Query tables to verify that the correct sub-set of the data is returned from the query.

This tutorial begins with a basic introduction of Query tables, but it assumes a basic understanding of Decision tables. If you are not familiar with Decision Tables, work through this tutorial first. Along the way, we’ll look at what it takes to produce query results manually and then review a small tool available from github to produce these results automatically.

As a final note, this tutorial picks up where this tutorial left off. However, you can start with these source files using the tag FitNesse.Tutorials.2.Start, see here for details:

Beginning

Consider the following user story:

To test this functionality, it looks like we need to check several things, here are a few of those things:

We’re going to grow our way into this. Before we can do that, we need to create a program schedule. You’ve already solved this problem in the previous tutorial(right?). So all we need to do is use a previous fixture and create original programming. Rather than try to create real programs and episodes, this example just creates a large amount of data and it also includes the configuration stuff:

|Add Programs To Schedule                         |
|name|episode|channel|date     |start time|minutes|
|P1  |E1     |7      |5/12/2008|7:00      |60     |
|P1  |E1     |7      |5/12/2008|10:00     |60     |
|P1  |E2     |7      |5/13/2008|7:00      |60     |
|P1  |E3     |7      |5/14/2008|7:00      |60     |
|P1  |E4     |7      |5/15/2008|7:00      |60     |
|P1  |E5     |7      |5/16/2008|7:00      |60     |
|P1  |E6     |7      |5/17/2008|7:00      |60     |
|P2  |E1     |5      |5/12/2008|7:00      |60     |
|P2  |E2     |5      |5/13/2008|7:00      |60     |
|P2  |E3     |5      |5/14/2008|7:00      |60     |
|P2  |E4     |5      |5/15/2008|7:00      |60     |
|P2  |E5     |5      |5/16/2008|7:00      |60     |
|P2  |E6     |5      |5/17/2008|7:00      |60     |
|P1  |E1     |9      |5/17/2008|7:00      |60     |

The goal of this table is to create several entires in the program schedule. However, the Fixture as written from the previous tutorial performs the actual creation in the created() method. We have a few options:

There are various forces driving this decision. For example, what happens if you accidentally have cross-test chatter and a previous test causes problems with this test data? If you do not indicate the problem as it happens (fast fail), then it might be unintuitive just what problem is causing the test to fail.

Also consider this, AddProgramsToSchedule was created early in this project. Fixtures will get created, mature and sometimes even disappear. It might be worth making this fixture a little more flexible. A simple fix would be to:

For this table to actually do anything, you must make some decision on how to proceed. For the purpose of moving this tutorial forward, I’m going with the option just described. Here are the changes to the fixture:

public class AddProgramsToSchedule {
   // snip
   private boolean lastCreationSuccessful;

   // snip
   public void execute() {
      try {
         Program p = schedule.addProgram(programName, episodeName, channel,
               buildStartDateTime(), minutes);
         lastId = p.getId();
         lastCreationSuccessful = true;
      } catch (ConflictingProgramException e) {
         lastCreationSuccessful = false;
      }
   }

   public boolean created() {
      return lastCreationSuccessful;
   }

   public String lastId() {
      if (lastCreationSuccessful)
         return lastId;
      return "n/a";
   }
}

Since you’ve just changed the fixture, you should go back to your DecisionTableExample page and verify that the test still passes. In fact, you’ll be making additional changes to this fixture as this tutorial proceeds. It might be a good idea to make it convenient to run all of the tests at the same time. Before moving forward, however, make sure the DecisionTableExample page still successfully passes.

Introducing a Test Suite

A test suite is simply a page above other pages that is set to be a suite. FitNesse will look at all of its children and execute the pages under it that are set to test pages. To do this, you’ll need to create the suite and move existing pages under it:

>DigitalVideoRecorderExamples
>DigitalVideoRecorder
>DecisionTableExample

Removing Future Duplication

The definition of the TEST_SYSTEM, !path and import statement will be the same for the pages we create during these tutorials. Right now it is duplicated across FirstExample and DecisionTableExample. We can put the TEST_SYSTEM and !path in the DigitivalVideoRecorderExamples and it will be inherited by FirstExample, DecisionTableExample and any other sub-pages.

We can also create a SetUp page as a sibling of DecisionTableExample and its contents will be part of the children of its parent page (its siblings)

Update your DigitalVideoRecorderExamples page to define the test system and path:

!contents -R2 -g -p -f -h

!define TEST_SYSTEM {slim}

!path fitnesse.jar
!path /Users/schuchert/src/fitnesse-tutorials/DVR/bin

!define COLLAPSE_SETUP {true}
!define COLLAPSE_TEARDOWN {true}

FitNesse import tables are not implicitly inherited. The import statements must actually execute on that page. You can do this in one of three ways:

We’ll use a SetUp page.

Creating a SetUp Page

There are many ways to create pages in FitNesse. You can:

We want to create a SetUp page that will be available for all pages under DigitalVideoRecorder, so:

|import|
|com.om.example.dvr.fixtures|

Summary of Page Hierarchy

Now as you create new test pages, put them under DigitalVideoRecorderExample and they will automatically pick up:

Back to a New Test

Near the top of this tutorial, there was a table with a lot of data. You have not yet created that page. Now we have a place to create that page. So you do not have to scroll back, here’s that table again:

|Add Programs To Schedule                         |
|name|episode|channel|date     |start time|minutes|
|P1  |E1     |7      |5/12/2008|7:00      |60     |
|P1  |E1     |7      |5/12/2008|10:00     |60     |
|P1  |E2     |7      |5/13/2008|7:00      |60     |
|P1  |E3     |7      |5/14/2008|7:00      |60     |
|P1  |E4     |7      |5/15/2008|7:00      |60     |
|P1  |E5     |7      |5/16/2008|7:00      |60     |
|P1  |E6     |7      |5/17/2008|7:00      |60     |
|P2  |E1     |5      |5/12/2008|7:00      |60     |
|P2  |E2     |5      |5/13/2008|7:00      |60     |
|P2  |E3     |5      |5/14/2008|7:00      |60     |
|P2  |E4     |5      |5/15/2008|7:00      |60     |
|P2  |E5     |5      |5/16/2008|7:00      |60     |
|P2  |E6     |5      |5/17/2008|7:00      |60     |
|P1  |E1     |9      |5/17/2008|7:00      |60     |

Create this page:

Now we need to create a season pass. That’s a new table and fixture. Here’s a table:

|Create Season Pass For|P1|9|
|id of program scheduled?   |
|$ID=                       |

This table’s goal is to send a message to the production code to create a season pass for the program named p1 on channel 9. When this happens, I want to have available the ID of the program found to use later. The first row provides the name and constructor arguments. The next row indicates calling a method called idOfProgramScheduled(), whose return will provide that information, which FitNesse will then assign to the symbol ID.

Now, when this happens, what do we expect for results? I could provide a description in text of my expectations, but better yet, I’ll express it as an expected result:

|query:Episodes In To Do List|$ID                |
|episodeName                 |date     |startTime|
|E1                          |5/17/2008|7:00     |

This is a simple expected result. I could have chosen p1 on channel 7, which we’ll do next, but it involves much more logic. This first test will get the basic infrastructure in place. We’ll then take a diversion to using a utility to help generate results, then we’ll work on a more difficult case.

Add the previous two tables to your existing page. If you execute the test, you’ll notice that the bottom two tables fail.

Create Missing Fixtures

Round 1 is simply getting a fixture that will make this test pass. To do this, the fixture will hard-code the results. Why do I do this? The structure that needs to be returned is complex enough that just looking at it first is enough to consider.

Execute the test with these two new tables. You’ll find you need to create two fixtures:

CreateSeasonPassFor.java

package com.om.example.dvr.fixtures;

public class CreateSeasonPassFor {
   public CreateSeasonPassFor(String programName, int channel) {
   }

   public String idOfProgramScheduled() {
      return "n/a";
   }
}

EpisodesInToDoList.java

package com.om.example.dvr.fixtures;

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

public class EpisodesInToDoList {
   public EpisodesInToDoList(String programId) {
   }

   public List<Object> query() {
      return Collections.emptyList();
   }
}

Create these two fixtures and execute the test. While it is not passing, this is a good start. Next, we’ll actually update one fixture to get the production test passing:

EpisodesInToDoList.java

package com.om.example.dvr.fixtures;

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

public class EpisodesInToDoList {
   public EpisodesInToDoList(String programId) {
   }

   private List<Object> list(Object... objs) {
      LinkedList<Object> result = new LinkedList<Object>();

      for (Object current : objs)
         result.add(current);

      return result;
   }

   public List<Object> query() {
      return
         list(
            list(
               list("episodeName", "E1"),
               list("date",        "5/17/2008"),
               list("startTime",   "7:00")
            )
         );
   }
}

Update your fixture and verify that your your test page passes all assertions.

What is this doing?

A key design element of Slim is simplicity at the protocol level. Fit, the original text executor, was written as a complete solution. It takes in (among other things) HTML tables, executes them, then returns HTML tables. Slim takes in lists, executes results and then returns lists. All formatting is done by FitNesse. This makes Slim smaller and therefore easier to maintain and port than Fit.

A side effect of this smaller system boundary for Slim results in a somewhat low-level expected return from the Query method. The structure of the output is a three-tiered list:

While this is a generic result, it is also a bit difficult to build. This example makes it easy because the result is hard-coded. But notice that you’d have to format the date and time information to match the expectations of the query table.

It will get easier to generate these results. However, to get there will require several steps.

Switch to Unit Testing

This next step requires a lot of work. We want to generate correct results, which we then feed back to Fixture. We could attempt to simply drive this from FitNesse, and I’ve done it successfully. However, the amount of time between tests passing is too long and therefore too risky. So these next steps move from Story Test writing into Unit Test writing.

Here’s what we need to have happen:

In the two types of DVR’s I’ve owned, there’s been something called the “Season Pass Manager.” So this is where we can start:

SeasonPassManagerTest.java

package com.om.example.dvr.domain;

import static org.junit.Assert.assertEquals;

import java.util.Calendar;
import java.util.Date;

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

public class SeasonPassManagerTest {
   private SeasonPassManager seasonPassManager;
   private Schedule schedule;

   private Date createDate(int year, int month, int day, int hour, int min) {
      Calendar calendar = Calendar.getInstance();
      calendar.clear();
      calendar.set(Calendar.YEAR, year);
      calendar.set(Calendar.MONTH, month);
      calendar.set(Calendar.DAY_OF_MONTH, day);
      calendar.set(Calendar.HOUR, hour);
      calendar.set(Calendar.MINUTE, min);

      return calendar.getTime();
   }

   @Before
   public void init() {
      schedule = new Schedule();
      schedule.addProgram("p1", "e1", 7, createDate(2008, 4, 5, 7, 0), 60);
      schedule.addProgram("p2", "e2", 7, createDate(2008, 4, 5, 8, 0), 60);
      seasonPassManager = new SeasonPassManager(schedule);
   }

   @Test
   public void AssertNewSeasonPassManagerHasEmptyToDoList() {
      assertEquals(0, seasonPassManager.sizeOfToDoList());
   }

   @Test
   public void schduleProgramWithOneEpisodeToDoListIs1() {
      seasonPassManager.createNewSeasonPass("p1", 7);
      assertEquals(1, seasonPassManager.sizeOfToDoList());
   }
}

SeasonPassManager.java

package com.om.example.dvr.domain;

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

public class SeasonPassManager {
   private final Schedule schedule;
   private List<Program> toDoList = Collections.emptyList();

   public SeasonPassManager(Schedule schedule) {
      this.schedule = schedule;
   }

   public int sizeOfToDoList() {
      return toDoList.size();
   }

   public void createNewSeasonPass(String programName, int channel) {
      toDoList = schedule.findProgramsNamedOn(programName, channel);
   }
}

Update: Schedule.java

   public List<Program> findProgramsNamedOn(String programName, int channel) {
      List<Program> result = new LinkedList<Program>();

      for(Program program : scheduledPrograms)
           if(program.timeSlot.channel == channel && program.programName.equals(programName))
              result.add(program);

      return result;
   }

Make these updates and verify that your two unit tests pass.

Wiring It Up

That was just enough to get the unit test passing. It may not seem complete, but there are no more story-based assertions that require more work, so the solution will be adequate. However, we next need to hook up the results in the fixture. That is, we need to replace:

   private List<Object> list(Object... objs) {
      LinkedList<Object> result = new LinkedList<Object>();

      for (Object current : objs)
         result.add(current);

      return result;
   }

   public List<Object> query() {
      return
         list(
            list(
               list("episodeName", "E1"),
               list("date", "5/17/2008"),
               list("startTime", "7:00")
            )
         );
   }

We need to replace that with code that will turn an object or list into a list into a list of list of list of strings. There is a utility available that can help: github - Query Result Builder. You can download and build the jar file, or you can simple download the following two jar files and add them to your class path in both your IDE and FitNesse:

Rather than describe this in great detail (you can review the source and embedded unit tests), here is a first example:

Update: Add Unit Test to SeasonPassManagerTest.java

   @Test
   public void queryResultBuilderCanTranslateToDoListCorrectly() {
      seasonPassManager.createNewSeasonPass("p1", 7);

      QueryResultBuilder builder = new QueryResultBuilder(Program.class);
      QueryResult result = builder.build(seasonPassManager.toDoListIterator());
      List<Object> renderedObjects = result.render();
   }

Note, this example requires the addition of one more method to SeasonPassManager.java:

Update: SeasonPassManager.java

   public Iterable<?> toDoListIterator() {
      return toDoList;
   }

This is close to what we need. What the builder will do is take each bean-field in the Program class and put in into the query result object. To make this test run (not really pass, since it’s just exercising/demonstrating the use of the Query Result Builder, there are no assertions):

Make these changes and run your unit tests. You should notice three passing tests.

This last bullet is an important one. The Query Result Builder as written will not simply return values from public fields, which is how the classes are presented. Also, by default, the QueryResultBuilder converts non-null objects via toString(). If you review the query table, it has three fields:

Not to worry, we can promote those fields up to our results in one of two ways:

The first option might seem simple, but it will involve putting fixture-specific code in our domain object, which is a bad idea. Instead, we’ll create a custom property handler to perform the promotion of the fields instead:

Create: TimeSlotPropertyHandler.java

package com.om.example.dvr.fixtures;

import java.text.SimpleDateFormat;
import java.util.Date;

import com.om.example.dvr.domain.TimeSlot;
import com.om.query.domain.ObjectDescription;
import com.om.query.handler.PropertyHandler;
import com.om.reflection.PropertyGetter;

public class TimeSlotPropertyHandler extends PropertyHandler {
   static SimpleDateFormat dateFormat = new SimpleDateFormat("M/d/yyyy");
   static SimpleDateFormat timeFormat = new SimpleDateFormat("h:mm");

   @Override
   public void handle(PropertyGetter propertyGetter, Object targetObject,
         ObjectDescription objectDescription) {
      TimeSlot timeSlot = propertyGetter.getValue(targetObject, TimeSlot.class);

      Date startDateTime = timeSlot.startDateTime;

      objectDescription.addPropertyDescription("date", dateFormat.format(startDateTime));
      objectDescription.addPropertyDescription("startTime", timeFormat
            .format(startDateTime));
   }
}

Slightly Updated Test

This test, which you might have put in SeasonPassManagerTest should no longer be in that class. Why? The class you just created is in the fixtures package. The SeasonPassManagerTest is in the domain package. The domain package should not point to the fixtures package. So leave that test as it is and instead create a new test class:

Create: QueryResultBuilderExampleTest

package com.om.example.dvr.fixtures;

import static org.junit.Assert.assertEquals;

import java.util.Calendar;
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.Schedule;
import com.om.example.dvr.domain.SeasonPassManager;
import com.om.query.QueryResultBuilder;
import com.om.query.domain.QueryResult;

public class QueryResultBuilderExampleTest {
   private SeasonPassManager seasonPassManager;
   private Schedule schedule;

   private Date createDate(int year, int month, int day, int hour, int min) {
      Calendar calendar = Calendar.getInstance();
      calendar.clear();
      calendar.set(Calendar.YEAR, year);
      calendar.set(Calendar.MONTH, month);
      calendar.set(Calendar.DAY_OF_MONTH, day);
      calendar.set(Calendar.HOUR, hour);
      calendar.set(Calendar.MINUTE, min);

      return calendar.getTime();
   }

   @Before
   public void init() {
      schedule = new Schedule();
      schedule.addProgram("p1", "e1", 7, createDate(2008, 4, 5, 7, 0), 60);
      schedule.addProgram("p2", "e2", 7, createDate(2008, 4, 5, 8, 0), 60);
      seasonPassManager = new SeasonPassManager(schedule);
   }

   @Test
   public void queryResultBuilderCanTranslateToDoListCorrectly() {
      seasonPassManager.createNewSeasonPass("p1", 7);

      QueryResultBuilder builder = new QueryResultBuilder(Program.class);
      builder.register("timeSlot", new TimeSlotPropertyHandler());
      QueryResult result = builder.build(seasonPassManager.toDoListIterator());
      List<Object> renderedObjects = result.render();
      assertEquals(1, renderedObjects.size());
   }
}

Try running your unit tests. They will fail with the following exception:

com.om.reflection.PropertyDoesNotExistInBeanException: Propery: timeSlot,
             does not exist in: com.om.example.dvr.domain.Program
    at com.om.reflection.ReflectionUtil.getPropertyGetterNamed(ReflectionUtil.java:83)
    at com.om.query.QueryResultBuilder.register(QueryResultBuilder.java:93)
        // snip

This exception is telling you that when you tried register a property handler for timeSlot, there was no corresponding getter method. To get this to work, you will need to add some getter methods to Program:

Update: Program.java

   public String getProgramName() {
      return programName;
   }

   public String getEpisodeName() {
      return episodeName;
   }

   public TimeSlot getTimeSlot() {
      return timeSlot;
   }

Once you get your tests passing, remove the old version of the queryResultBuilderCanTranslateToDoListCorrectly test from the SeasonPassManagerTest.

Updating the Fixtures

To complete this wiring, you’ll need to make some updates to the fixtures:

Update: CreateSeasonPassFor.java

package com.om.example.dvr.fixtures;

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

public class CreateSeasonPassFor {
   private static SeasonPassManager seasonPassManager = new SeasonPassManager(
         AddProgramsToSchedule.getSchedule());
   private Program lastProgramFound;

   public static SeasonPassManager getSeasonPassManager() {
      return seasonPassManager;
   }

   public CreateSeasonPassFor(String programName, int channel) {
      lastProgramFound = seasonPassManager.createNewSeasonPass(programName, channel);
   }

   public String idOfProgramScheduled() {
      if (lastProgramFound != null)
         return lastProgramFound.getId();
      return "n/a";
   }
}

This also requires a change to SeasonPassManager:

Update: SeasonPassManager.java

   public Program createNewSeasonPass(String programName, int channel) {
      List<Program> programsFound = schedule.findProgramsNamedOn(programName, channel);

      toDoList = programsFound;

      if (programsFound.size() > 0)
         return programsFound.get(0);
      return null;
   }

Update: EpisodesInToDoList.java:

package com.om.example.dvr.fixtures;

import java.util.List;

import com.om.example.dvr.domain.Program;
import com.om.query.QueryResultBuilder;
import com.om.query.domain.QueryResult;

public class EpisodesInToDoList {
   private final String programId;

   public EpisodesInToDoList(String programId) {
      this.programId = programId;
   }

   public List<Object> query() {
      List<Program> programs = CreateSeasonPassFor.getSeasonPassManager()
            .toDoListContentsFor(programId);
      QueryResultBuilder builder = new QueryResultBuilder(Program.class);
      builder.register("timeSlot", new TimeSlotPropertyHandler());
      QueryResult result = builder.build(programs);
      return result.render();
   }
}

And finally, this requires another change to SeasonPassManager (overly simplistic, maybe, but enough for our tests):

Update: SeasonPassManager.java

   public List<Program> toDoListContentsFor(String programId) {
      return toDoList;
   }

Update Path

Your new code uses two jar files (downloaded from above). You need to add these to the class path:

!path /Users/schuchert/src/fitnesse-tutorials/DVR/lib/**.jar

After all of these changes, see if in fact your story test still passes. Now, go to your suite, and verify that all tests in your suite pass.

Expand the Test, Grow the Logic

Now it’s time to make sure the same program/episode on the same channel is not scheduled to record more than once.

Update the page to add another few tables at the bottom:

|Create Season Pass For|P1|7|
|id of program scheduled?   |
|$ID=                       |

|query:Episodes In To Do List|$ID                |
|episodeName                 |date     |startTime|
|E1                          |5/12/2008|7:00     |
|E2                          |5/13/2008|7:00     |
|E3                          |5/14/2008|7:00     |
|E4                          |5/15/2008|7:00     |
|E5                          |5/16/2008|7:00     |
|E6                          |5/17/2008|7:00     |

After adding these tables, run the test again. Notice that you have a surplus result. Why? What is the intention of this table? How can we make it more clear? To make this more clear we could:

The last option leads to more tests so there’s a balance between it and adding commentary. However, for this example you’ll split these tests into separate, well-named pages.

Refactor the Tests

|Add Programs To Schedule                         |
|name|episode|channel|date     |start time|minutes|
|P1  |E1     |7      |5/12/2008|7:00      |60     |
|P1  |E1     |7      |5/12/2008|10:00     |60     |
|P1  |E2     |7      |5/13/2008|7:00      |60     |
|P1  |E3     |7      |5/14/2008|7:00      |60     |
|P1  |E4     |7      |5/15/2008|7:00      |60     |
|P1  |E5     |7      |5/16/2008|7:00      |60     |
|P1  |E6     |7      |5/17/2008|7:00      |60     |
|P2  |E1     |5      |5/12/2008|7:00      |60     |
|P2  |E2     |5      |5/13/2008|7:00      |60     |
|P2  |E3     |5      |5/14/2008|7:00      |60     |
|P2  |E4     |5      |5/15/2008|7:00      |60     |
|P2  |E5     |5      |5/16/2008|7:00      |60     |
|P2  |E6     |5      |5/17/2008|7:00      |60     |
|P1  |E1     |9      |5/17/2008|7:00      |60     |
!contents -R2 -g -p -f -h
>SingleProgramPlacedInToDoListTest
>DuplicateEpisodeNotIncludedInToDoListTest
|Create Season Pass For|P1|9|
|id of program scheduled?   |
|$ID=                       |

|query:Episodes In To Do List|$ID                |
|episodeName                 |date     |startTime|
|E1                          |5/17/2008|7:00     |

The test fails! Why? It is not finding the import included in the original SetUp page. FitNesse does not inherit SetUp pages. It finds the nearest one and runs it. To make sure that the global setup (import statements) are included down here, update the QeuryTableExamples setup page. Add the following line to the top of the page:

!include <DigitalVideoRecorderExamples.SetUp
|Create Season Pass For|P1|7|
|id of program scheduled?   |
|$ID=                       |

|query:Episodes In To Do List|$ID                |
|episodeName                 |date     |startTime|
|E1                          |5/12/2008|7:00     |
|E2                          |5/13/2008|7:00     |
|E3                          |5/14/2008|7:00     |
|E4                          |5/15/2008|7:00     |
|E5                          |5/16/2008|7:00     |
|E6                          |5/17/2008|7:00     |
!contents -R2 -g -p -f -h

Fix the Production Code

To fix this problem, we need to make a few changes.

Add Method to: Program.java

   public boolean sameEpisodeAs(Program program) {
      return timeSlot.channel == program.timeSlot.channel
            && programName.equals(program.programName)
            && episodeName.equals(program.episodeName);
   }

Update: SeasonPassManager.java

package com.om.example.dvr.domain;

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

public class SeasonPassManager {
   private final Schedule schedule;
   private List<Program> toDoList = new LinkedList<Program>();

   public SeasonPassManager(Schedule schedule) {
      this.schedule = schedule;
   }

   public int sizeOfToDoList() {
      return toDoList.size();
   }

   public Program createNewSeasonPass(String programName, int channel) {
      List<Program> programsFound = schedule.findProgramsNamedOn(programName, channel);

      for (Program current : programsFound)
         if (!alreadyInToDoList(current))
            toDoList.add(current);

      if (programsFound.size() > 0)
         return programsFound.get(0);
      return null;
   }

   private boolean alreadyInToDoList(Program candidate) {
      for (Program current : toDoList)
         if (current.sameEpisodeAs(candidate))
            return true;

      return false;
   }

   public Iterable<?> toDoListIterator() {
      return toDoList;
   }

   public List<Program> toDoListContentsFor(String programId) {
      List<Program> result = new LinkedList<Program>();

      for (Program current : toDoList)
         if (current.getId().equals(programId))
            result.add(current);

      return result;
   }
}

We’re done right? Wrong!

You will see a failure. The test DuplicateEpisodeNotIncludedInToDoListTest works by itself and even under its immediate parent suite. However, it does not work when run in the whole suite. Why is that? What is the failure. When I originally came across this problem, in the back of my mind I was thinking I wanted to clear out the program schedule between tests. Now that “spider sense”, which has been tingling, is finally coming to fruition.

If you review why the test fails, the test was expecting:

Upon further review, the test DecisionTableExample inserts something on channel 7 at time 7:00, an episode of House M.D.. That test runs before DuplicateEpisodeNotIncludedInToDoListTest, and it causes an undesirable side-effect. This is an example of cross-test chatter.

We have some options:

Either of the last two options are fine. Given we have not created a TearDown page, that’s the option I’ll pick:

|Clear Program Schedule|

To make this work, you’ll need a matching fixture:

Create: ClearProgramSchedule.java

package com.om.example.dvr.fixtures;

public class ClearProgramSchedule {
   public ClearProgramSchedule() {
      AddProgramsToSchedule.getSchedule().clear();
   }
}

And finally, this requires you to add a method to Schedule:

Add Method To: Schedule.java

   public void clear() {
      scheduledPrograms.clear();
   }

If you do not like this, you could have alternatively updated the AddProgramsToSchedule to clear out the schedule by simply reassigning the static variable. In any case, run your tests and the who suite should pass.

Congratulations, you’ve finished this tutorial.

Summary

This was a fairly detailed tutorial. You learned several things about Query tables:

You also learned that there is a simple utility that will help you build query results. If you look at those jars, there are test files in both of them. You can review the tests to get an idea of how the QueryResultBuilder works, though you saw most of what you need in this one example.

You learned quite a bit about FitNesse:

You learned that sometimes jumping from FitNess down in to unit tests is the right thing to do. This tutorial didn’t do that as much at it could have, but it at least gave you an idea of when to do it.

There’s more you could do with this code, quite a bit more. For example, if you review SeasonPassManager, there’s a lot of feature envy on a missing class. Many of the methods directly manipulate a language-provided collection. That’s rip for an extract class refactoring.

Finally, you’ve experience test cross-chatter and one way to clean it up. That’s an especially important consideration. Tests should run on their own, in suites and not cause other tests to fail.

At this point, you’ve learned enough about FitNesse with the first three tutorials to be fairly effective. There’s more to learn, e.g.,

Even so, you can do quite a bit right now.


Comments

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