Java: advanced testing tools (part 1)
// <![CDATA[ function styleCode() { if (typeof disableStyleCode != 'undefined') { return; } var a = false; $('code').each(function() { if (!$(this).hasClass('prettyprint')) { $(this).addClass('prettyprint'); a = true; } }); if (a) { prettyPrint(); } } $(function() {styleCode();}); // ]]>
First of all I must confess that I am a TDD fan. That's why when I hear about tests this picture appears in my head:
But is it comfortable to use only jUnit as primary testing framework? The answer is no but let me count what is missing:
Asserting collections is pain
Asserting complex objects is pain
Parameterized tests are not as cool as they could be.
Even though tests do not appear to be a part of production code every developer should pay attention to test code quality and it pushes us towards looking for new frameworks with better and fluent API.
At this point I would recommend the following frameworks, which I use on production:
AssertJ - cool framework for better and more readable assertions.
JunitParams - better parameterized tests API
So let's go right to examples! Let's consider the following data:
package com.devyou.objects; import static com.devyou.objects.HeroClass.*; import static com.devyou.objects.Race.*; public class Creature { private final Race race; private final double age; private final double weight; private final int numberOfLegs; private final double healthPoints; private final double manaPoints; private final HeroClass heroClass; private final String name; private Creature(Race race, double age, double weight, int numberOfLegs, double healthPoints, double manaPoints, HeroClass heroClass, String name) { this.race = race; this.age = age; this.weight = weight; this.numberOfLegs = numberOfLegs; this.healthPoints = healthPoints; this.manaPoints = manaPoints; this.heroClass = heroClass; this.name = name; } public static Creature orc(double age, double weight, HeroClass heroClass, String name) { if (heroClass == WARRIOR || heroClass == PALADIN) { return new Creature(ORC, age, weight, 4, 100, 100, heroClass, name); } throw new IllegalArgumentException("Orc can be only warrior or rogue!"); } public static Creature elf(double weight, HeroClass heroClass, String name) { return new Creature(ELF, Double.POSITIVE_INFINITY, weight, 4, 100, 100, heroClass, name); } public static Creature human(double age, double weight, HeroClass heroClass, String name) { if (heroClass == WARRIOR || heroClass == ROGUE || heroClass == PALADIN) { return new Creature(HUMAN, age, weight, 4, 100, 100, heroClass, name); } throw new IllegalArgumentException("Human can be only warrior, rogue or paladin!"); } public static Creature undead(double weight, HeroClass heroClass, String name) { if (heroClass == ROGUE || heroClass == MAGE) { return new Creature(UNDEAD, Double.POSITIVE_INFINITY, weight, 4, 100, 100, heroClass, name); } throw new IllegalArgumentException("Undead can be only mage or rogue!"); } public Creature castASpell(int cost) { double manaLeft = manaPoints - cost; if (manaLeft < 0) { throw new IllegalArgumentException("Low mana"); } return new Creature(race, age, weight, numberOfLegs, healthPoints, this.manaPoints - cost, heroClass, name); } public Creature isDamagedFor(double amount) { double healthAfterDamage = healthPoints - amount; double healthLeft = healthAfterDamage < 0 ? 0 : healthAfterDamage; return new Creature(race, age, weight, numberOfLegs, healthLeft, this.manaPoints, heroClass, name); } public Creature isHealedFor(double amount) { double healthAfterHeal = healthPoints + amount; double healthLeft = healthPoints > 100 ? 100 : healthAfterHeal; return new Creature(race, age, weight, numberOfLegs, healthLeft, this.manaPoints, heroClass, name); } public boolean isAlive() { return Double.compare(healthPoints, 0d) == 0; } public boolean isDead() { return !isAlive(); } public Race getRace() { return race; } public double getAge() { return age; } public double getWeight() { return weight; } public int getNumberOfLegs() { return numberOfLegs; } public double getHealthPoints() { return healthPoints; } public double getManaPoints() { return manaPoints; } public HeroClass getHeroClass() { return heroClass; } public String getName() { return name; } }
As you can see everything is simple: we have an immutable creature which can be an orc, human etc. How can we test this code? We will use Java 7, AssertJ and jUnitParams.
Let's start from testing how our armies are created:
package com.devyou.objects; import org.assertj.core.api.Condition; import org.junit.Test; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import static com.devyou.objects.HeroClass.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.extractProperty; public class CreaturesArmyTest { private static final Random random = ThreadLocalRandom.current(); private final Condition eternalLivers = new Condition() { @Override public boolean matches(Creature creature) { return creature.livesForever(); } }; private final Condition fullHp = new Condition() { @Override public boolean matches(Creature creature) { return Double.compare(creature.getHealthPoints(), 100) == 0; } }; private final Condition fullMana = new Condition() { @Override public boolean matches(Creature creature) { return Double.compare(creature.getManaPoints(), 100) == 0; } }; @Test public void undeadTest() { List undeadArmy = undeadArmy(); assertThat(undeadArmy) .doesNotContainNull() .extracting("heroClass") .contains(ROGUE, MAGE) .doesNotContain(WARRIOR, PALADIN); // All can cast and full of hp assertThat(undeadArmy).are(fullHp).are(fullMana); // And they have no age assertThat(undeadArmy).are(eternalLivers); } @Test public void humanTest() { List humanArmy = humanArmy(); assertThat(humanArmy) .doesNotContainNull() .extracting("heroClass") .contains(ROGUE, WARRIOR, PALADIN) .doesNotContain(MAGE); // Another approach with extractions assertThat(extractProperty("healthPoints", Double.class).from(humanArmy)) .containsOnly(100d); assertThat(extractProperty("manaPoints", Double.class).from(humanArmy)) .containsOnly(100d); } @Test public void orcTest() { List orcArmy = orcArmy(); assertThat(orcArmy) .doesNotContainNull() .extracting("heroClass") .contains(ROGUE, WARRIOR, PALADIN) .doesNotContain(MAGE); assertThat(extractProperty("healthPoints", Double.class).from(orcArmy)) .containsOnly(100d); assertThat(extractProperty("manaPoints", Double.class).from(orcArmy)) .containsOnly(100d); } @Test public void elfTest() { List elfArmy = elfArmy(); assertThat(elfArmy) .doesNotContainNull() .extracting("heroClass") .contains(ROGUE, WARRIOR, PALADIN, MAGE); assertThat(elfArmy).are(fullHp).are(fullMana); assertThat(elfArmy).are(eternalLivers); } public static Creature randomHuman() { return Creature.human(random.nextDouble() * 100, random.nextDouble() * 100, random.nextBoolean() ? ROGUE : (random.nextBoolean() ? WARRIOR : PALADIN), "test" + UUID.randomUUID().toString()); } public static List humanArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomHuman()); } return undeadArmy; } public static Creature randomUndead() { return Creature.undead(random.nextDouble() * 100, random.nextBoolean() ? ROGUE : MAGE, "test" + UUID.randomUUID().toString()); } public static List undeadArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomUndead()); } return undeadArmy; } public static Creature randomOrc() { return Creature.orc(random.nextDouble() * 100, random.nextDouble() * 100, random.nextBoolean() ? WARRIOR : PALADIN, "test" + UUID.randomUUID().toString()); } public static List orcArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomOrc()); } return undeadArmy; } public static Creature randomElf() { return Creature.elf(random.nextDouble() * 100, HeroClass.values()[random.nextInt(HeroClass.values().length)], "test" + UUID.randomUUID().toString()); } public static List elfArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomElf()); } return undeadArmy; } }
Pay attention to how tests become more readable. I think every person can understand what exactly is tested in this simple sentence:
assertThat(elfArmy).are(eternalLivers);
Also, you should notice that I used two equivallent approcahes for extracting, you are free to choose the best for you.
As we can see AssertJ solves two problems: extracting and matching collections in a very elegant way. But what should we do with the last problem named parameterization of tests? As we can see now our tests are just a boilerplate of code. I think we can try to unite them into one test as following:
package com.devyou.objects; import junitparams.JUnitParamsRunner; import junitparams.Parameters; import org.assertj.core.api.Condition; import org.junit.Test; import org.junit.runner.RunWith; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import static com.devyou.objects.HeroClass.*; import static junitparams.JUnitParamsRunner.$; import static org.assertj.core.api.Assertions.assertThat; @RunWith(JUnitParamsRunner.class) public class CreaturesArmyTest { private static final Random random = ThreadLocalRandom.current(); private final Condition eternalLivers = new Condition() { @Override public boolean matches(Creature creature) { return creature.livesForever(); } }; private final Condition fullHp = new Condition() { @Override public boolean matches(Creature creature) { return Double.compare(creature.getHealthPoints(), 100) == 0; } }; private final Condition fullMana = new Condition() { @Override public boolean matches(Creature creature) { return Double.compare(creature.getManaPoints(), 100) == 0; } }; @Test @Parameters public void eternalLiversTest(List soldiers) { assertThat(soldiers).are(eternalLivers); } @SuppressWarnings("unused") private Object[] parametersForEternalLiversTest() { return $( $(undeadArmy()), $(elfArmy()) ); } @Test @Parameters public void readyToFightTest(List soldiers) { assertThat(soldiers).are(fullHp).are(fullMana); } @SuppressWarnings("unused") private Object[] parametersForReadyToFightTest() { return $( $(undeadArmy()), $(elfArmy()), $(humanArmy()), $(orcArmy()) ); } @Test @Parameters public void correctHeroClassesTest(List soldiers, HeroClass[] allowedClasses) { assertThat(soldiers).extracting("heroClass").containsOnly(allowedClasses); } @SuppressWarnings("unused") private Object[] parametersForCorrectHeroClassesTest() { return $( $(undeadArmy(), new HeroClass[]{ROGUE, MAGE}), $(elfArmy(), HeroClass.values()), $(humanArmy(), new HeroClass[]{ROGUE, WARRIOR, PALADIN}), $(orcArmy(), new HeroClass[]{WARRIOR, PALADIN}) ); } public static Creature randomHuman() { return Creature.human(random.nextDouble() * 100, random.nextDouble() * 100, random.nextBoolean() ? ROGUE : (random.nextBoolean() ? WARRIOR : PALADIN), "test" + UUID.randomUUID().toString()); } public static List humanArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomHuman()); } return undeadArmy; } public static Creature randomUndead() { return Creature.undead(random.nextDouble() * 100, random.nextBoolean() ? ROGUE : MAGE, "test" + UUID.randomUUID().toString()); } public static List undeadArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomUndead()); } return undeadArmy; } public static Creature randomOrc() { return Creature.orc(random.nextDouble() * 100, random.nextDouble() * 100, random.nextBoolean() ? WARRIOR : PALADIN, "test" + UUID.randomUUID().toString()); } public static List orcArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomOrc()); } return undeadArmy; } public static Creature randomElf() { return Creature.elf(random.nextDouble() * 100, HeroClass.values()[random.nextInt(HeroClass.values().length)], "test" + UUID.randomUUID().toString()); } public static List elfArmy() { List undeadArmy = new LinkedList(); for (int i = 0; i < 1000; i++) { undeadArmy.add(randomElf()); } return undeadArmy; } }
Just look how our tests became dedicated only to a certain property. Moreover junitparams allowed us to be independent of the data we are going to test.
That's all for part one. All code can be found here. May the green light be with you.
P.S: In the next part I am going to write some notes about mocking and tools to create and verify mocks.