Test driven programming on GUI, in a hard way
Original on Live Space:Fri Apr 21 01:56:08 CST 2006
I was fascinated with XP(eXtreme Programming) from day 0. But I often deal with GUI and it's hard to do XP in that domain.
Here is a real-life sample for your reference.
Here is a real-life sample for your reference.
Problem: We need to display images on the screen, one images per line but their height may vary. User can scroll up and down, the image might be change (and with a new height) by external event.
Make sure the current selected image can be fully displayed unless it's taller than screen. In this case, just display the upper part.
Make sure leave no empty gap in the bottom unless the total size of all images are shorter than the screen.
Make sure the current selected image can be fully displayed unless it's taller than screen. In this case, just display the upper part.
Make sure leave no empty gap in the bottom unless the total size of all images are shorter than the screen.
Step 1: move all GUI related code out, and now our task becomes:
We have a list of images in a model, we can get height of image by dataModel.getItemAt(i).getHeight().
We can get the image count by dataModel.size().
We have the following global vars:
int selectedItemIndex; // index of current selected image.
int firstDisplayedItemIndex; // index of the 1st visible image in the screen.
int offY; // the offset of 2 points, one point is the top left of the 1st visible image, another is the top left of the screen.
We need to write a method to change firstDisplayedItemIndex, offY.
void calcPosition(int availableHeight)
We can get the image count by dataModel.size().
We have the following global vars:
int selectedItemIndex; // index of current selected image.
int firstDisplayedItemIndex; // index of the 1st visible image in the screen.
int offY; // the offset of 2 points, one point is the top left of the 1st visible image, another is the top left of the screen.
We need to write a method to change firstDisplayedItemIndex, offY.
void calcPosition(int availableHeight)
Comment: we can even exclude dataModel from the method and make it a math quiz, but it's good enough for me by now.
Step 2: Design test cases.
Basically we will provide initial value for firstDisplayedItemIndex and selectedItemIndex, and provide some image height data, after we call the method, we will check firstDisplayedItemIndex and offY to make sure they are correct.
I try my best to setup 17 test cases:
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is taller than screen
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is same height as screen
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is shorter than screen, and we can fully display the next image
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is shorter than screen, and we can't fully display the next image
// no scroll needed, we can fully display the last image on screen
// no scroll needed, we can't fully display the last image on screen
// no scroll needed, but selectedItemIndex would be the last image on screen.
// selectedItemIndex would be the last image on screen, and firstDisplayedItemIndex will increased by 1. (push 1 image out of screen top).
// selectedItemIndex would be the last image on screen, and firstDisplayedItemIndex will increased by 2. (push 2 images out of screen top).
// before: selectedItemIndex is the last image on screen. After: it should be the 1st on screen. selectedItemIndex is taller than screen
// before: selectedItemIndex is the last image on screen. After: it should be the 1st on screen. selectedItemIndex is same height as screen
// before: selectedItemIndex is the last image on screen. After: it should be the 1st on screen. selectedItemIndex is shorter than screen.
// selectedItemIndex is the last one in model, no scroll needed
// selectedItemIndex is the last one in model. After method, firstDisplayedItemIndex will increased by 1.
// selectedItemIndex is the last one in model. After method, firstDisplayedItemIndex will increased by 2.
// selectedItemIndex is the last one in model. And we have to scroll back a little bit to fit the bottom gap.
// selectedItemIndex is the last one in model. And we can display all images and still have bottom gap.
I try my best to setup 17 test cases:
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is taller than screen
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is same height as screen
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is shorter than screen, and we can fully display the next image
// selectedItemIndex == firstDisplayedItemIndex, selectedItemIndex is shorter than screen, and we can't fully display the next image
// no scroll needed, we can fully display the last image on screen
// no scroll needed, we can't fully display the last image on screen
// no scroll needed, but selectedItemIndex would be the last image on screen.
// selectedItemIndex would be the last image on screen, and firstDisplayedItemIndex will increased by 1. (push 1 image out of screen top).
// selectedItemIndex would be the last image on screen, and firstDisplayedItemIndex will increased by 2. (push 2 images out of screen top).
// before: selectedItemIndex is the last image on screen. After: it should be the 1st on screen. selectedItemIndex is taller than screen
// before: selectedItemIndex is the last image on screen. After: it should be the 1st on screen. selectedItemIndex is same height as screen
// before: selectedItemIndex is the last image on screen. After: it should be the 1st on screen. selectedItemIndex is shorter than screen.
// selectedItemIndex is the last one in model, no scroll needed
// selectedItemIndex is the last one in model. After method, firstDisplayedItemIndex will increased by 1.
// selectedItemIndex is the last one in model. After method, firstDisplayedItemIndex will increased by 2.
// selectedItemIndex is the last one in model. And we have to scroll back a little bit to fit the bottom gap.
// selectedItemIndex is the last one in model. And we can display all images and still have bottom gap.
Comment: some test cases are not necessary, but the great thing is algorithm might jump out when you prepare tests!
Step 3: Write the test code (and framework!).
I don't have JUnit here for my J2ME project, so I write my own simpler version.
class Test
{
int [][] testData={
// firstDisplayedItemIndex, selectedItemIndex, window height,
// new firstDisplayedItemIndex, offY and height data for test!
{0, 0, 10, 0, 0, 11, 2, 3, 4, 5, 6 },
{0, 0, 10, 0, 0, 1, 2, 3, 4, 5, 6 },
{0, 0, 9, 0, 0, 1, 2, 3, 4, 5, 6 },
{2, 5, 8, 3, -1, 1, 1, 1, 2, 1, 6 },
... // more test cases are removed.
};
final int OFFSET =5;
{
int [][] testData={
// firstDisplayedItemIndex, selectedItemIndex, window height,
// new firstDisplayedItemIndex, offY and height data for test!
{0, 0, 10, 0, 0, 11, 2, 3, 4, 5, 6 },
{0, 0, 10, 0, 0, 1, 2, 3, 4, 5, 6 },
{0, 0, 9, 0, 0, 1, 2, 3, 4, 5, 6 },
{2, 5, 8, 3, -1, 1, 1, 1, 2, 1, 6 },
... // more test cases are removed.
};
final int OFFSET =5;
public static void main(String[] arg)
{
Test inst = new Test();
for (int i=0; i<inst.testData.length; i++)
{
inst.testWrapper(i);
}
}
{
Test inst = new Test();
for (int i=0; i<inst.testData.length; i++)
{
inst.testWrapper(i);
}
}
int availableHeight, selectedItemIndex, firstDisplayedItemIndex, offY;
int currentCaseId;
int currentCaseId;
void testWrapper(int caseId)
{
// init
currentCaseId = caseId;
firstDisplayedItemIndex=testData[caseId][0];
selectedItemIndex=testData[caseId][1];
availableHeight=testData[caseId][2];
{
// init
currentCaseId = caseId;
firstDisplayedItemIndex=testData[caseId][0];
selectedItemIndex=testData[caseId][1];
availableHeight=testData[caseId][2];
// run the method to test!
calcPosition(availableHeight);
calcPosition(availableHeight);
// verify result
if (testData[caseId][3]!=firstDisplayedItemIndex ||
testData[caseId][4]!=offY)
if (testData[caseId][3]!=firstDisplayedItemIndex ||
testData[caseId][4]!=offY)
{
System.out.println("error caseId:"+caseId);
}
}
}
System.out.println("error caseId:"+caseId);
}
}
}
Comment: we don't have the most powerful tools, but it works for us!
Step 4: Code and debug.
I copy method calcPosition(int availableHeight) into my test class and do a little modification:
Change all code from:
dataModel.getItemAt(i).getHeight();
to:
testData[currentCaseId][i+OFFSET];
and from:
dataModel.size()
to:
testData[currentCaseId].length-OFFSET
Change all code from:
dataModel.getItemAt(i).getHeight();
to:
testData[currentCaseId][i+OFFSET];
and from:
dataModel.size()
to:
testData[currentCaseId].length-OFFSET
Not surprised, my test class keep reporting error but it's very fast for me to debug and correct.
Step 5: copy code back.
After the Test class stop complain, I copy the method back and change all testData stuffs back to dataModel stuffs.
After the Test class stop complain, I copy the method back and change all testData stuffs back to dataModel stuffs.
Conclusion:
Even in GUI related code, even without powerful tool, we can still work in a test-driven way.
It's not that painful!
Even in GUI related code, even without powerful tool, we can still work in a test-driven way.
It's not that painful!
Tag: XP agile GUI sample JUnit

0 Comments:
Post a Comment
<< Home