January 29, 2004

I've had to close my comments

A sign of the times, some a***hole of a comment spammer has discovered my site, so now I've had to close the comments section.

Email me if you want to respond and I'll post it.

Sigh

Posted by stevef at 4:54 PM

January 23, 2004

Missing the point of Mocks?

Triggered by Crazy Bob, Aslak and Simon Brown responded on whether it's better to create your own mocks with the IDE or use one of the dynamic mock packages, such as JMock. I think Bob and Simon have missed something...

One of the hard-earned lessons we learned from working on the original Mock Objects package was the importance of having good failure messages. It's worth spending the time and being verbose so that when an error does occur, you look at it and see the problem immediately. It's so annoying that most of the examples and textbooks don't bother to fill in the optional message parameter.

Prepackaged Expectations

The mockobjects package includes a set of expectation classes that check values, lists, sets, and so forth. They also implement Verifiable so instances will be picked up by the Verifier.verifyObject() method. Most people have forgotten, but our original idea was to expose the expectations in a mock class so we could address them directly, for example:

public class MockCheeseAction implements CheeseAction {
  public ExpectationValue cheeseName = new ExpectationValue("cheeseName");

  public void perform(String actualCheeseName) {
    cheeseName.setActual(actualCheeseName);
  }
}

public class CheeseCallerTest extends TestCase {
  public void testCheeseCalling() {
    MockCheeseAction mockAction = new MockCheeseAction();
    
    mockAction.cheeseName.setExpected("saint marcellin");
    
    new CheeseCaller("saint marcellin").call(mockAction);
    
    Verifier.verifyObject(mockAction);
  }
}

This will handle all the cases, including the method not being called, with a self-descriptive error message.

If you don't want to, or can't, use one of the dynamic mock libraries, then I agree, you can generate the stub and ignore the bits you don't need (I usually tweak the IDE to have the generated methods fail so that I don't get tests passing by accident). With anonymous classes, it's an obvious design.

public void testCheeseCalling() {
  final ExpectationValue cheeseName = new ExpectationValue("cheeseName");

  cheeseName.setExpected("saint marcellin");
    
  new CheeseCaller("saint marcellin").call(
    new CheeseAction() {
      public void perform(String actualCheeseName) {
        cheeseName.setActual(actualCheeseName);
      }
    });
    
  cheeseName.verify();
}

Crazy Bob codes this up by hand, but there are other options. The Expectation classes are also included in the nascent JMock library.

Lessons for framework developers

In working on the Mock Objects library we both succeeded and failed. I now think we shouldn't have gone so far in attempting to mock up the entire Java libraries. It's too much work and led us away from thinking about Mock Objects as a design rather than a testing technique. We also just didn't have the bandwidth to support it properly.

On the other hand, we have been quite good at layering our frameworks so you can get to the underlying behaviour that you need, especially in the dynamic libraries. We just haven't made it very obvious how. Best of all, at least people are still talking about us...

Posted by stevef at 1:55 PM | Comments (1)

January 18, 2004

Windows vs. Unix. Is that it?

Joel Spolsky's article on Biculturism suggests two things to me:

  • it more or less implies Windows on the desktop and Unix on the server, which is probably not how Microsoft sees the world but seems to be what many organisations do.
  • I may be a member of the last generation of programmers who got to work on operating systems other than variants of Unix or Windows, there's some supporting evidence from Rob Pike. Should we get on the boat with the elves and sail away?
Posted by stevef at 11:14 PM | Comments (1)

January 16, 2004

Sometimes you have to bend the rules

In a comment on his posting Testing the Mock, Aslak Hellesoy points out that one of the solutions is less than perfect because it breaks encapsulation to allow testing. That's true, but sometimes the right thing to do is bend the rules so you can test.

In Aslak's example, the twist was to introduce a factory method for the creation of new instances of an internal object.

public void testExecuteAddsCheeseDescriptionToDao() {
  final Mock mockDao = new Mock(CheeseDao.class);
  mockDao.expect("saveCheese", C.eq("stilton"));

  CheeseAction action = new CheeseAction() {
    // ---- override ----
    protected CheeseDao newDao() {
      return ((CheeseDao)mockDao.proxy());
    }
  };
  action.setCheese("stilton");

  assertEquals(ActionSupport.SUCCESS, action.execute());
  mockDao.verify();
}

It's true that we're exposing a method just for testing, but it's a step in the right direction because it starts to isolate the creation of new instances within our code. Later on, once we see how the construction patterns pan out, this might turn into a full-blown factory object. In the meantime, this gets around the chore of creating a new factory class each time (1).

Once again in C#.

The situation is more severe in the C# world because so much of the framework code is locked down. For example, you want to write unit tests for code that accesses web session state? Tough. Everything's sealed. The solution my last team came up with was to wrap the built-in session object in a thin veneer(2).

public class CheeseSession : ICheeseSession {
  private Session session;
  public CheeseSession(Session session) { this.session = session; }
  
  public string Variety {
    get { return (string) Session.Properties["variety"]; }
  }
}

We couldn't find a way to unit test CheeseSession, so we just kept it so simple that it was obviously right.

Then we had a base Form class that returns a CheeseSession when we needed one: new instances for now, and maybe cached with the session later.

public class CheeseForm : WebForm {
  protected virtual ICheeseSession CheeseSession {
    get { return new CheeseSession(Session); }
  }
}

When unit-testing forms, we can do the "subclass and override" trick:

public class StubCheeseActivityForm : CheeseActivityForm {
  public ICheeseSession mockCheeseSession;
  protected override ICheeseSession CheeseSession {
    get { return mockCheeseSession; }
  }
}

[TestFixture] public classs CheeseActivityFormTest {
  [Test] public void SmellyCheesesAreDoubleWrapped() {
    Mock mockWrapper = new Mock(typeof(CheeseWrapper));
    Mock mockSession = new Mock(typeof(CheeseSession));
    StubCheeseActivityForm form = new StubCheeseActivityForm((CheeseWrapper)mockWrapper.MockInstance);
    form.mockCheeseSession = (ICheeseSession)mockSession.MockInstance;
    
    mockSession.setupResult("Variety", "gorgonzola");
    
    mockWrapper.expect("LayerCount", 2);
    
    form.OnCommit(); // or whatever the call is
    
    mockWrapper.Verify();
  }
}

Bad news, good news.

Is this ugly? It sure is, but there aren't many options if you want to unit test .Net web form code without starting up a server. I also really missed Java's anonymous classes for minimizing code. That's the bad news. On the other hand, it forced us to clarify the interface between our domain (Cheese) and our platform (Web Forms), and that interface is realized exactly once, in the CheeseSession property. This turned out to have the nice property that we didn't have to commit early to a mechanism for preserving session state because we were confident that we could easily change our minds.

With a little attention and a sprinkling of interfaces, we found that our code was moving towards a structure where all the important code was defined in terms of the domain rather than the platform. We almost rediscovered the old lisp motto about writing a domain language and then writing your application in that language.



1) Which might tell us something about the statically typed language we're using, but that's another story.
2) This code typed from memory, please allow for errors.

Posted by stevef at 11:45 AM