Saturday, January 23, 2010

Creating a C# Window Inspector to parallel WindowLicker in the Java #goos sample code (updated)

This morning, I started into chapter 15 of Growing Object-Oriented Software, Guided by Tests. At this point, reliance on WindowLicker in the Java code to inspect the changes happening in the GUI layer is increasing. I had been avoiding this by simply using a mock IAuctionSniperView interface, and validating that the interface's Status string property had been set.

However, this creates a few problems:
1) The C# code isn't fully parallel to the Java sample code
2) In the book, the end-to-end/acceptance tests operate at single level of scope, calling either ApplicationRunner or FakeAuctionServer to validate each step of the test. By using a mocked interface, I had to place the mock expected actions (replay and verify) at the top level (rather than inside AppicationRunner).
3) And, of course, it's not truly an end-to-end test, as it stops at the view interface.

I decided to experiment with writing some tests in a new project to see what the minimal amount of code would be necessary to create a simple WinForm inspector that could start simply by observing the activity of the status Label being set.

After a few false starts (and needing to review my knowledge of the ParmeterizedThreadStart class) I managed to get this working and displaying a label.

The tests:


[TestFixture]
public class WinFormInspectorTests
{
private WinFormInspector _winFormInspector;

[SetUp]
public void Setup()
{
_winFormInspector = new WinFormInspector(new Main());
}

[Test]
public void Inspector_Can_Instantiate_WinForm()
{
Assert.IsNotNull(_winFormInspector.Main);
}

[Test]
public void Inspector_Can_Launch_Application()
{
_winFormInspector.LaunchApplication();
_winFormInspector.SleepApplication(1000);
_winFormInspector.QuitApplication();
}

[Test]
public void Inspector_Can_Observe_Status_Label()
{
const string status = "Lost";

_winFormInspector.LaunchApplication();
_winFormInspector.Main.SniperStatus = status;
_winFormInspector.ShowsSniperStatus(status);
_winFormInspector.SleepApplication(1000);
_winFormInspector.QuitApplication();
}
}


The WinFormInspector class:


public class WinFormInspector
{
private readonly Main _main;
private Thread _thread;


public WinFormInspector(Main main)
{
_main = main;
}

public Main Main
{
get { return _main; }
}

public void ShowsSniperStatus(string expectedStatus)
{
if (!_main.SniperStatus.Equals(expectedStatus))
{
throw new Exception("Expected status does not match SniperStatus label.");
}
}

public void LaunchApplication()
{
_thread = new Thread(new ParameterizedThreadStart(Launch));
_thread.Start(this.Main);
}

public void SleepApplication(int sleepMilliseconds)
{
Thread.Sleep(sleepMilliseconds);
}

public void QuitApplication()
{
this.Main.Close();
Application.Exit();
}

private static void Launch(object input)
{
var form = (Form)input;
Application.Run(form);
}
}


...and the WinForm class, "Main":


public class Main : Form
{
private readonly Label _lblStatus;

public Main()
{
_lblStatus = new Label();
this.Controls.Add(_lblStatus);
}

public string SniperStatus
{
get
{
return _lblStatus.Text;
}
set
{
_lblStatus.Text = value;
}
}
}


My next step is to move this over into the AuctionSniper C# sample code project. One of the things I'm debating is whether to keep the mocked view tests as well.

No comments: