viernes, julio 22, 2011

Tu I Jo Asseguts A La Barra D’un Bar, Sona Bona Música I Som Davant Del Mar (Al Mar - Manel)



In current post I am going to talk about differences between Hamcrest and Fest Fluent Assertions. The reason of this post is not to tell you if you should use one or the other. I only want to show you how both implementations resolve the problem of writing readable assertions.

To compare both APIs I have created two JUnits, one using Hamcrest assertions and one where Fest assertions are used.

The comparision is done using only common operations (Logical, Object, Collections, Number, Text) and creation of new matchers. I am not going to compare which ones have implemented more matchers or kinds of matchers, because I think this is not a parameter to use for choosing what library fits better to your project. Moreover in both APIs you can extend them for creating your own matchers.

Let's start showing model representing a simple modelling of Studio Ghibli http://en.wikipedia.org/wiki/Studio_Ghibli movies information. 

private static final Director hayao = new Director("Hayao Miyazaki", 70);
private static final Director goro = new Director("Goro Miyazaki", 44);
private static final Character totoro = new Character("Totoro", false);
private static final Character satsuki = new Character("Satsuki Kusakabe", true);
private static final Character mei = new Character("Mei Kusakabe", true);
private static final Character tatsuo = new Character("Tatsuo Kusakabe", true);
private static final Movie myNeighborTotoro = new Movie("My Neighbor Totoro", hayao);
private static final Character sheeta = new Character("Lusheeta Toel Ur Laputa", true);
private static final Character pazu = new Character("Pazu", true);
private static final Movie castleInTheSky = new Movie("Castle In The Sky", hayao);
private static final Character arren = new Character("Arren", true);
private static final Character therru = new Character("Therru", false);
private static final Movie talesFromEarthSea = new Movie("Tales From Eathsea", goro);
private static final List<Object> movies = new ArrayList<Object>();
private static final Map<Movie, Character> mainCharacters = new HashMap<Movie, Character>();
@Before
public void setup() {
myNeighborTotoro.addCharacter(tatsuo);
myNeighborTotoro.addCharacter(satsuki);
myNeighborTotoro.addCharacter(mei);
myNeighborTotoro.addCharacter(totoro);
movies.add(myNeighborTotoro);
mainCharacters.put(myNeighborTotoro, totoro);
castleInTheSky.addCharacter(pazu);
castleInTheSky.addCharacter(sheeta);
movies.add(castleInTheSky);
mainCharacters.put(castleInTheSky, pazu);
talesFromEarthSea.addCharacter(arren);
talesFromEarthSea.addCharacter(therru);
movies.add(talesFromEarthSea);
mainCharacters.put(talesFromEarthSea, arren);
}

Imports:

Hamcrest requires 11 imports:

import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsSame.sameInstance;
import static org.hamcrest.collection.IsMapContaining.hasKey;
import static org.hamcrest.collection.IsMapContaining.hasValue;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.number.OrderingComparison.greaterThan;
import static org.hamcrest.beans.HasPropertyWithValue.hasProperty;
while Fest requires 2 imports:

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.MapAssert.entry;
in fact this is not a good or bad thing how many imports are required, but it is always a headache remember where a required assertion is packaged. Because Fest uses Fluent Interfaces approach with two imports all operations are available with IDE's "auto-completation" feature.

Simple asserts:

Not much difference between Hamcrest and Fest, in case of Fest verb is also added in method name creating a much readable assert.

Hamcrest

assertThat(hayao.getFullName(), equalTo("Hayao Miyazaki"));
assertThat(hayao.getAge(), equalTo(70));

Fest

assertThat(hayao.getFullName()).isEqualTo("Hayao Miyazaki");
assertThat(hayao.getAge()).isEqualTo(70);

Logical asserts:

In this case we find a big difference. Hamcrest supports logical operations and can be used independently, meanwhile logical operations in Fest are a part of the method:

Hamcrest

assertThat(myNeighborTotoro.getDirectedBy(), not(equalTo(goro)));

Fest

assertThat(myNeighborTotoro.getDirectedBy()).isNotEqualTo(goro);

Object asserts:

Same object operations are provided in both APIs. There is only one difference, in Fest you can chain callings of methods, so number of asserts compared to Hamcrest are fewer.

Hamcrest

assertThat(mei, instanceOf(Character.class));
assertThat(mei, notNullValue());
assertThat(goro, sameInstance(goro));

Fest

assertThat(mei).isInstanceOf(Character.class).isNotNull();
assertThat(goro).isSameAs(goro);

Collection asserts:

In case of Collections is where they differ more from each other. Hamcrest deals with Collections and Maps with methods like hasKey, hasValue, hasItem. Fest uses a more generic calling like includes, excludes, entry; I have not found any way for asserting key instead of key-value in Fest.
Moreover in this example I also compare how to access a bean property in each elements of collection. From my point of view I think that Fest has a better approach resolving this case.

