Back to Java, Introducing TFUI
I've recently moved from .NET-land onto a large, legacy Java project. The last time that I made the transition from Java to .NET, it really felt like taking a step backwards in terms of productivity -- largely due to the lack of Agile tool support. However, now with tools like ReSharper, TestDriven.NET, and (of course) CruiseControl.NET, the gap doesn't feel so large. Coming back onto a Java project really made me appreciate other things about .NET -- namely, that as .NET is a more recent platform, the codebases (at least the ones that I've worked on) have been newer, smaller, and developed using agile best practices such as TDD, constructor dependency injection and mocks.
My current project is quite a contrast: it's been under-development for over six years, and has been worked on by a large number of programmers of varying levels of experience over that time. The codebase is massive, overly complex, highly duplicated and relatively untested. Basically, it could fill several chapters in Michael Feather's Working Effectively with Legacy Code.
My first assignment was to help a team figure out how to start writing unit tests around the client application. Widget-level security was getting layered into the system and it was quite hard to simulate and test certain permutations and combinations of permissions. Plus, the interaction of and selective enabling/disabling of widgets complicated matters considerably. A perfect candidate for unit testing; only the client had not previously been unit tested and was not really amenable to unit testing. To try to do granular class-level unit testing would have required disentangling myraid dependencies and the code was too scary to work with directly.
I opted instead to take a layer-based testing approach. By mocking out the client's dependencies, I could write tests around the client without having to alter the client code (at least not until I had a reasonable battery of tests in place). Fortunately, all client-server communication was performed through a single service gateway (ServerSession) that was passed into each screen class. This made it easy to inject a stub, letting me test the client layer in isolation. The communication between the client and the server was still quite complex; however, most of the calls simply retrieved reference data. After building a set of reusable factories to generate and return this data, the only set up for each test was the data required for the specific screen-under-test.
The first step in writing these client tests was to just try instantiating a screen class using the stub gateway. Failures in the test identified what data was required from the server to be stubbed by the test. Next, I borrowed a page from Phlip Plumlee's work on test-first user interfaces (TFUI) by building a reveal() method. As each screen class extends from JPanel, the reveal() method simply inserts the panel into a JDialog and pops it up. Here's what an example test would look like (translated into C#):
public class ScreenTest : ClientTestCase {
[Test]
public void CreateScreen() {
Screen screen = new Screen(new ServerSessionStub());
reveal(screen);
}
}
At this point, the developer can inspect the screen, including the state of its widgets, and can interact with the screen as if they had launched the client. It is a great way to conduct exploratory testing for existing screens. The developer can then write a set of assertions (assertWidgetIsEnabled, assertWidgetIsVisible, etc.) that verify the expected state of the widgets on the screen. In typical TDD fashion, this gives us a failing test to fix before going in and attaching security to the widgets-under-test. The developer just needs to remember to remove the reveal method call before committing their changes (in Java, we use the JVM argument java.awt.headless=true to prevent windows from popping up and hanging the build -- I don't know of a .NET equivalent).
One additional benefit of this approach is that it drastically reduced our code-and-verify cycle. Previously, any changes to the client required manual verification; given the complexity of the application, it was quite time-consuming to navigate to the necessary screen to validate the changes. And of course, now the desired functionality is captured in automated tests, protecting us from regression.
The ability to reveal individual screens through unit tests is something that I am going to continue to look to add on future projects -- certainly on .NET ones. It is a very powerful technique. From the .NET perspective, I'm also hoping that this approach might wean us off our dependency on the Forms Designers. My experiences working with the Visual Studio.NET 2003 WinForms designer has been painful at best. Aside from the fact that it tends to break on complex screens containing custom controls (deleting screen layout in the process), it really tends to encourage bad client coding practices and it introduces a huge mess of duplicated generated code in the process. Using unit tests to drive the construction of these screens, should remedy these problems.