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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assertThat(hayao.getFullName(), equalTo("Hayao Miyazaki")); | |
assertThat(hayao.getAge(), equalTo(70)); |
Fest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assertThat(myNeighborTotoro.getDirectedBy(), not(equalTo(goro))); |
Fest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assertThat(mei, instanceOf(Character.class)); | |
assertThat(mei, notNullValue()); | |
assertThat(goro, sameInstance(goro)); |
Fest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assertThat(mainCharacters, hasKey(myNeighborTotoro)); | |
assertThat(mainCharacters, hasValue(totoro)); | |
assertThat(movies, hasItem(hasProperty("name", equalTo("Tales From Eathsea")))); |
Fest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
As in simple asserts, there is no much difference between them.
Hamcrest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assertThat(hayao.getAge(), greaterThan(goro.getAge())); |
Fest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assertThat(hayao.getAge()).isGreaterThan(goro.getAge()); |
String asserts:
Hamcrest
Hamcrest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
assertThat(sheeta.getName(), equalToIgnoringCase("lusheeta toel ur laputa")); |
Fest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
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
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.
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.
Publicar un comentario