Kelly Anderson recently sent over a layout manager for C# that models the Boxes and Glue method from TeX. For those of you not aware of that, the general concept as I understand it is having a UI made up of boxes that can resize dynamically and stay in relative position to each other based on “glue” between them. Apparently to fully grasp it you should be able to natively understand $y^3\alpha_x \to \beta$
(which looks a whole lot easier to read then
).
The layout manager is available under a Berkley License from http://www.cornetdesign.com/files/gm.zip
Anyway, the code came because of a discussion we’ve been having on the TDD and TFUI lists about Test-Driving GUI development, and what aspects should actually be tested. Of course, we all agree that one should use a healthy dose of MVP and push as much business and presentation logic to the presenter as possible. But what do you do with the views?
Well, first we should start with some options. Let’s assume we have a C# Winforms app. It looks like:
Ok, so it’s ugly. It happens to be the Example app Kelly sent me. Anyway, we’ll say that it has logic that when a user clicks on Button1, and then Button2, it displays a message telling them they won.
Otherwise, it hides the button before the last one they clicked:
Ok, so the business logic is (theoretically) easy enough to test. But what about the view itself, the thing that made the (not-so)pretty pictures above?
Well, our options to test that the layout is working would be:
- Manually look at it
- Do screen shots and use an image comparison utility to compare them
- Use something like NUnitForms to verify the controls are present (but maybe not the layout)
- Don’t. It’s too simple to break
Well, as the above list implies, maybe there is more to testing the view than the layout. For example, one of the keys to having robust NUnitAsp tests is to only test for the existance of things that need to be there and basically trying to keep your tests as small as possible so that you don’t have these large, fragile tests.
Kelly’s hypothesis, as I understood it, was that by using a layout manager you could test the layout of the controls, and know that they sized themselves correctly. So, perhaps one is not quite at 100% confidence that the tests are going to be able to validate everything is kosher, but by verifying the presence and layout of the controls, one can be fairly confident in making changes, with maybe some minor manual testing just to get the initial verification level up.
I decided to test this out with the layout manager. Using the above game I created, I decided that it needed a new feature where the buttons switched positions. Basically the last button, instead of hiding, would switch positions with the button the user just clicked. So if we had:
3|4
—
2|1
and the user clicked on buttons 1 and then 3, it would end up with:
1|4
—
2|3
and if they then clicked on button 4, it would swap positions again:
1|3
—
2|4
Well, now it looks like we have some tests! The first test I want to write is:
[Test]
public void Button2IsLeftOfButton1()
{
}
Let’s see what the Layout Manager Kelly sent over has to offer. Well, his LayoutContainerBase
has a GetAlignment()
that returns a ContentAlignment
object, which happens to be an enum
with things like BottomCenter
and TopRight
. My plan is to use that to verify that two controls are next to each other. The logic will be that controls on the same row can be left/right of each other, and controls on the same column can be top/bottom of each other. Let’s finish out that test:
[Test]
public void Button2IsLeftOfButton1()
{
ContentAlignment button1Alignment = gridLayoutPanel1.GetAlignment(button1);
ContentAlignment button2Alignment = gridLayoutPanel1.GetAlignment(button2);
Assert.IsTrue(ControlIsLeftOfControl(button2Alignment, button1Alignment));
}
private bool ControlIsLeftOfControl(ContentAlignment control1, ContentAlignment control2)
{
return false;
}
Ok, so we get the Red Bar. This should be easy enough, right? Let’s see, a little implementation here:
private bool ControlIsLeftOfControl(ContentAlignment control1, ContentAlignment control2)
{
string possibleLeftControl = control1.ToString();
string possibleRightControl = control2.ToString();
int leftControlRow = GetRowFromControlString(possibleLeftControl);
int rightControlRow = GetRowFromControlString(possibleRightControl);
int leftControlColumn = GetColumnFromControlString(possibleLeftControl);
int rightControlColumn = GetColumnFromControlString(possibleRightControl);
return leftControlRow == rightControlRow && leftControlColumn < rightControlColumn;
}
a little more there:
private int GetRowFromControlString(string str)
{
if(str.IndexOf("Bottom") > -1)
{
return 0;
}
else if(str.IndexOf("Middle") > -1)
{
return 1;
}
else if(str.IndexOf("Top") > -1)
{
return 2;
}
return -1;
}
private int GetColumnFromControlString(string str)
{
if(str.IndexOf("Left") > -1)
{
return 0;
}
else if(str.IndexOf("Center") > -1)
{
return 1;
}
else if(str.IndexOf("Right") > -1)
{
return 2;
}
return -1;
}
and voila! Red Bar! ;) (Smacks self on head). Where’s a pair partner where you need it. More importantly, where are the tests for all that crazy logic?!
Ok, let’s back up. Let’s see if this is doing what I want. Let’s start by testing the same row and same column. I definately need to extract those out into methods:
[Test]
public void TestFakeControlStringAlignmentShouldBeOnSameRow()
{
string fakeControl1Alignment = "BottomLeft";
string fakeControl2Alignment = "BottomRight";
Assert.IsTrue(ControlStringsAreOnSameRow(fakeControl1Alignment, fakeControl2Alignment), "Controls not on same row");
}
private bool ControlStringsAreOnSameRow(string control1, string control2)
{
int leftControlRow = GetRowFromControlString(control1);
int rightControlRow = GetRowFromControlString(control2);
return leftControlRow == rightControlR
ow;
}
Ok that got me a green bar for that test. Now, lets see about that left/right thing:
[Test]
public void TestFakeControlStringAlignment1ShouldBeLeftOfAlignment2()
{
string fakeControl1Alignment = "BottomLeft";
string fakeControl2Alignment = "BottomRight";
int control1Column = GetColumnFromControlString(fakeControl1Alignment);
int control2Column = GetColumnFromControlString(fakeControl2Alignment);
Assert.IsTrue(control1Column < control2Column, "Control 1 not left of (less than) Control 2");
}
And green bar for that test as well! I’m on the edge of using the debugger, which means something is *way* too complicated here. Time to step back for a second. And yep, there it is. I’m making the assumption that control1.ToString()
for a ContentAlignment
will give me something like “BottomRow”. Which, my guess is, it isn’t. Let’s just check and see:
[Test]
public void Button1ContentAlignmentToStringIsInARow()
{
ContentAlignment button1Alignment = gridLayoutPanel1.GetAlignment(button1);
Assert.IsTrue(GetRowFromControlString(button1Alignment.ToString()) > -1,
String.Format("Content Alignment string '{0}' not in a row"), button1Alignment.ToString());
}
Which green bars. I even cheated and Console.WriteLine
d it out, and sure enough, it’s TopLeft. Wait! TopLeft? On the screen shot I have it’s on the bottom right! And imagine that, I started with too coarse a test and sure enough it bit me. So, let’s do:
[Test]
public void Button1ContentAlignmentShouldBeBottomRight()
{
ContentAlignment button1Alignment = gridLayoutPanel1.GetAlignment(button1);
Assert.AreEqual("BottomRight", button1Alignment.ToString(),
String.Format("Button1 alignment '' is incorrect", button1Alignment.ToString()));
}
[Test]
public void Button2ContentAlignmentShouldBeBottomLeft()
{
ContentAlignment button2Alignment = gridLayoutPanel1.GetAlignment(button2);
Assert.AreEqual("BottomLeft", button2Alignment.ToString(),
String.Format("Button2 alignment '' is incorrect", button2Alignment.ToString()));
}
Which fail saying that Button1 is at TopLeft, and Button2 is at MiddleCenter. And looking at the property windows, it appears to be some default thing, as Buttons 3 and 4 are also at TopLeft.
Well, I guess the layout manager isn’t quite as straightforward as I thought. But it taught me a heck of a lesson. When you try to leap too far ahead, it can bite you hard. In this case, Nearly 2 and a half hours of work for not putting in one test. I think I’ll sleep on this some more and give it a try over the weekend.
Cory,
The point of the layout manager is not to make TDD of GUIs easier, but rather to make the resulting dialogs more powerful without the necessity of adding additional code into the business layer.
In other words, Microsoft should have provided a layout manager (and indeed will in the next version of Windows) and since they didn’t, I did.
Testing GUIs is a tricky business, and I think you bit off a pretty big chunk on your first time out, as your results show.
What would be interesting is creating tests to test the library itself. If you have three items in a horizontal box each with a minimum size of 10, the left and middle with a maximum size of a hundred, and the right item with a size of 1000, then when the box is stretched to 1200, are the first two elements now 100 pixels wide, and the third element 1000 pixels wide?
That’s the sort of test that I thought might be interesting.
-Kelly