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'
: theTestEngine
that runs specs. This only needs to be on the classpath you use at runtime when running tests.- some kind of library for assertions. For example:
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
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:<version>'
testRuntimeOnly 'info.javaspec:javaspec-engine:<version>'
//Add an assertion library (JUnit 5's assertions shown here)
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
}
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:
- Implement
SpecClass
(info.javaspec:javaspec-api
) and#declareSpecs(JavaSpec)
. - Inside
declareSpecs
, callJavaSpec#describe
with:- some description of what you are testing (or the class itself)
- a lambda to hold all the specifications for what you're describing
- Inside the
describe
lambda, callJavaSpec#it
with:- some description of the expected behavior
- a lambda with Arrange Act Assert in it, like in any other test
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:
- Make sure you have added the dependencies listed in Add Dependencies.
- Add the JUnit Platform Commons dependency:
testImplementation 'org.junit.platform:junit-platform-commons:<version>'
- Add
@Testable
to eachSpecClass
that contains specifications, as a hint to your IDE that this class contains some sort of tests that run on aTestEngine
.
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-0.0.1.jar \
--classpath=<compiled production code and its dependencies> \
--classpath=<compiled specs and their dependencies> \
--classpath=info.javaspec.javaspec-engine-0.0.1.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
forjavaspec-api
andjavaspec-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 examplejavaspec.given("a name", () -> ...)
results in a container calledgiven 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.
- There is not an equivalent of
@BeforeEach
and@AfterEach
yet, for defining shared setup and teardown around a series of related specs. - Running specs in Gradle causes specs to be reported under
default-package
andUnknownClass
instead of their actual package and class names. This applies to HTML test reports inbuild/reports/tests/test
. - Running specs in Gradle with the Gradle Test Logger Plugin causes the name of the spec class to be printed after all the specs in the class have run, instead of before it.