Please note that this project is now part of the Kommons library.
Below you will find the description of the last release.
Kommons Test is a Kotlin Multiplatform Library to ease testing for kotlin.test and JUnit users.
Adding this library as a dependency...
- adds Kotest Assertions based helpers and matchers, and
- provides you with some useful fixtures.
JUnit users benefit from:
- an optimized set of defaults settings,
- the testEach dynamic test builder with automatically derived display name,
- a @SystemProperty extension,
- and a small selection of parameter resolvers, among other things.
Furthermore, there are some more advanced features for the JVM platform.
Kommons Test is hosted on GitHub with releases provided on Maven Central.
-
Gradle
testImplementation("com.bkahlert.kommons:kommons-test:0.4.4") { because("JUnit defaults, testAll, ...") }
-
Gradle
implementation("com.bkahlert.kommons:kommons-test:0.4.4") { because("JUnit defaults, testAll, ...") }
(for MPP projects) -
Maven
<dependency> <groupId>com.bkahlert.kommons</groupId> <artifactId>kommons-test</artifactId> <version>0.4.4</version> <scope>test</scope> </dependency>
Write a bunch of soft assertions conveniently in a single test:
@Test fun test_contain() = testAll {
"foo bar" shouldContain "Foo"
"foo bar" shouldContain "foo"
"foo bar" shouldContain "baz"
}
The above test has three assertions of which the first and last fail when run with the following output:
The following 2 assertions failed:
1) "foo bar" should include substring "Foo"
at sample.Tests.test_contain(Tests.kt:1)
2) "foo bar" should include substring "baz"
at sample.Tests.test_contain(Tests.kt:3)
Write a bunch of soft assertions conveniently for one or more subjects in a single test:
@Test fun test_contain() = testAll("foo bar", "FOO BAR") {
it shouldContain "foo"
it shouldContain "bar"
it shouldContain "BAR"
}
// The following invocations are equivalent:
testAll("foo bar", "FOO BAR") { /* ... */ }
listOf("foo bar", "FOO BAR").testAll { /* ... */ }
sequenceOf("foo bar", "FOO BAR").testAll { /* ... */ }
mapOf("key1" to "foo bar", "key2" to "FOO BAR").testAll { (_, v) -> /* ... */ }
The above test has three assertions of which the first and second fail when run with the following output:
0 elements passed but expected 2
The following elements passed:
--none--
The following elements failed:
"foo bar" => "foo bar" should include substring "BAR"
"FOO BAR" =>
The following 2 assertions failed:
1) "FOO BAR" should include substring "foo"
at sample.Tests.test_contain(Tests.kt:1)
2) "FOO BAR" should include substring "bar"
at sample.Tests.test_contain(Tests.kt:2)
Write a bunch of soft assertions conveniently for all enum entries in a single test:
enum class FooBar { foo_bar, FOO_BAR }
@Test fun test_contain() = testEnum<FooBar> {
it.name shouldContain "foo"
it.name shouldContain "bar"
it.name shouldContain "BAR"
}
The above test has three assertions of which the first and second fail when run with the following output:
0 elements passed but expected 2
The following elements passed:
--none--
The following elements failed:
foo_bar => "foo_bar" should include substring "BAR"
FOO_BAR =>
The following 2 assertions failed:
1) "FOO_BAR" should include substring "foo"
at sample.Tests.test_contain(Tests.kt:3)
2) "FOO_BAR" should include substring "bar"
at sample.Tests.test_contain(Tests.kt:4)
Match single- and multiline strings with glob patterns:
@Test fun test_glob_match() = testAll {
val multilineString = """
foo
.bar()
.baz()
""".trimIndent()
// ✅ matches thanks to the multiline wildcard **
multilineString shouldMatchGlob """
foo
.**()
""".trimIndent()
// ❌ fails to match since the simple wildcard *
// does not match across line breaks
multilineString shouldMatchGlob """
foo
.*()
""".trimIndent()
}
The preceding test has two assertions of which the second fails when run with the following output:
"""
foo
.bar()
.baz()
"""
should match the following glob pattern \
(wildcard: *, multiline wildcard: **, line separators: CRLF (\r\n), LF (\n), CR (\r)):
"""
foo
.*()
"""
Or, you can use shouldMatchCurly
/ shouldNotMatchCurly
/ matchCurly
if you prefer SLF4J / Logback style
wildcards {}
and {{}}
.
Testing file operations and tired of making up data to test with?
To help you focus on your actual test, the following binary and textual file fixtures are provided:
- GifImageFixture
A GIF image consisting of a red and white pixel. - SvgImageFixture
An SVG image with the animated Kommons logo. - HtmlDocumentFixture
An HTML document that renders "Hello World!" on a red white striped background. - UnicodeTextDocumentFixture
A text document containing different line separators.
An UTF-8 encoded character can take between one and four bytes;
this document includes at least one character for each encoding length. - EmojiTextDocumentFixture
A text document encompassing the differently composed emoji 🫠, 🇩🇪, 👨🏾🦱, and 👩👩👦👦.
GifImageFixture.name // "pixels.gif"
GifImageFixture.mimeType // "image/gif"
GifImageFixture.content // byte array
GifImageFixture.dataURI // "data:image/gif;base64,R0lGODdhAQADAPABAP////8AACwAAAAAAQADAAACAgxQADs="
Furthermore, on the JVM you'll find a bunch of extensions such as copyToTempFile
.
If all you want is any files, you can use createAnyFile
, createRandomFile
and createDirectoryWithFiles
.
Find the class directory, the source directory or the source file itself of a class.
Foo::class.findClassesDirectoryOrNull() // /home/john/dev/project/build/classes/kotlin/jvm/test
Foo::class.findSourceDirectoryOrNull() // /home/john/dev/project/src/jvmTest/kotlin
Foo::class.findSourceFileOrNull() // /home/john/dev/project/src/jvmTest/kotlin/packages/source.kt
Ever wondered what the code that triggered an exception looks like?
Doing so was never easier with getLambdaBodyOrNull
:
val caught = catchException {
foo {
bar {
val now = Instant.now()
throw RuntimeException("failed at $now")
}
}
}
val bodyOfBarCall = caught.getLambdaBodyOrNull()?.body
// """
// val now = Instant.now()
// throw RuntimeException("failed at ${'$'}now")
// """
val bodyOfFooCall = caught.getLambdaBodyOrNull("foo")?.body
// """
// bar {
// val now = Instant.now()
// throw RuntimeException("failed at $now")
// }
// """
// helper
private fun <R> foo(block: () -> R): R = block()
private fun <R> bar(block: () -> R): R = block()
private inline fun catchException(block: () -> Nothing): Throwable =
try {
block()
} catch (e: Throwable) {
e
}
This library comes with a junit-platform.properties
and the following settings:
# concise test names with no parameter list
junit.jupiter.displayname.generator.default=\
com.bkahlert.kommons.test.junit.MethodNameOnlyDisplayNameGenerator
# default 10s timeout for each test
junit.jupiter.execution.timeout.default=10s
# disable timeout when debugging
junit.jupiter.execution.timeout.mode=disabled_on_debug
# run top-level test containers in parallel
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=5
# run tests inside a test tree sequentially
junit.jupiter.execution.parallel.mode.default=same_thread
# auto-detect extensions located in META-INF/services
junit.jupiter.extensions.autodetection.enabled=true
# instantiate test classes only once for all tests
# same as annotating all test classes with @TestInstance(PER_CLASS)
junit.jupiter.testinstance.lifecycle.default=per_class
# enable constructor dependency injection for Spring tests
spring.test.constructor.autowire.mode=all
Please note that the default timeout is set to 10 seconds.
The following annotations are provided to change the timeout for
single tests or whole test containers.
@OneMinuteTimeout
@TwoMinutesTimeout
/@Slow
(also adds the tagslow
)@FiveMinutesTimeout
@TenMinutesTimeout
@FifteenMinutesTimeout
@ThirtyMinutesTimeout
Platform properties have the lowest precedence and can be overridden with system properties.
If, for example, you want to change the default timeout to 30s for all tests, in Gradle you can configure:
tasks {
test {
useJUnitPlatform()
systemProperty("junit.jupiter.execution.timeout.default", "30s")
}
}
Please consult Configuration Parameters for more information.
Test results are printed at the end of a test run by TestExecutionReporter as follows:
120 tests within 1.8s: ✘︎ 2 failed, ϟ 3 crashed, ✔︎ 113 passed, 2 ignored
Or if all went well:
120 tests within 1.55s: ✔︎ all passed
This feature is enabled by default but can be disabled by setting:
com.bkahlert.kommons.test.junit.launcher.reporter.disabled=true
Write a bunch of soft assertions conveniently for multiple subjects in a single test.
In contrast to testAll this function returns a DynamicNode stream with one DynamicTest for each test subject.
Also, a TestFactory annotation has to be used in place of Test.
@TestFactory fun test_contain() = testEach("foo bar", "FOO BAR") {
it shouldContain "foo"
it shouldContain "bar"
it shouldContain "BAR"
}
// The following invocations are equivalent:
testEach("foo bar", "FOO BAR") { /* ... */ }
listOf("foo bar", "FOO BAR").testEach { /* ... */ }
sequenceOf("foo bar", "FOO BAR").testEach { /* ... */ }
mapOf("key1" to "foo bar", "key2" to "FOO BAR").testEach { (_, v) -> /* ... */ }
The above test has three assertions of which the first and second fail when run.
class UniqueIdResolverTest {
@Nested inner class NestedTest {
@Test fun test_name(uniqueId: UniqueId) {
uniqueId.segments.first() // "[engine:junit-jupiter]"
uniqueId.segments.last() // "[method:test_name(org.junit.platform.engine.UniqueId)]"
}
}
}
class SimpleIdResolverTest {
@Nested inner class NestedTest {
@Test fun test_name(simpleId: SimpleId) {
simpleId.segments.first() // "SimpleIdResolverTest"
simpleId.segments.last() // "test_name"
simpleId.toString() // "SimpleIdResolverTest.test_name"
}
}
}
class DisplayNameResolverTest {
@Nested inner class NestedTest {
@Test fun `test name`(displayName: DisplayName) {
displayName.displayName // "test_name"
displayName.composedDisplayName // "DisplayNameResolverTest ➜ NestedTest ➜ test_name"
}
}
}
class ExtensionContextResolverTest {
@Nested inner class NestedTest {
@Test fun `test name`(extensionContext: ExtensionContext) {
extensionContext.simpleId // "ExtensionContextResolverTest.NestedTest.test_name"
}
}
}
This extension allows you to set the system properties for the duration of a text execution.
Tests that use this annotation are guaranteed to not run concurrently.
class SystemPropertiesTest {
@Test
@SystemProperty(name = "foo", value = "bar")
fun test() {
System.getProperty("foo") // "bar"
}
}
For authors of JUnit extensions getStore
and getTestStore
can
be used to obtain differently namespaced stores.
Reified variants of getTyped
, getTypedOrDefault
, getTypedOrComputeIfAbsent
, and removeTyped
can be used in place of their type-safe counterparts that require
a class instance argument.
class MyExtension : BeforeAllCallback, BeforeEachCallback {
override fun beforeAll(context: ExtensionContext) {
// store the moment the tests were started in the container store
context.getStore<MyExtension>().put("start", Instant.now())
// will throw an exception because there is no current test
context.getTestStore<MyExtension>().put("foo", "bar")
}
override fun beforeEach(context: ExtensionContext) {
// returns the moment the tests were started
context.getStore<MyExtension>().getTyped<Instant>("start")
// returns null, because the store is namespaced with the test itself
context.getTestStore<MyExtension>().getTyped<Instant>("start")
}
}
Launch JUnit tests programmatically using launchTests
.
Use KotlinDiscoverySelectors to easily select the tests to run
explicitly using
selectKotlinClass
, selectKotlinMemberFunction
,
selectKotlinNestedClass
, selectKotlinNestedMemberFunction
.
Alternatively use select
to no longer have to write the full paths to your tests
yourself.
class FooTest {
@Test
fun test_foo() {
"foo" shouldBe "foo"
}
}
class BarTest {
@Test
fun test_bar() {
"bar" shouldBe "baz"
}
@Nested
inner class BazTest {
@Test
fun test_baz() {
"baz" shouldBe "baz"
}
}
}
fun main() {
// launches all FooTest tests and BazTest.test_bat()
launchTests(
select(FooTest::class),
select(BazTest::test_baz),
)
// same as above but with classic discovery function
launchTests(
selectClass(FooTest::class.java),
selectNestedMethod(listOf(BarTest::class.java), BazTest::class.java, "test_baz"),
)
// customizes how tests are discovered and run
launchTests(
select(FooTest::class),
select(BazTest::test_baz),
) {
request {
// customize discovery request
parallelExecutionEnabled(true)
}
config {
// customize launcher configuration
}
launcher {
// customize launcher
}
}
}
Want to contribute? Awesome! The most basic way to show your support is to star the project, or to raise issues. You can also support this project by making a PayPal donation to ensure this journey continues indefinitely!
Thanks again for your support, it is much appreciated! 🙏
MIT. See LICENSE for more details.