Print
ConversationalVerification

PicoUnit, like jMock, already has a 'conversational api' for expressing colloborations:

Mocker should = ...
Database mockDatabase = ...

should.call(mockDatabase.insert("..."))
  .andReturn(true)
  .because("yada yada");

It does not, however, have a conversation api for verification:

Verify verify = ...
  
verify.equal(123, 123);
verify.equal("a number should equal itself!", 123, 123);

I'm working on a much more expressive way of writing verification which will look like this:

Verify verify = ...

verify.that(123).isEqualTo(123);

verify.because("a number should equal itself!")
  .that(123).isEqualTo(123);

The nice thing about this style of API is that the API can be quite wide, it can have many options (string constraints, integer constraints, etc.), but you only see the options that are relevant. So when typing 'verify.that(123).' the IDE will not show you methods to do with strings. Another benefit is new types of verification just by adding an additional 'that' method.

Suppose you wanted to add verification of files, you might want to check that a file was beneath a certain directory, a first attempt might look like this:

verify.that(isBelow(file, directory));

private boolean isBelow(File file, File directory) {
  ...
}

This code will work fine but it commits a PicoUnit no-no, it requires inheritance of Tests, abstract base classes containing the 'isBelow' method, etc. etc.

Fortunately it's easy to provide a conversational version that will be invoked like this:

verify.that(file).isBelow(directory);

Here's the new interface, it extends the current one:

interface CustomVerify extends Verify {
  FileConstraints that(File file);

  interface FileConstraints {
    void isBelow(File directory);
  }
}

Here's the implementation of CustomVerify, it naturally extends the current implementation.

The base implementation contains helper methods (which I suggest you use but which can be

ignored) to evaluate Constraints and to store data.

class CustomVerifyImpl extends VerifyImpl implements CustomVerify {
  private FileConstraints fileConstraints = new FileConstraintsImpl(getEvaluator());

  public FileConstraints that(File file) {
    setValue(file);

    return fileConstraints;
  }
}

Here's the class that implements the constraints that can be made on a file, currently just 'isBelow'.

This implementation just constructs a 'FileIsBelowConstraint' with the directory and asks an Evaluator

to evaluate the constraint.

class FileConstraintImpl implements FileConstraints {
  private Evaluator evaluator;

  public FileConstraintsImpl(Evaluator evaluator) {
    this.evaluator = evaluator;
  }

  pubilc isBelow(File directory) {
    evaluator.evaluate(new FileIsBelowConstraint(directory));
  }
}

Both the evaluator and the original file are stored in the base class (VerifyImpl),

so when the evaluator is asked to evaluate the constraint it can bring all the data together and

invoke 'evaluate' on the FileIsBelowConstraint, which is implemented like this:

class FileIsBelowConstraint implements Constraint {
  private File directory;

  public FileIsBelowConstraint(File directory) {
    this.directory = directory;
  }

  public boolean evaluate(Object value) {
    File file = (File) value;

    return isBelow(file, directory);
  }

  public String describeFailure(Object value) {
    return "<" + value + "> is not below <" + directory + ">";
  }

  public Class getType() {
    return File.class;
  }

  private boolean isBelow(File file, File directory) {
    ...
  }
}

This is quite a lot of additional code, but most of this is boilerplate, when you want to add additional constraints on File, such as 'isEmpty', etc. you just need an additional Constraint: FileIsEmptyConstraint, and an additional method on FileConstraints: isEmpty();

I recommend that the FileConstraint have an additional placeholder method:

interface FileConstraints {
  ...

  void passes(Constraint constraint);
}

This will allow users to use new constraints a little more easily before they have added them to the conversational api, they could write code like this:

verify.that(file).passes(isVeryBig());

This style of API, I think, reads much better than one involving many methods with lots of parameters in lots of combinations. The implementation of the API will allow you to add new types of verification for File, URL, custom types etc. It does not allow you to add new verifications for existing types, for example PicoUnit will come with verification for Strings. In Java 1.4 you will not be able to add additional verifications for string, like "isPalindrome", to do this requires Generics.

Powered by Atlassian Confluence