Hamcrest

assertThat(mainCharacters, hasKey(myNeighborTotoro));
assertThat(mainCharacters, hasValue(totoro));
assertThat(movies, hasItem(hasProperty("name", equalTo("Tales From Eathsea"))));
Fest

assertThat(mainCharacters).includes(entry(myNeighborTotoro, totoro));
assertThat(movies).onProperty("name").contains("Tales From Eathsea");

Number asserts:

As in simple asserts, there is no much difference between them.

Hamcrest

assertThat(hayao.getAge(), greaterThan(goro.getAge()));

Fest

assertThat(hayao.getAge()).isGreaterThan(goro.getAge());

String asserts:


Hamcrest
assertThat(sheeta.getName(), equalToIgnoringCase("lusheeta toel ur laputa"));

Fest
assertThat(sheeta.getName()).isEqualToIgnoringCase("lusheeta toel ur laputa");


As you have noted Hamcrest and Fest are very similar, but I think that Fest solves the same problem but in much cleaner way.

Although both solutions offer very similar matchers, each implementation is so different, so creation of a custom matcher is different too. In Hamcrest you should create a class that extends from TypeSafeMatcher. In Fest there are two possibilities, first one is extending Fest-assertions with custom condition (using already Fest structure and defined types), and the second one is extending with custom assertion (creating a new assertThat implementation for required type).

Hamcrest extension for asserting that a character is a human:

public class IsCharacterHuman extends TypeSafeMatcher<Character> {
public void describeTo(Description description) {
description.appendText("a human");
}
@Override
protected boolean matchesSafely(Character character) {
return character.isHuman();
}
@Factory
public static <T> Matcher<Character> isHuman() {
return new IsCharacterHuman();
}
}
import static org.assertions.IsCharacterHuman.isHuman;
assertThat(arren, isHuman());

In case of Fest there are two approaches, the first one, that requires that our class extends from Condition. As I told previously, extending Condition implies using the same kind of types supported by org.fest.assertions.Assertions.assertThat method (object, int, short, list, array, ...). In previous case our type is Character, and of course does not exist an overload of Assertions.assertThat method with Character, but java.lang.Object. So our class should be:

public class CharacterHumanCondition extends Condition<Object> {
@Override
public boolean matches(Object character) {
return ((Character)character).isHuman();
}
public static CharacterHumanCondition human() {
return new CharacterHumanCondition();
}
}

As you probably noted, you need to cast character variable to Character, neither a clean solution nor optimal. But of course you can use second method, extending from GenericAssert. This class (a Fluent class) will be the responsible of implementing an assertThat that accepts Character objects, and moreover will implement matcher methods. And I say methods because unlike Hamcrest, Fest can contain more than one matcher in each class. For example if there are some tests that we are asserting if character is human, and in another ones asserting if is human and if has a name, in Hamcrest we should create two classes. In Fest you don't have this restriction.

public class CharacterAssert extends GenericAssert<CharacterAssert, Character> {
public CharacterAssert(Character character) {
this(CharacterAssert.class, character);
}
protected CharacterAssert(Class<CharacterAssert> selfType, Character actual) {
super(selfType, actual);
}
public static CharacterAssert assertThat(Character actual) {
return new CharacterAssert(actual);
}
public CharacterAssert isHuman() {
isNotNull();
String errorMessage = String.format("Expected Human Character but <%s> found.", actual.isHuman());
Assertions.assertThat(actual.isHuman()).overridingErrorMessage(errorMessage).isTrue();
return this;
}
public CharacterAssert hasName(String name) {
isNotNull();
String errorMessage = String.format("Expected Character Name <%s> but <%s> found.", actual.getName(), name);
Assertions.assertThat(actual.getName()).overridingErrorMessage(errorMessage).isEqualTo(name);
return this;
}
}

As it is told in Fest site "Which one to use? Hamcrest or FEST-Assert? It is up to you...it depends on the needs of your project and your coding style!"

Download code.

Music: http://www.youtube.com/watch?v=GjWLXZ4WdQU

3 comentarios:

Anónimo dijo...

First, thank you for the comparison. This is the first I'd seen of Fest, and it definitely looks good.

Just some suggestions for your usage of Hamcrest:

1. For imports, you should be using the Matchers factory class rather than importing each individual item. Can statically import Matchers.*.

2. For chaining multiple matchers, can use the allOf matcher to combine them. Not as clean as Fest's implementation, but at least makes it clearer that they all must be true, and are all applicable to one actual value.

Mike

Alex dijo...

Hi Mike thank you very much for your comments, I agree with you that importing Matchers class, you avoid having to import each class.

About allOf operation it is true that you can acquire the same behavior with allOf but as you said it is not a clean solution.

Thank again for your comments.
Alex.

Paul Duffin dijo...

What about anyOf() and not()? You cannot do either of those in FEST unless you use is and roll your own Condition instances which are basically Hamcrest matchers.