JavaSpec 2.x is a Java library that lets you use lambdas to write specifications that run on the JUnit 5 platform.
JavaSpec 2 is a library for the Java Virtual Machine that lets you use lambdas to write specifications (unit tests) that run on the JUnit Platform. Specifications run anywhere JUnit 5 runs: Gradle, JUnit Platform Console, or your favorite IDE. It does the same thing you can do with JUnit 5, but with a syntax that is more descriptive–and more concise–than its JUnit counterpart.
JavaSpec 2 should work just about anywhere JUnit 5 works. All you have to do is add a compile dependency for the API (providing the new spec syntax) and a runtime dependency for a JUnit Test Engine that knows how to turn specifications into JUnit tests.
TL;DR - it’s kind of like the syntax from Jest, Mocha, and Jasmine, but for Java.
Note that this documentation is for the new version of JavaSpec. It uses a different syntax than JavaSpec 1.x.
JavaSpec helps you take a JUnit test that looks like this…
class GreeterTest {
@Nested
@DisplayName("#greet")
class greet {
@Test
@DisplayName("greets the world, given no name")
void givenNoNameGreetsTheWorld() {
Greeter subject = new Greeter();
assertEquals("Hello world!", subject.greet());
}
@Test
@DisplayName("greets a person by name, given a name")
void givenANameGreetsThePersonByName() {
Greeter subject = new Greeter();
assertEquals("Hello Adventurer!", subject.greet("Adventurer"));
}
}
}
…and declare it with lambdas instead:
@Testable
public class GreeterSpecs implements SpecClass {
public void declareSpecs(JavaSpec javaspec) {
javaspec.describe(Greeter.class, () -> {
javaspec.describe("#greet", () -> {
javaspec.it("greets the world, given no name", () -> {
Greeter subject = new Greeter();
assertEquals("Hello world!", subject.greet());
});
javaspec.it("greets a person by name, given a name", () -> {
Greeter subject = new Greeter();
assertEquals("Hello Adventurer!", subject.greet("Adventurer"));
});
});
});
}
}
This results in test output that looks like this:
Greeter
#greet
✔ greets the world, given no name
✔ greets a person by name, given a name
Using this syntax, you can describe behavior with plain language without having
to add extra decorators or name tests twice (one machine-readable method name
and one human readable @DisplayName
).
If you’re into testing, like being descriptive, and don’t mind lambdas: this might be the testing library for you.
To start using JavaSpec, add the following dependencies:
testImplementation 'info.javaspec:javaspec-api'
: the syntax you need to
declare specs. This needs to be on the classpath you use for compiling test
sources and on the one you use when running tests.testRuntimeOnly 'info.javaspec:javaspec-engine'
: the TestEngine
that runs
specs. This only needs to be on the classpath you use at runtime when
running tests.testImplementation 'org.junit.jupiter:junit-jupiter-api'
In Gradle, that means adding the following to your build.gradle
file:
//build.gradle
dependencies {
//Add these dependencies for JavaSpec
testImplementation 'info.javaspec:javaspec-api:2.0.0'
testRuntimeOnly 'info.javaspec:javaspec-engine:2.0.0'
//Add an assertion library (JUnit 5's assertions shown here)
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
}
Maven users add the same dependencies this way:
<dependency>
<groupId>info.javaspec</groupId>
<artifactId>javaspec-api</artifactId>
<version>2.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.javaspec</groupId>
<artifactId>javaspec-engine</artifactId>
<version>2.0.0</version>
<!-- Not needed to compile tests, but other scopes lead to transitive
dependencies in consumers: https://stackoverflow.com/a/27729783/112682 -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
Start writing specs with JavaSpec the same way you would with JUnit: by making a
new class. It is often helpful for the name of that class to end in something
like Specs
, but JavaSpec does not require following any particular convention.
Once you have your new spec class:
SpecClass
(info.javaspec:javaspec-api
) and
#declareSpecs(JavaSpec)
.declareSpecs
, call JavaSpec#describe
with:
describe
lambda, call JavaSpec#it
with:
Put it all together, and a basic spec class looks something like this:
import info.javaspec.api.JavaSpec;
import info.javaspec.api.SpecClass;
public class GreeterSpecs implements SpecClass {
public void declareSpecs(JavaSpec javaspec) {
javaspec.describe(Greeter.class, () -> {
javaspec.describe("#greet", () -> {
javaspec.it("greets the world, given no name", () -> {
Greeter subject = new Greeter();
assertEquals("Hello world!", subject.greet());
});
});
});
}
}
Once you have the right dependencies, you need a way to run specs on the JUnit Platform. This section describes how to do that in a Gradle project.
As with regular JUnit tests, you still need to add this to your build.gradle
:
//build.gradle
test {
useJUnitPlatform()
}
Then it’s ./gradlew test
to run specs, like usual.
For extra-pretty console output, try adding the Gradle Test Logger
Plugin with the mocha
theme.
If you have an IDE that can already run JUnit 5 tests, there’s a good chance that it can also run JavaSpec by following these steps:
testImplementation 'org.junit.platform:junit-platform-commons:<version>'
@Testable
to each SpecClass
that contains specifications, as a hint
to your IDE that this class contains some sort of tests that run on a
TestEngine
.This is usually enough for your IDE to indicate that it can run tests in a class, once it has had time to download any new dependencies and index your sources.
For example:
import org.junit.platform.commons.annotation.Testable;
@Testable //Add this IDE hint
public class GreeterSpecs implements SpecClass {
public void declareSpecs(JavaSpec javaspec) {
javaspec.describe(Greeter.class, () -> {
...
});
}
}
Since this is just another TestEngine
for the JUnit Platform, you can also run
specs on the JUnit Platform Console as seen in this
shell snippet:
junit_console_jar='junit-platform-console-standalone-1.8.1.jar'
java -jar "$junit_console_jar" \
--classpath=info.javaspec.javaspec-api-2.0.0.jar \
--classpath=<compiled production code and its dependencies> \
--classpath=<compiled specs and their dependencies> \
--classpath=info.javaspec.javaspec-engine-2.0.0.jar \
--include-engine=javaspec-engine \
...
Specifically, this means running passing the following arguments to JUnit Platform Console, on top of whichever options you are already using:
--classpath
for javaspec-api
and javaspec-engine
--include-engine=javaspec-engine
The JavaSpec API supports a few more things that developers often need to do while testing:
JavaSpec#pending
: Stub in a pending / todo item reminding you to test
something later. JUnit Platform skips the resulting test.JavaSpec#skip
: Skip running a spec that already has a defined procedure.
This can be useful for allowing a spec to be temporarily disabled while you
fix something else.The API also has a variety of ways to help you organize your specs:
JavaSpec#describe
is used most often to define the class and methods being
tested, but it is really just a general-purpose container with no special
behavior of its own.JavaSpec#context
can be useful for defining any circumstances under which
some specifications apply. It’s not implemented any differently from
#describe
, so use #context
if you feel like it reads better.JavaSpec#given
is like the other containers, except that it adds the word
“given” before your description. For example
javaspec.given("a name", () -> ...)
results in a container called given a name
.Note that these containers exist simply to help you be as descriptive and organized as you need to be. Try to use them judiciously to enhance human readability.
Feel free to file an Issue on Github if you have any questions about using JavaSpec, or if something is not working the way you expected.
@BeforeEach
and @AfterEach
yet, for defining
shared setup and teardown around a series of related specs.default-package
and UnknownClass
instead of their actual package and class names. This
applies to HTML test reports in build/reports/tests/test
.