diff --git a/build.gradle b/build.gradle index 389dfca6dee7..e3d0ff2b0955 100644 --- a/build.gradle +++ b/build.gradle @@ -187,6 +187,12 @@ allprojects { subproj -> options.compilerArgs += '-parameters' } + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { + kotlinOptions { + jvmTarget = '1.8' + } + } + checkstyle { toolVersion = '7.6' } @@ -537,6 +543,13 @@ subprojects { subproj -> replaceRegex 'Empty line between last method and class closure', /\n([\s]+)}\n}\n$/, '\n$1}\n\n}\n' replaceRegex 'Remove line breaks between consecutive closing parentheses', /\)\n[\s]+\)\n/, '))\n' } + + kotlin { + ktlint("0.9.0") + licenseHeaderFile headerFile + trimTrailingWhitespace() + endWithNewline() + } } afterEvaluate { diff --git a/documentation/documentation.gradle b/documentation/documentation.gradle index 37c88f1ae66c..3ce91d33983c 100644 --- a/documentation/documentation.gradle +++ b/documentation/documentation.gradle @@ -39,6 +39,7 @@ dependencies { testImplementation(project(path: ':junit-jupiter-params', configuration: 'shadow')) testImplementation(project(':junit-platform-runner')) testImplementation(project(':junit-platform-launcher')) + testImplementation("org.jetbrains.kotlin:kotlin-stdlib") // Include junit-platform-console so that the JUnit Gradle plugin // uses the local version of the ConsoleLauncher. @@ -91,6 +92,7 @@ asciidoctor { 'revnumber' : project.version, 'mainDir': project.sourceSets.main.java.srcDirs[0], 'testDir': project.sourceSets.test.java.srcDirs[0], + 'kotlinTestDir' : project.sourceSets.test.kotlin.srcDirs[0], 'generatedAsciiDocInputDir': generatedAsciiDocInputDir, 'testResourcesDir': project.sourceSets.test.resources.srcDirs[0], 'outdir': outputDir.absolutePath, diff --git a/documentation/src/docs/asciidoc/release-notes-5.1.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes-5.1.0-M1.adoc index e15dfb65eb2f..e228504358a6 100644 --- a/documentation/src/docs/asciidoc/release-notes-5.1.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes-5.1.0-M1.adoc @@ -3,7 +3,7 @@ *Date of Release:* ❓ -*Scope:* ❓ +*Scope:* Addition of Kotlin assertion helpers. For a complete list of all _closed_ issues and pull requests for this release, consult the link:{junit5-repo}+/milestone/14?closed=1+[5.1 M1] milestone page in the JUnit repository @@ -68,7 +68,9 @@ on GitHub. * The `JupiterTestEngine` supports the new JUnit Platform `ModuleSelector` for selecting Java 9 modules. - This is an alternative to the existing classpath scanning support. - +* New Kotlin friendly assertions added as top-level functions in the `org.junit.jupiter.api` package. +** `assertAll` that takes `Stream<() -> Unit>` or `vararg () -> Unit`. +** `assertThrow` that uses Kotlin reified generics. [[release-notes-5.1.0-junit-vintage]] ==== JUnit Vintage diff --git a/documentation/src/docs/asciidoc/writing-tests.adoc b/documentation/src/docs/asciidoc/writing-tests.adoc index 7321107cc861..ba1aab67f205 100644 --- a/documentation/src/docs/asciidoc/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/writing-tests.adoc @@ -94,6 +94,16 @@ are `static` methods in the `{Assertions}` class. include::{testDir}/example/AssertionsDemo.java[tags=user_guide] ---- +JUnit Jupiter also comes with a few assertion methods that lend themselves well to being +used in https://kotlinlang.org/[Kotlin]. All JUnit Jupiter Kotlin assertions are top-level +functions in the `org.junit.jupiter.api` package. + +// TODO: Change to using kotlin language highlighting after switch to rouge syntax highlighter +[source,java,indent=0] +---- +include::{kotlinTestDir}/example/AssertionsDemoKotlin.kt[tags=user_guide] +---- + [[writing-tests-assertions-third-party]] ==== Third-party Assertion Libraries diff --git a/documentation/src/test/java/example/AssertionsDemo.java b/documentation/src/test/java/example/AssertionsDemo.java index 96aaf85fb009..1c1e33731011 100644 --- a/documentation/src/test/java/example/AssertionsDemo.java +++ b/documentation/src/test/java/example/AssertionsDemo.java @@ -143,23 +143,3 @@ private static String greeting() { } // end::user_guide[] // @formatter:on - -class Person { - - private final String firstName; - private final String lastName; - - Person(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - String getFirstName() { - return firstName; - } - - String getLastName() { - return lastName; - } - -} diff --git a/documentation/src/test/kotlin/example/AssertionsDemoKotlin.kt b/documentation/src/test/kotlin/example/AssertionsDemoKotlin.kt new file mode 100644 index 000000000000..7c69a0199f7c --- /dev/null +++ b/documentation/src/test/kotlin/example/AssertionsDemoKotlin.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2017 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ +package example + +// tag::user_guide[] +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.assertThrows + +class AssertionsDemoKotlin { + + // end::user_guide[] + val person = Person("John", "Doe") + val people = setOf(person, Person("James", "Doe")) + + // tag::user_guide[] + @Test + fun `grouped assertions`() { + assertAll("person", + { assertEquals("John", person.firstName) }, + { assertEquals("Doe", person.lastName) } + ) + } + + @Test + fun `exception testing`() { + val exception = assertThrows ("Should throw an exception") { + throw IllegalArgumentException("a message") + } + assertEquals("a message", exception.message) + } + + @Test + fun `assertions from a stream`() { + assertAll( + "people with name starting with J", + people + .stream() + .map { + // This mapping returns Stream<() -> Unit> + { assertTrue(it.firstName.startsWith("J")) } + } + ) + } +} +// end::user_guide[] diff --git a/documentation/src/test/kotlin/example/Person.kt b/documentation/src/test/kotlin/example/Person.kt new file mode 100644 index 000000000000..d57ae4afd0e5 --- /dev/null +++ b/documentation/src/test/kotlin/example/Person.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2015-2017 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ +package example + +data class Person(val firstName: String, val lastName: String) diff --git a/junit-jupiter-api/junit-jupiter-api.gradle b/junit-jupiter-api/junit-jupiter-api.gradle index 360dcd82c81c..0383faec905b 100644 --- a/junit-jupiter-api/junit-jupiter-api.gradle +++ b/junit-jupiter-api/junit-jupiter-api.gradle @@ -1,12 +1,32 @@ dependencies { api("org.opentest4j:opentest4j:${ota4jVersion}") - api(project(':junit-platform-commons')) + api(project(":junit-platform-commons")) + compileOnly("org.jetbrains.kotlin:kotlin-stdlib") } jar { manifest { attributes( - 'Automatic-Module-Name': 'org.junit.jupiter.api' + 'Automatic-Module-Name': 'org.junit.jupiter.api' ) } } + +configurations { + apiElements { + /* + * Needed to configure kotlin to work correctly with the "java-library" plugin. + * See: + * https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_known_issues + * https://youtrack.jetbrains.com/issue/KT-18497 + */ + outgoing + .variants + .getByName("classes") + .artifact( + "file" : compileKotlin.destinationDir, + "type" : "java-classes-directory", + "builtBy" : compileKotlin + ) + } +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java index 9764e01573a2..04b49b06128c 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java @@ -29,6 +29,9 @@ * {@code Assertions} is a collection of utility methods that support asserting * conditions in tests. * + *

Additional Kotlin assertions can be found + * as top-level functions on the {@link org.junit.jupiter.api} package. + * *

Unless otherwise noted, a failed assertion will throw an * {@link org.opentest4j.AssertionFailedError} or a subclass thereof. * diff --git a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt new file mode 100644 index 000000000000..be0aee1a2a0d --- /dev/null +++ b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2015-2017 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ +@file:API(status = EXPERIMENTAL, since = "5.1") +package org.junit.jupiter.api + +import org.apiguardian.api.API +import org.apiguardian.api.API.Status.EXPERIMENTAL +import org.junit.jupiter.api.function.Executable +import java.util.function.Supplier +import java.util.stream.Stream + +/** + * [Stream] of functions to be executed. + */ +private typealias ExecutableStream = Stream<() -> Unit> +private fun ExecutableStream.convert() = map { Executable(it) } + +/** + * @see Assertions.assertAll + */ +fun assertAll(executables: ExecutableStream) = + Assertions.assertAll(executables.convert()) + +/** + * @see Assertions.assertAll + */ +fun assertAll(heading: String?, executables: ExecutableStream) = + Assertions.assertAll(heading, executables.convert()) + +/** + * @see Assertions.assertAll + */ +fun assertAll(vararg executables: () -> Unit) = + assertAll(executables.toList().stream()) + +/** + * @see Assertions.assertAll + */ +fun assertAll(heading: String?, vararg executables: () -> Unit) = + assertAll(heading, executables.toList().stream()) + +/** + * Example usage: + * ```kotlin + * val exception = assertThrows { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * @see Assertions.assertThrows + */ +inline fun assertThrows(noinline executable: () -> Unit): T = + Assertions.assertThrows(T::class.java, Executable(executable)) + +/** + * Example usage: + * ```kotlin + * val exception = assertThrows("Should throw an Exception") { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * @see Assertions.assertThrows + */ +inline fun assertThrows(message: String, noinline executable: () -> Unit): T = + assertThrows({ message }, executable) + +/** + * Example usage: + * ```kotlin + * val exception = assertThrows({ "Should throw an Exception" }) { + * throw IllegalArgumentException("Talk to a duck") + * } + * assertEquals("Talk to a duck", exception.message) + * ``` + * @see Assertions.assertThrows + */ +inline fun assertThrows(noinline message: () -> String, noinline executable: () -> Unit): T = + Assertions.assertThrows(T::class.java, Executable(executable), Supplier { + /* + * This is a hacky workaround due to a bug in how the JDK 9 JavaDoc code generator interacts with the + * generated Kotlin Bytecode. + * https://youtrack.jetbrains.com/issue/KT-20025 + */ + message() + }) diff --git a/junit-jupiter-engine/junit-jupiter-engine.gradle b/junit-jupiter-engine/junit-jupiter-engine.gradle index f4024934aa68..2a2ad547897f 100644 --- a/junit-jupiter-engine/junit-jupiter-engine.gradle +++ b/junit-jupiter-engine/junit-jupiter-engine.gradle @@ -47,7 +47,7 @@ dependencies { testImplementation(project(path: ':junit-platform-engine', configuration: 'testArtifacts')) testImplementation("org.assertj:assertj-core:${assertJVersion}") testImplementation("org.mockito:mockito-core:${mockitoVersion}") - testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}") + testImplementation("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") // Include junit-platform-console so that the JUnit Gradle plugin // uses the local version of the ConsoleLauncher. diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertAllAssertionsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertAllAssertionsTests.java index e0336d0bb2f4..040826503314 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertAllAssertionsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertAllAssertionsTests.java @@ -149,7 +149,7 @@ void assertAllWithExecutableThatThrowsBlacklistedException() { } @SafeVarargs - private static void assertExpectedExceptionTypes(MultipleFailuresError multipleFailuresError, + static void assertExpectedExceptionTypes(MultipleFailuresError multipleFailuresError, Class... exceptionTypes) { assertNotNull(multipleFailuresError, "MultipleFailuresError"); diff --git a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/AssertionsAssertAllKotlinTests.kt b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/AssertionsAssertAllKotlinTests.kt new file mode 100644 index 000000000000..60ee0e7a3526 --- /dev/null +++ b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/AssertionsAssertAllKotlinTests.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2017 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ +package org.junit.jupiter.api + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith +import org.junit.jupiter.api.AssertionTestUtils.expectAssertionFailedError +import org.opentest4j.AssertionFailedError +import org.opentest4j.MultipleFailuresError +import java.util.stream.Stream +import kotlin.reflect.KClass + +/** + * Unit tests for JUnit Jupiter [org.junit.jupiter.api] top-level assertion functions. + */ +class AssertionsAssertAllKotlinTests { + + // Bonus: no null check tests as these get handled by the compiler! + + @Test + fun `assertAll with functions that do not throw exceptions`() { + assertAll( + { assertTrue(true) }, + { assertFalse(false) }, + { assertTrue(true) } + ) + } + + @Test + fun `assertAll with functions that throw AssertionErrors`() { + val multipleFailuresError = assertThrows { + assertAll( + { assertFalse(true) }, + { assertFalse(true) } + ) + } + assertExpectedExceptionTypes(multipleFailuresError, AssertionFailedError::class, AssertionFailedError::class) + } + + @Test + fun `assertAll with streamOf functions that throw AssertionErrors`() { + val multipleFailuresError = assertThrows("Should have thrown multiple errors") { + assertAll(Stream.of({ assertFalse(true) }, { assertFalse(true) })) + } + assertExpectedExceptionTypes(multipleFailuresError, AssertionFailedError::class, AssertionFailedError::class) + } + + @Test + fun `assertThrows that does not have exception thrown does not throw KotlinNullPointer`() { + val assertionMessage = "This will not throw an exception" + val error = assertThrows("assertThrows did not throw the correct exception") { + assertThrows(assertionMessage) { } + // This should never execute + expectAssertionFailedError() + } + assertMessageStartsWith(error, assertionMessage) + } + + companion object { + fun assertExpectedExceptionTypes( + multipleFailuresError: MultipleFailuresError, + vararg exceptionTypes: KClass) = + AssertAllAssertionsTests.assertExpectedExceptionTypes( + multipleFailuresError, *exceptionTypes.map { it.java }.toTypedArray()) + } +} diff --git a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerClassKotlinTestCase.kt b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerClassKotlinTestCase.kt index fa327a939555..99bd5d29f9d6 100644 --- a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerClassKotlinTestCase.kt +++ b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerClassKotlinTestCase.kt @@ -1,48 +1,62 @@ +/* + * Copyright 2015-2017 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ package org.junit.jupiter.engine.kotlin -import org.junit.jupiter.api.* +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS @TestInstance(PER_CLASS) class InstancePerClassKotlinTestCase { - companion object { - @JvmField - val TEST_INSTANCES: MutableMap> = HashMap() - } - - @BeforeAll - fun beforeAll() { - increment("beforeAll") - } - - @BeforeEach - fun beforeEach() { - increment("beforeEach") - } - - @AfterEach - fun afterEach() { - increment("afterEach") - } - - @AfterAll - fun afterAll() { - increment("afterAll") - } - - @Test - fun firstTest() { - increment("test") - } - - @Test - fun secondTest() { - increment("test") - } - - private fun increment(name: String) { - TEST_INSTANCES.computeIfAbsent(this, { _ -> HashMap() }) - .compute(name, { _, oldValue -> (oldValue ?: 0) + 1 }) - } + companion object { + @JvmField + val TEST_INSTANCES: MutableMap> = HashMap() + } + + @BeforeAll + fun beforeAll() { + increment("beforeAll") + } + + @BeforeEach + fun beforeEach() { + increment("beforeEach") + } + + @AfterEach + fun afterEach() { + increment("afterEach") + } + + @AfterAll + fun afterAll() { + increment("afterAll") + } + + @Test + fun firstTest() { + increment("test") + } + + @Test + fun secondTest() { + increment("test") + } + + private fun increment(name: String) { + TEST_INSTANCES.computeIfAbsent(this, { _ -> HashMap() }) + .compute(name, { _, oldValue -> (oldValue ?: 0) + 1 }) + } } diff --git a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerMethodKotlinTestCase.kt b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerMethodKotlinTestCase.kt index c2caa69595ad..b9fbd349ba86 100644 --- a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerMethodKotlinTestCase.kt +++ b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/engine/kotlin/InstancePerMethodKotlinTestCase.kt @@ -1,48 +1,61 @@ +/* + * Copyright 2015-2017 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * http://www.eclipse.org/legal/epl-v20.html + */ package org.junit.jupiter.engine.kotlin -import org.junit.jupiter.api.* +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test class InstancePerMethodKotlinTestCase { - companion object { - @JvmField - val TEST_INSTANCES: MutableMap> = LinkedHashMap() - - @JvmStatic - @BeforeAll - fun beforeAll() { - increment(this, "beforeAll") - } - - @JvmStatic - @AfterAll - fun afterAll() { - increment(this, "afterAll") - } - - private fun increment(instance: Any, name: String) { - TEST_INSTANCES.computeIfAbsent(instance, { _ -> LinkedHashMap() }) - .compute(name, { _, oldValue -> (oldValue ?: 0) + 1 }) - } - } - - @BeforeEach - fun beforeEach() { - increment(this, "beforeEach") - } - - @AfterEach - fun afterEach() { - increment(this, "afterEach") - } - - @Test - fun firstTest() { - increment(this, "test") - } - - @Test - fun secondTest() { - increment(this, "test") - } + companion object { + @JvmField + val TEST_INSTANCES: MutableMap> = LinkedHashMap() + + @JvmStatic + @BeforeAll + fun beforeAll() { + increment(this, "beforeAll") + } + + @JvmStatic + @AfterAll + fun afterAll() { + increment(this, "afterAll") + } + + private fun increment(instance: Any, name: String) { + TEST_INSTANCES.computeIfAbsent(instance, { _ -> LinkedHashMap() }) + .compute(name, { _, oldValue -> (oldValue ?: 0) + 1 }) + } + } + + @BeforeEach + fun beforeEach() { + increment(this, "beforeEach") + } + + @AfterEach + fun afterEach() { + increment(this, "afterEach") + } + + @Test + fun firstTest() { + increment(this, "test") + } + + @Test + fun secondTest() { + increment(this, "test") + } }