1. Overview
Test runner frameworks like JUnit and TestNG provide some basic assertion methods (assertTrue, assertNotNull, etc.).
Then there are assertion frameworks like Hamcrest, AssertJ, and Truth, which provide fluent and rich assertion methods with names that usually begin with “assertThat”.
JSpec is another framework that allows us to write fluent assertions closer to the way we write specifications in our natural language, albeit in a slightly different manner from other frameworks.
In this article, we’ll learn how to use JSpec. We’ll demonstrate the methods required to write our specifications and the messages that will print in case of test failure.
2. Maven Dependencies
Let’s import the javalite-common dependency, which contains JSpec:
<dependency> <groupId>org.javalite</groupId> <artifactId>javalite-common</artifactId> <version>1.4.13</version> </dependency>
For the latest version, please check the Maven Central repository.
3. Comparing Assertion Styles
Instead of the typical way of asserting based on rules, we just write the specification of behavior. Let’s look at a quick example for asserting equality in JUnit, AssertJ, and JSpec.
In JUnit, we’d write:
assertEquals(1 + 1, 2);
And in AssertJ, we’d write:
assertThat(1 + 1).isEqualTo(2);
Here’s how we’d write the same test in JSpec:
$(1 + 1).shouldEqual(2);
JSpec uses the same style as fluent assertion frameworks but omits the leading assert/assertThat keyword and uses should instead.
Writing assertions in this way makes it easier to represent the real specifications, promoting TDD and BDD concepts.
Look how this example is very close to our natural writing of specifications:
String message = "Welcome to JSpec demo"; the(message).shouldNotBe("empty"); the(message).shouldContain("JSpec");
4. Structure of Specifications
The specification statement consists of two parts: an expectation creator and an expectation method.
4.1. Expectation Creator
The expectation creator generates an Expectation object using one of these statically imported methods: a(), the(), it(), $():
$(1 + 2).shouldEqual(3); a(1 + 2).shouldEqual(3); the(1 + 2).shouldEqual(3); it(1 + 2).shouldEqual(3);
All these methods are essentially the same — they all exist only for providing various ways to express our specification.
The only difference is that the it() method is type-safe, allowing comparison only of objects that are of the same type:
it(1 + 2).shouldEqual("3");
Comparing objects of different types using it() would result in a compilation error.
4.2. Expectation Method
The second part of the specification statement is the expectation method, which tells about the required specification like shouldEqual, shouldContain.
When the test fails, an exception of the type javalite.test.jspec.TestException displays an expressive message. We’ll see examples of these failure messages in the following sections.
5. Built-in Expectations
JSpec provides several kinds of expectation methods. Let’s take a look at those, including a scenario for each that shows the failure message that JSpec generates upon test failure.
5.1. Equality Expectation
shouldEqual(), shouldBeEqual(), shouldNotBeEqual()
These specify that two objects should/shouldn’t be equal, using the java.lang.Object.equals() method to check for equality:
$(1 + 2).shouldEqual(3);
Failure scenario:
$(1 + 2).shouldEqual(4);
would produce the following message:
Test object:java.lang.Integer == <3> and expected java.lang.Integer == <4> are not equal, but they should be.
5.2. Boolean Property Expectation
shouldHave(), shouldNotHave()
We use these methods to specify whether a named boolean property of the object should/shouldn’t return true:
Cage cage = new Cage(); cage.put(tomCat, boltDog); the(cage).shouldHave("animals");
This requires the Cage class to contain a method with the signature:
boolean hasAnimals() {...}
Failure scenario:
the(cage).shouldNotHave("animals");
would produce the following message:
Method: hasAnimals should return false, but returned true
shouldBe(), shouldNotBe()
We use these to specify that the tested object should/shouldn’t be something:
the(cage).shouldNotBe("empty");
This requires the Cage class to contain a method with the signature “boolean isEmpty()”.
Failure scenario:
the(cage).shouldBe("empty");
would produce the following message:
Method: isEmpty should return true, but returned false
5.3. Type Expectation
shouldBeType(), shouldBeA()
We can use these methods to specify that an object should be of a specific type:
cage.put(boltDog); Animal releasedAnimal = cage.release(boltDog); the(releasedAnimal).shouldBeA(Dog.class);
Failure scenario:
the(releasedAnimal).shouldBeA(Cat.class);
would produce the following message:
class com.baeldung.jspec.Dog is not class com.baeldung.jspec.Cat
5.4. Nullability Expectation
shouldBeNull(), shouldNotBeNull()
We use these to specify that the tested object should/shouldn’t be null:
cage.put(boltDog); Animal releasedAnimal = cage.release(dogY); the(releasedAnimal).shouldBeNull();
Failure scenario:
the(releasedAnimal).shouldNotBeNull();
would produce the following message:
Object is null, while it is not expected
5.5. Reference Expectation
shouldBeTheSameAs(), shouldNotBeTheSameAs()
These methods are used to specify that an object’s reference should be the same as the expected one:
Dog firstDog = new Dog("Rex"); Dog secondDog = new Dog("Rex"); $(firstDog).shouldEqual(secondDog); $(firstDog).shouldNotBeTheSameAs(secondDog);
Failure scenario:
$(firstDog).shouldBeTheSameAs(secondDog);
would produce the following message:
references are not the same, but they should be
5.6. Collection and String Contents Expectation
shouldContain(), shouldNotContain()
We use these to specify that the tested Collection or Map should/shouldn’t contain a given element:
cage.put(tomCat, felixCat); the(cage.getAnimals()).shouldContain(tomCat); the(cage.getAnimals()).shouldNotContain(boltDog);
Failure scenario:
the(animals).shouldContain(boltDog);
would produce the following message:
tested value does not contain expected value: Dog [name=Bolt]
We can also use these methods to specify that a String should/shouldn’t contain a given substring:
$("Welcome to JSpec demo").shouldContain("JSpec");
And although it may seem strange, we can extend this behavior to other object types, which are compared using their toString() methods:
cage.put(tomCat, felixCat); the(cage).shouldContain(tomCat); the(cage).shouldNotContain(boltDog);
To clarify, the toString() method of the Cat object tomCat would produce:
Cat [name=Tom]
which is a substring of the toString() output of the cage object:
Cage [animals=[Cat [name=Tom], Cat[name=Felix]]]
6. Custom Expectations
In addition to the built-in expectations, JSpec allows us to write custom expectations.
6.1. Difference Expectation
We can write a DifferenceExpectation to specify that the return value of executing some code should not be equal to a particular value.
In this simple example we’re making sure that the operation (2 + 3) will not give us the result (4):
expect(new DifferenceExpectation<Integer>(4) { @Override public Integer exec() { return 2 + 3; } });
We can also use it to ensure that executing some code would change the state or value of some variable or method.
For example, when releasing an animal from a Cage that contains two animals, the size should be different:
cage.put(tomCat, boltDog); expect(new DifferenceExpectation<Integer>(cage.size()) { @Override public Integer exec() { cage.release(tomCat); return cage.size(); } });
Failure scenario:
Here we’re trying to release an animal that doesn’t exist inside the Cage:
cage.release(felixCat);
The size won’t be changed, and we get the following message:
Objects: '2' and '2' are equal, but they should not be
6.2. Exception Expectation
We can write an ExceptionExpectation to specify that the tested code should throw an Exception.
We’ll just pass the expected exception type to the constructor and provide it as a generic type:
expect(new ExceptionExpectation<ArithmeticException>(ArithmeticException.class) { @Override public void exec() throws ArithmeticException { System.out.println(1 / 0); } });
Failure scenario #1:
System.out.println(1 / 1);
As this line wouldn’t result in any exception, executing it would produce the following message:
Expected exception: class java.lang.ArithmeticException, but instead got nothing
Failure scenario #2:
Integer.parseInt("x");
This would result in an exception different from the expected exception:
class java.lang.ArithmeticException, but instead got: java.lang.NumberFormatException: For input string: "x"
7. Conclusion
Other fluent assertion frameworks provide better methods for collections assertion, exception assertion, and Java 8 integration, but JSpec provides a unique way for writing assertions in the form of specifications.
It has a simple API that let us write our assertions like natural language, and it provides descriptive test failure messages.
The complete source code for all these examples can be found over on GitHub – in the package com.baeldung.jspec.