class JUnit5Test {
@Test
void test() {
assertTrue(true);
}
}
JUnit Pioneer — junit-pioneer.org:
provides extensions for JUnit 5 and its Jupiter API
small project (10.6k lines of code, 4 maintainers)
Why is it interesting?
JUnit 5 is thrilling
grew a (small) community on Twitch
neat build and Git practices
one-click releases
automatic website build
JUnit 5 and its extension model
Pioneer’s extensions, mission, and history
how live-streaming grew a community
organizational style and contribution guide
architecture and dependency management
building with quality control and
on multiple Java/JUnit versions and OS
one-click releases
website build
Crash course to JUnit 5 and
its extension model.
(This is very incomplete!
More in the user guide or this article.)
A simple test:
class JUnit5Test {
@Test
void test() {
assertTrue(true);
}
}
need an API to write tests against
need an engine that executes them
Separation of concerns in JUnit 5:
an API to write tests against
an API to discover and run tests
specific engine per variant of tests
(e.g. JUnit 4 or JUnit 5)
orchestration of engines
API between them
defines test API
defines extension API
contains engine for them
Often referred to as "JUnit 5".
Jupiter allows seamless extensions:
@Test
@DisabledOnFriday
void failingTest() {
assertTrue(false);
}
Cornerstones:
extensions interact with extension points
extensions must be registered
(possibly with annotations)
An incomplete list:
instance post processor
@BeforeAll
and @BeforeEach
execution condition
exception handling
@AfterEach
and @AfterAll
Each represented by an interface,
e.g. ExecutionCondition
.
How to disable tests on Friday:
public class DisabledOnFridayCondition
implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluate(/**/) {
if (isFriday())
return ConditionEvaluationResult
.disabled("Weekend! 🕺🏾💃🏼");
else
return ConditionEvaluationResult
.enabled("Run the test 😬");
}
}
Jupiter is aware of meta annotations.
⇝ We can create our own annotations!
// [... more annotations ...]
@ExtendWith(DisabledOnFridayCondition.class)
public @interface DisabledOnFriday { }
@Test
@DisabledOnFriday
void failingTest() {
assertTrue(false);
}
An incomplete list:
Cartesian products for parameters
set default locale
set system properties
publish issue information
retry failed tests
Let’s see some incomplete examples!
@CartesianProductTest
@CartesianValueSource(ints = { 1, 2 })
@CartesianValueSource(strings = { "A", "B" })
void test(int number, String character) {
// called four times - with:
// (1, "A") (1, "B") (2, "A") (2, "B")
}
@Test
@DefaultLocale(language = "en")
void test() {
assertThat(Locale.getDefault())
.isEqualTo(new Locale("en"));
}
@Test
@ClearSystemProperty(key = "user.name")
@SetSystemProperty(key = "user.dir", value = "...")
void test() {
assertThat(System.getProperty("user.name"))
.isNull();
assertThat(System.getProperty("user.dir"))
.isEqualTo("...");
}
Mark tests that belong to issues:
@Test
@Issue("REQ-123")
void test() {
// a test for the issue "REQ-123"
}
Process information after execution:
@RetryingTest(3)
void test() {
// if 1st, 2nd, or 3rd execution passes,
// the test passes; otherwise fails
}
We have a few more:
ranges for parameters
set default time zone
set environment variables
disable specific parameterized tests
publish report entries
mocking standard I/O
Isn’t this all a bit random?
⇝ Yes!
Mission statement:
JUnit Pioneer provides extensions for JUnit 5 and its Jupiter API. It does not limit itself to proven ideas with wide application but is purposely open to experiments. It aims to spin off successful and cohesive portions into sibling projects or back into the JUnit 5 code base.
We want to:
provide helpful extensions
be open to experimental ideas
catch-all extensions that are too small
to be their own project
We are:
Matthias Bünger — Bishuemaster
Mihály Verhás — Chief Developer
Nicolai Parlog — Benevolent Dictator
Simon Schottner — Build Engineer
Plus about a dozen contribtors,
some of them recurring.
We have:
4.7k lines of production code
5.9k lines of test code
26 releases, 5 since 1.0.0
Java 8 as requirement
Java 9-15 supported
Sounds good? Not so fast!
this is a hobby project
life gets in the way
for 3 years, little happened
Nicolai discovers Twitch
does a few live streams in spring
decides to stream regularly in December
30 JUnit Pioneer streams
usually 3 to 5 hours
Expected effects:
commits Nicolai to ~10 h/month for Pioneer
interests Java devs
Unexpected effects:
gives viewers insight into project
viewers help in their areas of expertise
in Twitch chat
on GitHub
viewers pick up small issues
viewers become contributors
contributors become maintainers
Thanks to live-streaming, we became a small community.
In November 2020, we got together for donations:
Twitch sub money + individual donations
570 EUR to Climate Action Fund and DKMS
When the pandemic is over,
we’ll finally meet for drinks.
🍺🍹🥛🥃
Thanks to live-streaming, this one-man show,
became a real project:
in April 2020, Simon and Matthias
became maintainers
in November 2020, Mihaly joined
A few contributors also stop by the stream
(and always enjoy when Nicolai reviews their PRs).
communication & prioritization
doing the dirty work
fostering contributions
managing expectations
Once a repo turns into a project,
project management becomes essential.
There’s plenty of ways of
contributing without coding.
These contributions are often
more important than code.
curating issues
creating, labeling, replying, closings
prioritizing, organizing
reviewing PRs
technical merits
completeness
following up on uncomfortable tasks
remembering/updating documentation
Coding is fun! Cleaning up (often) isn’t.
As a maintainer:
fix bugs first
tackle the hard issues
(annotations, threading, merge conflicts)
set up build pipeline, releases,
documentation, website, etc.
We created milestone Cleaner Pioneers for that.
Spring 2020 we started thinking about 1.0,
but Pioneer has no cohesive feature set.
⇝ there’s no good point for 1.0
Instead, prepare everything for users and contributors.
⇝ Cleaner Pioneers became 1.0
Pioneer still has no cohesive feature set
all must-dos are done (for the time being)
⇝ Milestone is wrong concept.
So now we use a Kanban board
(via GitHub’s Projects feature).
Noteworthy details about PRs:
checklist
approval
squash & merge
Two maintainers need to approve a PR.
Lack of trust?
No, sharing responsibility.
(More on that later.)
When a PR is ready to be merged:
all commits are squashed into one
commit message is carefully crafted
that commit goes onto main
"But don’t you loose the history?"
Yes!
lets contributors use Git however they like
keeps commit history clean
leads to really good commit messages
(prepared as part of the PR)
appreciation
contribution guide
explicit rules
(preferably simple)
We’re appreciative:
positive tone
prioritize replies
thank for contributions,
excuse delays
list contributions
We have a (very long) CONTRIBUTING.md
:
describes all aspects in detail
binds maintainers and contributors
grew organically over time
(more in a few slides)
There’s no expectation of availability! This applies to users opening issues, contributors providing PRs, and other maintainers - none of them can expect a maintainer to have time to reply to their request.
Struggle for newest maintainers:
first open source project
project pre-existed
worried to break things
Solution:
two maintainers sign off PRs
Nicolai is the benevolent dictator
Nicolai has special…
privilege — can overrule anything
duty — should’ve prevented all mistakes
Writes Nicolai:
I bare responsibility for all mistakes. (Moral responsibility, that is - legally, nobody has any responsibility. 😉)
As you can see, quite a lof of
project and team management.
Many ways to contribute without coding.
architecture
dependency management
how to test a test framework
JUnit Pioneer has high-end architecture:
📦 org.junitpioneer
├─ 📦 internal # our utilities - internal
├─ 📦 jupiter # most of our extensions
│ ├─ 📦 issue # issue impl - internal
│ └─ 📦 params # ext. re parameterized tests
└─ 📦 vintage # ext. re to JUnit 4
We mirror Jupiter’s packages:
org.junit.jupiter.api
org.junit.jupiter.params
Speaking of internal packages:
we created two (e.g. for annotations)
we don’t want people to use them
How?
⚠️ one package is called internal
⚠️ package info says it’s internal
🛑 we ship modular JAR that
doesn’t export these packages
Projects have enough problems
with dependencies.
We don’t want to add to that.
⇝ JUnit 5 should be our only
runtime dependency.
The @Issue
extension:
collects information about tests
wants to create a report
But reports could be XML, JSON, etc.
"need" dependencies for that
probably not many users use @Issue
What now?
Dependency inversion to the rescue
(via Java’s ServiceLoader
):
Pioneer declares interface IssueProcessor
users implement it and register implementation
Pioneer finds implementations and passes info
How do you test a test framework?
want to verify error behavior
want to test behavior outside of tests
(e.g. report entry publication)
We often write tests that run other tests
and then evaluate their results.
JUnit 5 has good support for that:
we added a few usability methods
we created our own assertions
ExecutionResults results = executeTestMethod(
MethodLevelInitializationFailureTestCase.class,
"shouldFailMissingConfiguration");
assertThat(results)
.hasSingleFailedTest()
.withExceptionInstanceOf(/*...*/);
Thread safety:
all our extensions should be thread-safe
to test that, we run our tests in parallel
that’s not always fun
We’re mostly sure, we got this. 😬
quality control
compatibility
one-click releases
website build
build project with Gradle (Kotlin style)
trigger build on commit with GitHub Actions
kick off release build on GitHub
trigger website build
Code style:
we want a uniform code style
we don’t want to manually
discuss or enforce it
So we let Spotless
and Checkstyle
break the build on violations.
Code quality:
we want to avoid pitfalls and gotchas
we want high test coverage
we know better than any tool 😁
We use SonarQube
to analyze and report.
Build against:
range of operating systems
range of Java versions
as module and not
range of JUnit versions
All with GitHub Actions.
Manually dispatched workflow
on GitHub Actions:
Kicks of Gradle release build.
In release build, Gradle uses Shipkit to:
detect next version
build artifacts
upload to Bintray (with Bintray plugin)
create Git tag
create GitHub release with changelog
Last step triggers website build:
pulls in all projects
builds website with docs
pulls version(s) from files
builds website with Jekyll
From code to community, from planning to building
we hope you saw something interesting!
To follow up:
visit junit-pioneer.org
come to github.com/junit-pioneer
watch nipafx on Twitch
tweet @nipafx (DMs open)
join our Discord