From 1388fcbacb10a4f849025e02cf3fc2b3bdb89d76 Mon Sep 17 00:00:00 2001 From: Robert Stoll Date: Thu, 5 Oct 2023 22:25:28 +0200 Subject: [PATCH] introduce expectGrouped (ExpectGrouping) and group --- README.md | 217 ++++++++++++------ .../api/fluent/en_GB/fun0Expectations.kt | 11 +- .../atrium/api/fluent/en_GB/grouping.kt | 51 ++++ .../atrium/api/fluent/en_GB/GroupingTest.kt | 8 + .../fluent/en_GB/samples/GroupingSamples.kt | 79 +++++++ .../api/infix/en_GB/fun0Expectations.kt | 7 +- .../atrium/api/infix/en_GB/grouping.kt | 52 +++++ .../atrium/api/infix/en_GB/GroupingTest.kt | 8 + .../infix/en_GB/samples/GroupingSamples.kt | 76 ++++++ .../assertions/GroupingAssertionGroupType.kt | 19 ++ .../assertions/builders/explanatoryGroup.kt | 2 +- .../tutteli/atrium/creating/ErrorMessages.kt | 2 +- .../ch/tutteli/atrium/creating/Expect.kt | 30 ++- .../atrium/creating/impl/BaseExpectImpl.kt | 2 +- .../impl/ComponentFactoryContainerImpl.kt | 7 + .../AssertionFormatterParameterObject.kt | 18 +- .../ch/tutteli/atrium/reporting/Text.kt | 6 + ...attingSingleAssertionGroupTypeFormatter.kt | 8 +- .../TextGroupingAssertionGroupFormatter.kt | 52 +++++ .../TextListBasedAssertionGroupFormatter.kt | 2 +- .../reporting/text/BulletPointProviderSpec.kt | 22 +- .../atrium/logic/GroupingAssertions.kt | 25 ++ .../logic/impl/DefaultGroupingAssertions.kt | 36 +++ .../kotlin/ch/tutteli/atrium/logic/logic.kt | 23 ++ .../kotlin/ch/tutteli/atrium/logic/utils.kt | 15 +- .../commonMain/ch/tutteli/atrium/logic/any.kt | 1 - .../ch/tutteli/atrium/logic/grouping.kt | 28 +++ misc/atrium-specs/build.gradle.kts | 5 + .../atrium/specs/defaultBulletPoints.kt | 6 +- .../atrium/specs/integration/GroupingTest.kt | 82 +++++++ .../ch/tutteli/atrium/specs/testUtils.kt | 3 + .../ch/tutteli/atrium/specs/verbs/VerbSpec.kt | 144 +++++++++++- .../atriumVerbs.kt | 49 ++-- .../atrium/api/verbs/internal/VerbSpec.kt | 12 +- .../tutteli/atrium/api.verbs/AssertionVerb.kt | 1 + .../ch/tutteli/atrium/api.verbs/expect.kt | 79 ++++++- .../ch/tutteli/atrium/api/verbs/VerbSpec.kt | 14 +- .../kotlin/readme/examples/DataDrivenSpec.kt | 75 ++++-- .../kotlin/readme/examples/utils/expect.kt | 21 +- 39 files changed, 1141 insertions(+), 157 deletions(-) create mode 100644 apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/grouping.kt create mode 100644 apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/GroupingTest.kt create mode 100644 apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/GroupingSamples.kt create mode 100644 apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/grouping.kt create mode 100644 apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/GroupingTest.kt create mode 100644 apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/GroupingSamples.kt create mode 100644 atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/GroupingAssertionGroupType.kt create mode 100644 atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextGroupingAssertionGroupFormatter.kt create mode 100644 logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/GroupingAssertions.kt create mode 100644 logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/impl/DefaultGroupingAssertions.kt create mode 100644 logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/grouping.kt create mode 100644 misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/integration/GroupingTest.kt diff --git a/README.md b/README.md index 2b858b0ed7..78e93e17a3 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Please have a look at the README of the corresponding release/git tag -- latest - [Examples](#examples) - [Your First Expectation](#your-first-expectation) - [Define Single Expectations or an Expectation-Group](#define-single-expectations-or-an-expectation-group) + - [Soft-Expectations](#soft-expectations) - [Expect an Exception](#expect-an-exception) - [Feature Extractors](#feature-extractors) - [Property and Method](#property-and-methods) @@ -287,9 +288,32 @@ An expectation-group throws an `AssertionError` at the end of its block (i.e. at hence reports that both expectations do not hold. The reporting can be read as `I expected the subject of the expectation, which was 10, to be less than 5 and to be greater than 10` -This is similar to the concept of soft assertions in AssertJ with the difference that you do not need an extra utility, -and you do not have to repeat the subject. -The above is the equivalent of the following AssertJ example: +
+ +You can use `and` as filling element between single expectations and expectation-groups: + + + +```kotlin +expect(5).toBeGreaterThan(2).and.toBeLessThan(10) + +expect(5) { + // ... +} and { // if the previous block fails, then this one is not evaluated + // ... +} +``` + + +### Soft-Expectations + +An [expectation-group](#define-single-expectations-or-an-expectation-group) is similar to the concept of +soft assertions in AssertJ although with a few differences: +- you do not need an extra utility such as `assertSoftly` if you define expectations about the same subject, + you can just use `expect` as always. +- you do not have to repeat the subject + +The [above example](#ex-group) is the equivalent of the following AssertJ example: ```kotlin assertSoftly { assertThat(4 + 6).isLessThan(5) @@ -323,26 +347,18 @@ expect(mansion) { } ``` -Note that you are free to choose a fail-fast behaviour at any level. For instance, above we have used the single -expectation syntax for `toBeGreaterThan(5).toBeLessThan(10)` and thus `toBeLessThan(10)` will not show up in reporting -if `toBeGreaterThan(5)` already fails. - -
- -You can use `and` as filling element between single expectations and expectation-groups: +
+💬 fail-fast in expectation-groups - +Note that you are free to choose the dot-notation (e.g. `toBeGreaterThan(5).toBeLessThan(10)`) at any level, however +once you are within an expectation-group block, all of them are evaluated (no more fail-fast behaviour applies). +In other words, `toBeLessThan(10)` is still reported, even though `toBeGreaterThan(5)` already fails +in the above example. -```kotlin -expect(5).toBeGreaterThan(2).and.toBeLessThan(10) +
-expect(5) { - // ... -} and { // if the previous block fails, then this one is not evaluated - // ... -} -``` - +If you want to state expectations about multiple unrelated subjects and want to report them together (or introduce groups), +then you might be interested in using `expectGrouped` instead of `expect` -> take a look at the [data driven testing](#data-driven-testing) section. ## Expect an Exception @@ -1655,38 +1671,48 @@ In this sense it can be used for data driven testing. This is especially helpful in case your test runner does not support data driven testing (or other mechanisms like hierarchical or dynamic tests). As an example, Atrium can help you to write data driven tests in a common module of a multiplatform-project. -The trick is to wrap your expectations into an [expectation-group](#define-single-expectations-or-an-expectation-group), -use [Feature Extractors](#feature-extractors) and state expectations about those feautres. Following an example: +Use `expectGrouped` (a pre-defined expectation verb which ships along with `expect`) instead and then define multiple +`expect` in it. Following an example: ```kotlin fun myFun(i: Int) = (i + 97).toChar() -expect("calling myFun with...") { +expectGrouped { mapOf( 1 to 'a', 2 to 'c', 3 to 'e' ).forEach { (arg, result) -> - feature { f(::myFun, arg) }.toEqual(result) + group("calling myFun with $arg") { + expect(myFun(arg)).toEqual(result) + } } } ``` -↑ [Example](https://github.com/robstoll/atrium/tree/main/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt#L35)[Output](#ex-data-driven-1) +↑ [Example](https://github.com/robstoll/atrium/tree/main/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt#L32)[Output](#ex-data-driven-1) ```text -I expected subject: "calling myFun with..." <1234789> -◆ ▶ myFun(1): 'b' - ◾ to equal: 'a' -◆ ▶ myFun(3): 'd' - ◾ to equal: 'e' +my expectations: +# calling myFun with 1: + ◆ ▶ I expected subject: 'b' + ◾ to equal: 'a' +# calling myFun with 3: + ◆ ▶ I expected subject: 'd' + ◾ to equal: 'e' ``` Per default, only failing expectations are shown. This is also the reason why the call of `myFun(2)` is not listed (as the result is `c` as expected). +`expectGrouped` creates an ExpectGrouping-Block which is very similar to an expectation-group block +(see [Define an expectation-group](#define-single-expectations-or-an-expectation-group)) just that you have not yet +defined a subject. It also specifies that all expectations specified in it are evaluated and reported together +and this is also the reason why we see `calling myFun with 3` in the above [Output](#ex-data-driven-1) even though +calling it with `2` failed. + Please [create a feature request](https://github.com/robstoll/atrium/issues/new?template=feature_request.md&title=[Feature]) if you want to see a summary, meaning also successful expectations -- we happily add more functionality if it is of use for someone. @@ -1696,32 +1722,81 @@ We are going to reuse the `myFun` from above: ```kotlin -import ch.tutteli.atrium.logic.utils.expectLambda - -expect("calling myFun with ...") { - mapOf( - 1 to expectLambda { toBeLessThan('f') }, - 2 to expectLambda { toEqual('c') }, - 3 to expectLambda { toBeGreaterThan('e') } +expectGrouped { + mapOf>( + 1 to { toBeLessThan('f') }, + 2 to { toEqual('c') }, + 3 to { toBeGreaterThan('e') } ).forEach { (arg, assertionCreator) -> - feature({ f(::myFun, arg) }, assertionCreator) + group("calling myFun with $arg") { + expect(myFun(arg), assertionCreator) + } } } ``` -↑ [Example](https://github.com/robstoll/atrium/tree/main/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt#L49)[Output](#ex-data-driven-2) +↑ [Example](https://github.com/robstoll/atrium/tree/main/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt#L48)[Output](#ex-data-driven-2) ```text -I expected subject: "calling myFun with ..." <1234789> -◆ ▶ myFun(3): 'd' - ◾ to be greater than: 'e' +my expectations: +# calling myFun with 3: + ◆ ▶ I expected subject: 'd' + ◾ to be greater than: 'e' ``` The example should be self-explanatory. -One detail to note though is the usage of `expectLambda`. -It is a helper function which circumvents certain [Kotlin type inference bugs](https://github.com/robstoll/atrium/wiki/Kotlin-Bugs-and-missing-features) (upvote them please). -Writing the same as `mapOf.() -> Unit>( 1 to { ... } )` would not work as the type for a lambda -involved in a `Pair` is not (yet) inferred correctly by Kotlin. +One detail to note though is the usage of `ExpectationCreator`. +It's a `typealias` for `Expect.() -> Unit` and reduces some verbosity. Its usage is of course optional. +In case you should run into type inference issues, then prepend your lambda with `expectLambda` +(for instance `expectLambda { toBeLessThan('f') }`), it's a helper function which gives Kotlin an additional hint. + +So far we have not shown it but you can also nest groups and even use groups within `expect`. For instance: + + + +```kotlin +val x1 = 1 +val x2 = 3 +val y = 6 + +expectGrouped { + group("first group") { + expect(x1).toEqual(2) + group("sub-group") { + expect(x2).toBeGreaterThan(5) + } + } + group("second group") { + expect(y) { + group("sub-group 1") { + toBeGreaterThan(0) + toBeLessThan(5) + } + group("sub-group 2") { + notToEqual(6) + } + } + } +} +``` +↑ [Example](https://github.com/robstoll/atrium/tree/main/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt#L85)[Output](#ex-data-driven-nesting) + +```text +my expectations: +# first group: + ◆ ▶ I expected subject: 1 (kotlin.Int <1234789>) + ◾ to equal: 2 (kotlin.Int <1234789>) + # sub-group: + ◆ ▶ I expected subject: 3 (kotlin.Int <1234789>) + ◾ to be greater than: 5 (kotlin.Int <1234789>) +# second group: + ◆ ▶ I expected subject: 6 (kotlin.Int <1234789>) + # sub-group 1: + ◆ to be less than: 5 (kotlin.Int <1234789>) + # sub-group 2: + ◆ not to equal: 6 (kotlin.Int <1234789>) +``` + There is one last function worth mentioning here which comes in handy in data-driven testing in case the subject has a [nullable type]((https://kotlinlang.org/docs/reference/null-safety.html).) @@ -1736,29 +1811,33 @@ Following another fictional example which illustrates `toEqualNullIfNullGivenEls ```kotlin fun myNullableFun(i: Int) = if (i > 0) i.toString() else null -expect("calling myNullableFun with ...") { - mapOf( - Int.MIN_VALUE to expectLambda { toContain("min") }, +expectGrouped { + mapOf?>( + Int.MIN_VALUE to { toContain("min") }, -1 to null, 0 to null, - 1 to expectLambda { toEqual("1") }, - 2 to expectLambda { toEndWith("2") }, - Int.MAX_VALUE to expectLambda { toEqual("max") } + 1 to { toEqual("1") }, + 2 to { toEndWith("2") }, + Int.MAX_VALUE to { toEqual("max") } ).forEach { (arg, assertionCreatorOrNull) -> - feature { f(::myNullableFun, arg) }.toEqualNullIfNullGivenElse(assertionCreatorOrNull) + group("calling myFun with $arg") { + expect(myNullableFun(arg)).toEqualNullIfNullGivenElse(assertionCreatorOrNull) + } } } ``` -↑ [Example](https://github.com/robstoll/atrium/tree/main/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt#L67)[Output](#ex-data-driven-3) +↑ [Example](https://github.com/robstoll/atrium/tree/main/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt#L66)[Output](#ex-data-driven-3) ```text -I expected subject: "calling myNullableFun with ..." <1234789> -◆ ▶ myNullableFun(-2147483648): null - » to contain: - ⚬ value: "min" <1234789> - » but no match was found -◆ ▶ myNullableFun(2147483647): "2147483647" <1234789> - ◾ to equal: "max" <1234789> +my expectations: +# calling myFun with -2147483648: + ◆ ▶ I expected subject: null + » to contain: + ⚬ value: "min" <1234789> + » but no match was found +# calling myFun with 2147483647: + ◆ ▶ I expected subject: "2147483647" <1234789> + ◾ to equal: "max" <1234789> ``` @@ -1932,7 +2011,7 @@ expect(listOf(1)).get(0) {} I expected subject: [1] (java.util.Collections.SingletonList <1234789>) ◆ ▶ get(0): 1 (kotlin.Int <1234789>) ◾ at least one expectation defined: false - » You forgot to define expectations in the expectationCreator-lambda + » You forgot to define expectations in the assertionCreator-lambda » Sometimes you can use an alternative to `{ }` For instance, instead of `toThrow<..> { }` you should use `toThrow<..>()` ``` @@ -2326,13 +2405,14 @@ In the meantime we might help you via slack, please post your questions in the [ # Use own Expectation Verb -Atrium offers the expectation verb `expect` out of the box. +Atrium offers the expectation verbs `expect` and `expectGrouped` out of the box. -You can also define your own expectation verb if `expect` does not suite you or in case you want to change some default implementation. +You can also define your own expectation verb if the pre-defined verbs do not suite you or +in case you want to change some default implementation. In order to create an own expectation verb it is sufficient to: 1. Copy the file content of [atriumVerbs.kt](https://github.com/robstoll/atrium/tree/main/misc/atrium-verbs-internal/src/commonMain/kotlin/ch.tutteli.atrium.api.verbs.internal/atriumVerbs.kt) 2. Create your own atriumVerbs.kt and paste the previously copied content - 3. Adjust package name and `import`s and rename `expect` as desired (you can also leave it that way of course). + 3. Adjust package name and `import`s and rename `expect`/`expectGrouped` as desired (you can also leave it that way of course). 4. exclude `atrium-verbs` from your dependencies. Taking the setup shown in the [Installation](#installation) section for the JVM platform, you would replace the `dependencies` block as follows: ```kotlin @@ -2346,14 +2426,15 @@ In order to create an own expectation verb it is sufficient to: What are the benefits of creating an own expectation verb: - you can encapsulate the reporting style.
This is especially useful if you have multiple projects and want to have a consistent reporting style. - For instance, you could change from same-line to multi-line reporting or report not only failing but also successful expectations, change the output language etc. + For instance, you could change from same-line to multi-line reporting or report not only failing but also successful expectations etc.
💬 where should I put the atriumVerbs.kt? We suggest you create an adapter project for Atrium where you specify the expectation verb. - And most likely you will accumulate them with expectation functions which are so common - that they appear in multiple projects -- please share them with us (get in touch with us via issue or slack) if they are not of an internal nature 😉 + And most likely you will accumulate them with expectation functions which are so common, + that they appear in multiple of your projects -- please share them with us + (get in touch with us via issue/discussion/slack if you need help) if they are not of an internal nature 😉
diff --git a/apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/fun0Expectations.kt b/apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/fun0Expectations.kt index 63b7db13a6..e726a03f0e 100644 --- a/apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/fun0Expectations.kt +++ b/apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/fun0Expectations.kt @@ -8,7 +8,8 @@ import ch.tutteli.atrium.logic.toThrow import kotlin.reflect.KClass /** - * Expects that the thrown [Throwable] *is a* [TExpected] (the same type or a sub-type). + * Expects that invoking the subject (a function with arity 0, i.e. without arguments) throws a [TExpected] + * (the same type or a sub-type). * * Notice, that asserting a generic type is [flawed](https://youtrack.jetbrains.com/issue/KT-27826). * For instance `toThrow>` would only check if the subject is a `MyException` without checking if @@ -27,8 +28,8 @@ internal fun Expect Any?>.toThrow( ): SubjectChangerBuilder.ExecutionStep<*, TExpected> = _logic.toThrow(kClass) /** - * Expects that the thrown [Throwable] *is a* [TExpected] (the same type or a sub-type) and - * that it holds all assertions the given [assertionCreator] creates. + * Expects that invoking the subject (a function with arity 0, i.e. without arguments) throws a [TExpected] + * (the same type or a sub-type) and that it holds all assertions the given [assertionCreator] creates. * * Notice, in contrast to other assertion functions which expect an [assertionCreator], this function returns not * [Expect] of the initial type, which was `Throwable?` but an [Expect] of the specified type [TExpected]. @@ -66,7 +67,7 @@ inline fun Expect Any?>.toThrow( /** - * Expects that no [Throwable] is thrown at all when calling the subject (a lambda with arity 0, i.e. without arguments) + * Expects that no [Throwable] is thrown at all when invoking the subject (a function with arity 0, i.e. without arguments) * and changes the subject of `this` expectation to the return value of type [R]. * * @return An [Expect] with the new type [R]. @@ -77,7 +78,7 @@ fun R> Expect.notToThrow(): Expect = _logic.notToThrow().transform() /** - * Expects that no [Throwable] is thrown at all when calling the subject (a lambda with arity 0, i.e. without arguments) + * Expects that no [Throwable] is thrown at all when invoking the subject (a function with arity 0, i.e. without arguments) * and that the corresponding return value holds all assertions the given [assertionCreator] creates. * * @return An [Expect] with the new type [R]. diff --git a/apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/grouping.kt b/apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/grouping.kt new file mode 100644 index 0000000000..b4fbb1f21f --- /dev/null +++ b/apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/fluent/en_GB/grouping.kt @@ -0,0 +1,51 @@ +package ch.tutteli.atrium.api.fluent.en_GB + +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping +import ch.tutteli.atrium.logic.* +import ch.tutteli.atrium.reporting.Text + +/** + * Creates and appends a group based on the given [description] (optionally [representationProvider]) + * and [groupingActions] and returns an [ExpectGrouping]. + * + * @param description The description of the group. + * @param representationProvider Optionally, can be specified if an additional representation shall be reported + * (default is [Text.EMPTY_PROVIDER]) + * @param groupingActions Some action which defines what happens within the group (typically, creating some + * expectations via an expectation-verb such as `expect` or nesting the grouping further). + * + * @return An [ExpectGrouping], allowing to define further subgroups or expectations. + * + * @sample ch.tutteli.atrium.api.fluent.en_GB.samples.GroupingSamples.group + * + * @since 1.1.0 + */ +fun ExpectGrouping.group( + description: String, + representationProvider: () -> Any? = Text.EMPTY_PROVIDER, + groupingActions: ExpectGrouping.() -> Unit +): ExpectGrouping = + _logicAppend { this.grouping(description, representationProvider, groupingActions) } + +/** + * Creates and appends a group based on the given [description] (optionally [representationProvider]) + * and [assertionCreator] and returns an [Expect]. + * + * @param description The description of the group. + * @param representationProvider Optionally, can be specified if an additional representation shall be reported + * (default is [Text.EMPTY_PROVIDER]) + * @param assertionCreator a provider which states the expectations for the current subject belonging to this + * newly created group. + * + * @return an [Expect] for the subject of `this` expectation. + * + * @sample ch.tutteli.atrium.api.fluent.en_GB.samples.GroupingSamples.group + * + * @since 1.1.0 + */ +fun Expect.group( + description: String, + representationProvider: () -> Any? = Text.EMPTY_PROVIDER, + assertionCreator: Expect.() -> Unit +): Expect = _logicAppend { this.group(description, representationProvider, assertionCreator) } diff --git a/apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/GroupingTest.kt b/apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/GroupingTest.kt new file mode 100644 index 0000000000..c9f0c32495 --- /dev/null +++ b/apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/GroupingTest.kt @@ -0,0 +1,8 @@ +package ch.tutteli.atrium.api.fluent.en_GB + +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.specs.fun3 + +class GroupingTest : ch.tutteli.atrium.specs.integration.GroupingTest( + fun3(Expect::group) +) diff --git a/apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/GroupingSamples.kt b/apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/GroupingSamples.kt new file mode 100644 index 0000000000..fa839977ed --- /dev/null +++ b/apis/fluent/atrium-api-fluent/src/commonTest/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/GroupingSamples.kt @@ -0,0 +1,79 @@ +package ch.tutteli.atrium.api.fluent.en_GB.samples + +import ch.tutteli.atrium.api.fluent.en_GB.group +import ch.tutteli.atrium.api.fluent.en_GB.toBeGreaterThan +import ch.tutteli.atrium.api.fluent.en_GB.toBeLessThan +import ch.tutteli.atrium.api.fluent.en_GB.toEqual +import ch.tutteli.atrium.api.verbs.expect +import ch.tutteli.atrium.api.verbs.expectGrouped +import kotlin.test.Test + +class GroupingSamples { + + @Test + fun group() { + fails { + + // You can use `expectGrouped` instead of `expect` in case you have multiple + // unrelated subjects but want to evaluate/report them together. + // Use independent `expect` if you want fail fast behaviour instead. + expectGrouped { + + // you can state multiple `expect` within this ExpectGrouping-block where all `expect` and `group` + // inside are evaluated together; similar to an expectation-group block. + + val someProperty = 1 + expect(someProperty) + .toBeGreaterThan(5) // fails + .toBeLessThan(0) // still evaluated since we are within an ExpectGrouping-block + // in contrast to a standalone expect where toBeGreaterThan would have + // failed fast + + // ... however, this `expect` is still evaluated even though the first `expect` failed + expect(1) { + // now we are within an expectation-group block. In contrast to an ExpectGrouping-block, + // we already defined a subject (2) for which we want to state multiple expectations. + // Likewise, they are evaluated together: + + toBeGreaterThan(5) // fails + toBeLessThan(0) // still evaluated, fails as well + } + + // this group is still evaluated even though the first two `expect` failed + group("verifying basic properties") { + // also all expect within a group are evaluated together + + expect(1).toEqual(2) + // imagine multiple expect within this group, + // they are all evaluated even though the first in this group already failed + + // you can nest groups as often as you like + group("sub-group") { + //... + } + } + + // another group which is still evaluated despite all the failures above + group("verifying edge cases") { + //... + } + + expect(2) + // you can also use group to structure reporting + .group("first group") { + toBeGreaterThan(5) // fails + toBeLessThan(20) // still evaluated, holds ... + } // ... the group as such failed though and since we have not used an + // expectation-group block for this expect, the following group is not evaluated: + .group("verifying failing cases") { + //... + } + } + + // you can optionally change the default top-level group description + expectGrouped("Verifying privileged actions") { + //... + } + } + } +} diff --git a/apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/fun0Expectations.kt b/apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/fun0Expectations.kt index 45447e6f21..7f1f42bb27 100644 --- a/apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/fun0Expectations.kt +++ b/apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/fun0Expectations.kt @@ -8,7 +8,8 @@ import ch.tutteli.atrium.logic.toThrow import kotlin.reflect.KClass /** - * Expects that the thrown [Throwable] *is a* [TExpected] (the same type or a sub-type). + * Expects that invoking the subject (a function with arity 0, i.e. without arguments) throws a [TExpected] + * (the same type or a sub-type). * * Notice, that asserting a generic type is [flawed](https://youtrack.jetbrains.com/issue/KT-27826). * For instance `toThrow>` would only check if the subject is a `MyException` without checking if @@ -25,8 +26,8 @@ internal fun Expect Any?>.toThrow( ): SubjectChangerBuilder.ExecutionStep<*, TExpected> = _logic.toThrow(kClass) /** - * Expects that the thrown [Throwable] *is a* [TExpected] (the same type or a sub-type) and - * that it holds all assertions the given [assertionCreator] creates. + * Expects that invoking the subject (a function with arity 0, i.e. without arguments) throws a [TExpected] + * (the same type or a sub-type) and that it holds all assertions the given [assertionCreator] creates. * * Notice, in contrast to other assertion functions which expect an [assertionCreator], this function returns not * [Expect] of the initial type, which was `Throwable?` but an [Expect] of the specified type [TExpected]. diff --git a/apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/grouping.kt b/apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/grouping.kt new file mode 100644 index 0000000000..e39905f518 --- /dev/null +++ b/apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/infix/en_GB/grouping.kt @@ -0,0 +1,52 @@ +package ch.tutteli.atrium.api.infix.en_GB + +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping +import ch.tutteli.atrium.logic._logicAppend +import ch.tutteli.atrium.logic.group +import ch.tutteli.atrium.logic.grouping +import ch.tutteli.atrium.reporting.Text + +/** + * Creates and appends a group based on the given [description] (optionally [representationProvider]) + * and [groupingActions] and returns an [ExpectGrouping]. + * + * @param description The description of the group. + * @param representationProvider Optionally, can be specified if an additional representation shall be reported + * (default is [Text.EMPTY_PROVIDER]) + * @param groupingActions Some action which defines what happens within the group (typically, creating some + * expectations via an expectation-verb such as `expect` or nesting the grouping further). + * + * @return An [ExpectGrouping], allowing to define further subgroups or expectations. + * + * @sample ch.tutteli.atrium.api.infix.en_GB.samples.GroupingSamples.group + * + * @since 1.1.0 + */ +fun ExpectGrouping.group( + description: String, + representationProvider: () -> Any? = Text.EMPTY_PROVIDER, + groupingActions: ExpectGrouping.() -> Unit +): ExpectGrouping = _logicAppend { grouping(description, representationProvider, groupingActions) } + +/** + * Creates and appends a group based on the given [description] (optionally [representationProvider]) + * and [assertionCreator] and returns an [Expect]. + * + * @param description The description of the group. + * @param representationProvider Optionally, can be specified if an additional representation shall be reported + * (default is [Text.EMPTY_PROVIDER]) + * @param assertionCreator a provider which states the expectations for the current subject belonging to this + * newly created group. + * + * @return an [Expect] for the subject of `this` expectation. + * + * @sample ch.tutteli.atrium.api.infix.en_GB.samples.GroupingSamples.group + * + * @since 1.1.0 + */ +fun Expect.group( + description: String, + representationProvider: () -> Any? = Text.EMPTY_PROVIDER, + assertionCreator: Expect.() -> Unit +): Expect = _logicAppend { group(description, representationProvider, assertionCreator) } diff --git a/apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/GroupingTest.kt b/apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/GroupingTest.kt new file mode 100644 index 0000000000..cd501258b8 --- /dev/null +++ b/apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/GroupingTest.kt @@ -0,0 +1,8 @@ +package ch.tutteli.atrium.api.infix.en_GB + +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.specs.fun3 + +class GroupingTest : ch.tutteli.atrium.specs.integration.GroupingTest( + fun3(Expect::group) +) diff --git a/apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/GroupingSamples.kt b/apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/GroupingSamples.kt new file mode 100644 index 0000000000..d424941053 --- /dev/null +++ b/apis/infix/atrium-api-infix/src/commonTest/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/GroupingSamples.kt @@ -0,0 +1,76 @@ +package ch.tutteli.atrium.api.infix.en_GB.samples + +import ch.tutteli.atrium.api.infix.en_GB.* +import ch.tutteli.atrium.api.verbs.expect +import ch.tutteli.atrium.api.verbs.expectGrouped +import kotlin.test.Test + +class GroupingSamples { + + @Test + fun group() { + fails { + + // You can use `expectGrouped` instead of `expect` in case you have multiple + // unrelated subjects but want to evaluate/report them together. + // Use independent `expect` if you want fail fast behaviour instead. + expectGrouped { + + // you can state multiple `expect` within this ExpectGrouping-block where all `expect` and `group` + // inside are evaluated together; similar to an expectation-group block. + + val someProperty = 1 + expect(someProperty) toBeGreaterThan 5 toBeLessThan 0 + // | fails | + // | still evaluated since we are within an ExpectGrouping-block + // in contrast to a standalone expect where toBeGreaterThan + // would have failed fast + + // ... however, this `expect` is still evaluated even though the first `expect` failed + expect(1) { + // now we are within an expectation-group block. In contrast to an ExpectGrouping-block, + // we already defined a subject (2) for which we want to state multiple expectations. + // Likewise, they are evaluated together: + + it toBeGreaterThan 5 // fails + it toBeLessThan 0 // still evaluated, fails as well + } + + // this group is still evaluated even though the first two `expect` failed + group("verifying basic properties") { + // also all expect within a group are evaluated together + + expect(1) toEqual 2 + // imagine multiple expect within this group, + // they are all evaluated even though the first in this group already failed + + // you can nest groups as often as you like + group("sub-group") { + //... + } + } + + // another group which is still evaluated despite all the failures above + group("verifying edge cases") { + //... + } + + expect(2) + // you can also use group to structure reporting + .group("first group") { + it toBeGreaterThan 5 // fails + it toBeLessThan 20 // still evaluated, holds ... + } // ... the group as such failed though and since we have not used an + // expectation-group block for this expect, the following group is not evaluated: + .group("verifying failing cases") { + //... + } + } + + // you can optionally change the default top-level group description + expectGrouped("Verifying privileged actions") { + //... + } + } + } +} diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/GroupingAssertionGroupType.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/GroupingAssertionGroupType.kt new file mode 100644 index 0000000000..06e0b0097a --- /dev/null +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/GroupingAssertionGroupType.kt @@ -0,0 +1,19 @@ +package ch.tutteli.atrium.assertions + +import ch.tutteli.atrium.reporting.Reporter + +/** + * Represents the [AssertionGroupType] for [AssertionGroup]s whose [assertions][AssertionGroup.assertions] + * all be reported in reporting (no filtering by a [Reporter]) since it represents a group of assertions made + * for (most likely) unrelated subjects. + * + * @since 1.1.0 + */ +interface GroupingAssertionGroupType : AssertionGroupType + +/** + * The [AssertionGroupType] for [AssertionGroup]s which contain assertions which shall be grouped. + * + * @since 1.1.0 + */ +object DefaultGroupingAssertionGroupType : GroupingAssertionGroupType diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/builders/explanatoryGroup.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/builders/explanatoryGroup.kt index 93860795f7..82bb9b6d1c 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/builders/explanatoryGroup.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/assertions/builders/explanatoryGroup.kt @@ -27,7 +27,7 @@ interface ExplanatoryGroup { val withWarningType: AssertionsOption /** - * Builder to create an [AssertionGroup] with a [WarningAssertionGroupType]. + * Builder to create an [AssertionGroup] with a [HintAssertionGroupType]. * @since 1.0.0 */ val withHintType: AssertionsOption diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/ErrorMessages.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/ErrorMessages.kt index eb551bbc46..0f850145d1 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/ErrorMessages.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/ErrorMessages.kt @@ -11,7 +11,7 @@ enum class ErrorMessages(override val value: String) : StringBasedTranslatable { AT_LEAST_ONE_EXPECTATION_DEFINED("at least one expectation defined"), /** @since 0.18.0 */ - FORGOT_DO_DEFINE_EXPECTATION("You forgot to define expectations in the expectationCreator-lambda"), + FORGOT_DO_DEFINE_EXPECTATION("You forgot to define expectations in the assertionCreator-lambda"), /** @since 0.18.0 */ HINT_AT_LEAST_ONE_EXPECTATION_DEFINED("Sometimes you can use an alternative to `{ }` For instance, instead of `toThrow<..> { }` you should use `toThrow<..>()`"), diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/Expect.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/Expect.kt index 4cdcd9c0a1..c7f14a1656 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/Expect.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/Expect.kt @@ -3,20 +3,34 @@ package ch.tutteli.atrium.creating import ch.tutteli.atrium.assertions.Assertion /** - * Sole purpose of this interface is to hide [AssertionContainer] from newcomers which usually don't have to deal with - * this. + * The internal type which we work with which combines [Expect], [AssertionContainer] and [ExpectGrouping]. * - * Moreover, we separate Expect from AssertionContainer so that we can provide extension functions for - * AssertionContainer which are more or less identical to the ones defined for api-fluent but don't return Expect but - * [Assertion] etc. + * In theory, [Expect] should extend [ExpectGrouping] but due to Kotlin type inference/overload resolution bugs, we have + * to split it. One benefit of it, we can define extensions for [ExpectGrouping] which are not visible for [Expect]. * - * See https://github.com/robstoll/atrium-roadmap/wiki/Requirements#personas for more information about the personas. + * Similarly, we separate [Expect] from [AssertionContainer] so that we can provide extension functions for + * [AssertionContainer] which are more or less identical to the ones defined for api-fluent but don't return an [Expect] + * but [Assertion] etc. + * + * Also, we separate [Expect] form [AssertionContainer] since a lot of functionality defined for AssertionContainer is + * not relevant for newcomers to Atrium (see [https://github.com/robstoll/atrium-roadmap/wiki/Requirements#personas](https://github.com/robstoll/atrium-roadmap/wiki/Requirements#personas) + * for more information about the personas). */ -interface ExpectInternal : Expect, AssertionContainer +interface ExpectInternal : Expect, AssertionContainer, ExpectGrouping /** - * Represents the extension point for [Assertion] functions and sophisticated builders for subjects of type [T]. + * Represents the extension point for expectation functions and sophisticated builders for subjects of type [T]. * * @param T The type of the subject of `this` expectation. */ interface Expect +typealias ExpectationCreator = Expect.() -> Unit + +/** + * Represents a group of expectations including nested groups of expectations (nested [ExpectGrouping]). + * + * It's the extension point for groups of expectations with unrelated subjects. + * + * @since 1.1.0 + */ +interface ExpectGrouping diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/BaseExpectImpl.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/BaseExpectImpl.kt index 29e184aa79..ed2c3599aa 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/BaseExpectImpl.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/BaseExpectImpl.kt @@ -63,7 +63,7 @@ abstract class BaseExpectImpl( representationInsteadOfFeature?.let { provider -> maybeSubject.fold({ null }) { provider(it) } } ?: maybeSubject.getOrElse { - // a RootExpect without a defined subject is almost certain a bug + // a RootExpect without a defined subject is almost certainly a bug Text(SHOULD_NOT_BE_SHOWN_TO_THE_USER_BUG) } } diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/ComponentFactoryContainerImpl.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/ComponentFactoryContainerImpl.kt index f7f6a80d73..5d3c6cb07c 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/ComponentFactoryContainerImpl.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/impl/ComponentFactoryContainerImpl.kt @@ -191,6 +191,13 @@ internal object DefaultComponentFactoryContainer : ComponentFactoryContainer by TextSummaryAssertionGroupFormatter(bulletPoints, controller, textAssertionPairFormatter) } }, + { c -> + val bulletPoints = c.build().getBulletPoints() + val textAssertionPairFormatter = c.build() + TextAssertionFormatterFactory { controller -> + TextGroupingAssertionGroupFormatter(bulletPoints, controller, textAssertionPairFormatter) + } + }, { c -> val objectFormatter = c.build() val bulletPoints = c.build().getBulletPoints() diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/AssertionFormatterParameterObject.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/AssertionFormatterParameterObject.kt index b02ddf6639..2f01bdb9d7 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/AssertionFormatterParameterObject.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/AssertionFormatterParameterObject.kt @@ -93,7 +93,7 @@ class AssertionFormatterParameterObject private constructor( * * @return The newly created [AssertionFormatterParameterObject]. */ - fun createForExplanatoryFilterAssertionGroup(newPrefix : String = prefix): AssertionFormatterParameterObject = + fun createForExplanatoryFilterAssertionGroup(newPrefix: String = prefix): AssertionFormatterParameterObject = AssertionFormatterParameterObject( sb, newPrefix, @@ -103,6 +103,22 @@ class AssertionFormatterParameterObject private constructor( numberOfExplanatoryGroups + 1 ) + /** + * Clones the current [AssertionFormatterParameterObject] but uses the given [newPrefix]. + * + * @return The newly created [AssertionFormatterParameterObject]. + * + * @since 1.1.0 + */ + fun createWithNewPrefix(newPrefix: String): AssertionFormatterParameterObject = + AssertionFormatterParameterObject( + sb, + newPrefix, + indentLevel, + assertionFilter, + numberOfDoNotFilterGroups, + numberOfExplanatoryGroups + ) /** * Indicates that the formatting process is currently not formatting the [Assertion]s (or any nested assertion) diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/Text.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/Text.kt index 048832d815..baf1e2c0ef 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/Text.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/Text.kt @@ -32,5 +32,11 @@ data class Text private constructor(val string: String) { * An empty string as [Text] */ val EMPTY = Text("") + + /** + * A provider which returns [EMPTY]. + * @since 1.1.0 + */ + val EMPTY_PROVIDER = { EMPTY } } } diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/NoSpecialChildFormattingSingleAssertionGroupTypeFormatter.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/NoSpecialChildFormattingSingleAssertionGroupTypeFormatter.kt index eceac60bdb..f67d006fe4 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/NoSpecialChildFormattingSingleAssertionGroupTypeFormatter.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/NoSpecialChildFormattingSingleAssertionGroupTypeFormatter.kt @@ -13,13 +13,13 @@ import kotlin.reflect.KClass * [AssertionGroup]s of one specific [AssertionGroupType] and does nothing special when it comes to formatting * [AssertionGroup.assertions] (merely delegates to [assertionFormatterController]). * - * @param T The [AssertionGroupType] which the concrete sub class [canFormat][AssertionFormatter.canFormat]. + * @param T The [AssertionGroupType] which the concrete subclass [canFormat][AssertionFormatter.canFormat]. * - * @property clazz The [AssertionGroupType] which the concrete sub class [canFormat][AssertionFormatter.canFormat]. + * @property clazz The [AssertionGroupType] which the concrete subclass [canFormat][AssertionFormatter.canFormat]. * * @constructor A base type for [AssertionFormatter] which [canFormat][AssertionFormatter.canFormat] only * [AssertionGroup]s of one specific [AssertionGroupType]. - * @param clazz The [AssertionGroupType] which the concrete sub class [canFormat][AssertionFormatter.canFormat]. + * @param clazz The [AssertionGroupType] which the concrete subclass [canFormat][AssertionFormatter.canFormat]. * @param assertionFormatterController The controller to which this formatter gives back the control * when it comes to format children of an [AssertionGroup]. */ @@ -28,7 +28,7 @@ abstract class NoSpecialChildFormattingSingleAssertionGroupTypeFormatter(clazz) { - override fun formatGroupAssertions( + final override fun formatGroupAssertions( formatAssertions: (AssertionFormatterParameterObject, (Assertion) -> Unit) -> Unit, childParameterObject: AssertionFormatterParameterObject ) { diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextGroupingAssertionGroupFormatter.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextGroupingAssertionGroupFormatter.kt new file mode 100644 index 0000000000..4fef3e2c22 --- /dev/null +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextGroupingAssertionGroupFormatter.kt @@ -0,0 +1,52 @@ +package ch.tutteli.atrium.reporting.text.impl + +import ch.tutteli.atrium.assertions.* +import ch.tutteli.atrium.reporting.AssertionFormatter +import ch.tutteli.atrium.reporting.AssertionFormatterController +import ch.tutteli.atrium.reporting.AssertionFormatterParameterObject +import ch.tutteli.atrium.reporting.AssertionPairFormatter +import ch.tutteli.atrium.reporting.text.TextAssertionFormatter +import kotlin.reflect.KClass + +/** + * Represents an [AssertionFormatter] which formats [AssertionGroup]s with a [GroupingAssertionGroupType] by + * using the given [assertionPairFormatter] to format the group header and uses the bullet point defined for + * [GroupingAssertionGroupType] as prefix for the group as such and the bullet point defined for + * [RootAssertionGroupType] for [AssertionGroup.assertions]. + * + * Its usage is intended for text output (e.g. to the console). + * + * @constructor Represents an [AssertionFormatter] which formats [AssertionGroup]s with a [GroupingAssertionGroupType] + * by putting each assertion on an own line prefixed with a bullet point. + * @param bulletPoints The formatter uses the bullet point defined for [GroupingAssertionGroupType] + * (`"# "` if absent) as prefix of the group and [RootAssertionGroupType] (`◆ ` if absent) + * of the child-[AssertionFormatterParameterObject]. + * @param assertionFormatterController The controller to which this formatter gives back the control + * when it comes to format children of an [AssertionGroup]. + * @param assertionPairFormatter The formatter which is used to format assertion pairs. + * + * @since 1.1.0 + */ +class TextGroupingAssertionGroupFormatter( + bulletPoints: Map, String>, + private val assertionFormatterController: AssertionFormatterController, + private val assertionPairFormatter: AssertionPairFormatter +) : NoSpecialChildFormattingSingleAssertionGroupTypeFormatter( + GroupingAssertionGroupType::class, + assertionFormatterController +), + TextAssertionFormatter { + private val groupPrefix = (bulletPoints[GroupingAssertionGroupType::class] ?: "# ") + private val rootPrefix = bulletPoints[RootAssertionGroupType::class] ?: "◆ " + private val formatter = TextPrefixBasedAssertionGroupFormatter(rootPrefix) + + override fun formatGroupHeaderAndGetChildParameterObject( + assertionGroup: AssertionGroup, + parameterObject: AssertionFormatterParameterObject + ): AssertionFormatterParameterObject = + formatter.formatWithGroupName( + assertionPairFormatter, + assertionGroup, + parameterObject.createWithNewPrefix(groupPrefix) + ) +} diff --git a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextListBasedAssertionGroupFormatter.kt b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextListBasedAssertionGroupFormatter.kt index c53d9dab51..44c6977a87 100644 --- a/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextListBasedAssertionGroupFormatter.kt +++ b/atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/reporting/text/impl/TextListBasedAssertionGroupFormatter.kt @@ -19,7 +19,7 @@ import kotlin.reflect.KClass * @param assertionFormatterController The controller to which this formatter gives back the control * when it comes to format children of an [AssertionGroup]. * @param assertionPairFormatter The formatter which is used to format assertion pairs. - * @param clazz The [AssertionGroupType] which the concrete sub class [canFormat][AssertionFormatter.canFormat]. + * @param clazz The [AssertionGroupType] which the concrete subclass [canFormat][AssertionFormatter.canFormat]. */ abstract class TextListBasedAssertionGroupFormatter( bulletPoint: String, diff --git a/atrium-core/src/commonTest/kotlin/ch/tutteli/atrium/reporting/text/BulletPointProviderSpec.kt b/atrium-core/src/commonTest/kotlin/ch/tutteli/atrium/reporting/text/BulletPointProviderSpec.kt index bb06a82416..fecb2b332c 100644 --- a/atrium-core/src/commonTest/kotlin/ch/tutteli/atrium/reporting/text/BulletPointProviderSpec.kt +++ b/atrium-core/src/commonTest/kotlin/ch/tutteli/atrium/reporting/text/BulletPointProviderSpec.kt @@ -44,7 +44,7 @@ class BulletPointProviderSpec : Spek({ FeatureAssertionGroupType::class to (">> " to { p -> expectWitNewBulletPoint(p, "a") feature { f("m", it.length) } toEqual 2 }), - PrefixFeatureAssertionGroupHeader::class to ("=> " to { p -> + PrefixFeatureAssertionGroupHeader::class to ("=> " to { p -> expectWitNewBulletPoint(p, "a") feature { f("m", it.length) } toEqual 2 }), PrefixSuccessfulSummaryAssertion::class to ("(/) " to { p -> @@ -79,13 +79,29 @@ class BulletPointProviderSpec : Spek({ expectWitNewBulletPoint(p, "a")._logic.appendAsGroup { _logic.append( assertionBuilder.explanatoryGroup - .withInformationType(false) + .withInformationType(withIndent = false) .withAssertion(_logic.toBe("b")) .failing .build() ) } - }) + }), + HintAssertionGroupType::class to ("(h) " to { p -> + expectWitNewBulletPoint(p, "a")._logic.appendAsGroup { + _logic.append( + assertionBuilder.explanatoryGroup + .withHintType + .withAssertion(_logic.toBe("b")) + .failing + .build() + ) + } + }), + GroupingAssertionGroupType::class to ("== " to { p -> + expectWitNewBulletPoint(p, listOf(1)).group("a group") { + toContain(2) + } + }), ) defaultBulletPoints.map { (kClass, defaultBulletPoint) -> diff --git a/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/GroupingAssertions.kt b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/GroupingAssertions.kt new file mode 100644 index 0000000000..8d64dcd901 --- /dev/null +++ b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/GroupingAssertions.kt @@ -0,0 +1,25 @@ +@file:Suppress("ObjectPropertyName", "FunctionName") + +package ch.tutteli.atrium.logic + +import ch.tutteli.atrium.assertions.Assertion +import ch.tutteli.atrium.creating.AssertionContainer +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping + +/** + * @since 1.1.0 + */ +interface GroupingAssertions { + + fun grouping( + container: AssertionContainer, description: String, representationProvider: () -> Any?, + groupingActions: ExpectGrouping.() -> Unit + ): Assertion + + fun group( + container: AssertionContainer, description: String, representationProvider: () -> Any?, + assertionCreator: Expect.() -> Unit + ): Assertion + +} diff --git a/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/impl/DefaultGroupingAssertions.kt b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/impl/DefaultGroupingAssertions.kt new file mode 100644 index 0000000000..cbb9b95b43 --- /dev/null +++ b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/impl/DefaultGroupingAssertions.kt @@ -0,0 +1,36 @@ +package ch.tutteli.atrium.logic.impl + +import ch.tutteli.atrium.assertions.Assertion +import ch.tutteli.atrium.assertions.DefaultGroupingAssertionGroupType +import ch.tutteli.atrium.assertions.builders.assertionBuilder +import ch.tutteli.atrium.creating.AssertionContainer +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping +import ch.tutteli.atrium.logic.GroupingAssertions +import ch.tutteli.atrium.logic.collectForComposition +import ch.tutteli.atrium.logic.toAssertionCreator + +class DefaultGroupingAssertions : GroupingAssertions { + + override fun grouping( + container: AssertionContainer, + description: String, + representationProvider: () -> Any?, + groupingActions: ExpectGrouping.() -> Unit + ): Assertion = group(container, description, representationProvider, groupingActions.toAssertionCreator()) + + override fun group( + container: AssertionContainer, + description: String, + representationProvider: () -> Any?, + assertionCreator: Expect.() -> Unit + ): Assertion { + val assertions = container.collectForComposition(assertionCreator) + return assertionBuilder + .customType(DefaultGroupingAssertionGroupType) + .withDescriptionAndRepresentation(description, representationProvider) + .withAssertions(assertions) + .build() + } + +} diff --git a/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/logic.kt b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/logic.kt index 6fa58a4d37..586d28b776 100644 --- a/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/logic.kt +++ b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/logic.kt @@ -5,6 +5,9 @@ package ch.tutteli.atrium.logic import ch.tutteli.atrium.assertions.Assertion import ch.tutteli.atrium.creating.AssertionContainer import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping +import ch.tutteli.atrium.creating.ExpectInternal +import ch.tutteli.atrium.reporting.BUG_REPORT_URL /** * Appends the [Assertion] the given [assertionCreator] creates based on this [Expect]. @@ -23,3 +26,23 @@ inline fun Expect._logicAppend(assertionCreator: AssertionContainer.() inline val Expect._logic: AssertionContainer get() = this.toAssertionContainer() +/** + * Appends the [Assertion] the given [assertionCreator] creates based on this [ExpectGrouping]. + * + * @since 1.1.0 + */ +//TODO deprecate with 1.3.0 and move toProofContainer to core +inline fun ExpectGrouping._logicAppend(assertionCreator: AssertionContainer<*>.() -> Assertion): ExpectGrouping = + _logic.run { append(assertionCreator()) }.toExpectGrouping() + +/** + * Turns this [ExpectGrouping] into an [AssertionContainer] without known subject type. + * + * @since 1.1.0 + */ +//TODO deprecate with 1.3.0 and move toProofContainer to core +inline val ExpectGrouping._logic: AssertionContainer<*> + get() = when (this) { + is ExpectInternal<*> -> this + else -> throw UnsupportedOperationException("Unsupported Expect: $this -- please open an issue that a hook shall be implemented: $BUG_REPORT_URL?template=feature_request&title=Hook%20for%20ExpectGrouping.toAssertionContainer") + } diff --git a/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/utils.kt b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/utils.kt index 97486efe38..7c51dd77df 100644 --- a/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/utils.kt +++ b/logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/utils.kt @@ -1,5 +1,3 @@ -@file:Suppress("NOTHING_TO_INLINE") - package ch.tutteli.atrium.logic import ch.tutteli.atrium.assertions.Assertion @@ -7,6 +5,7 @@ import ch.tutteli.atrium.assertions.DescriptiveAssertion import ch.tutteli.atrium.assertions.builders.assertionBuilder import ch.tutteli.atrium.creating.AssertionContainer import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping import ch.tutteli.atrium.creating.ExpectInternal import ch.tutteli.atrium.logic.creating.transformers.FeatureExtractorBuilder import ch.tutteli.atrium.logic.creating.transformers.SubjectChangerBuilder @@ -71,3 +70,15 @@ fun AssertionContainer.toExpect(): Expect = else -> throw UnsupportedOperationException("Unsupported AssertionContainer: $this -- Please open an issue that a hook shall be implemented: $BUG_REPORT_URL?template=feature_request&title=Hook%20for%20AssertionContainer.toExpect") } +/** + * Casts this [Expect] back to an [ExpectGrouping] so that you can use it in places where an [ExpectGrouping] is used. + */ +//TODO deprecate with 1.3.0 and move ProofContainer.toExpect to core +fun Expect.toExpectGrouping(): ExpectGrouping = + when (this) { + is ExpectInternal -> this + else -> throw UnsupportedOperationException("Unsupported AssertionContainer: $this -- Please open an issue that a hook shall be implemented: $BUG_REPORT_URL?template=feature_request&title=Hook%20for%Expect.toExpectGrouping") + } + +@Suppress("UNCHECKED_CAST") // safe to cast as long as Expect is the only subtype of ExpectGrouping +fun (ExpectGrouping.() -> Unit).toAssertionCreator(): Expect<*>.() -> Unit = this as Expect<*>.() -> Unit diff --git a/logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/any.kt b/logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/any.kt index 0940315a3a..dcd58ec15c 100644 --- a/logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/any.kt +++ b/logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/any.kt @@ -33,7 +33,6 @@ fun AssertionContainer.because(reason: String, assertionCreator: (Expect< fun AssertionContainer.notToBeAnInstanceOf(notExpectedTypes: List>): Assertion = impl.notToBeAnInstanceOf(this, notExpectedTypes) - @OptIn(ExperimentalNewExpectTypes::class) private inline val AssertionContainer.impl: AnyAssertions get() = getImpl(AnyAssertions::class) { DefaultAnyAssertions() } diff --git a/logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/grouping.kt b/logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/grouping.kt new file mode 100644 index 0000000000..0991dc2466 --- /dev/null +++ b/logic/atrium-logic/src/generated/commonMain/ch/tutteli/atrium/logic/grouping.kt @@ -0,0 +1,28 @@ +// @formatter:off +//--------------------------------------------------- +// Generated content, modify: +// buildSrc/generation.kt +// if necessary - enjoy the day 🙂 +//--------------------------------------------------- +@file:Suppress("ObjectPropertyName", "FunctionName") + +package ch.tutteli.atrium.logic + +import ch.tutteli.atrium.assertions.Assertion +import ch.tutteli.atrium.creating.AssertionContainer +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping +import ch.tutteli.atrium.core.ExperimentalNewExpectTypes +import ch.tutteli.atrium.logic.impl.DefaultGroupingAssertions + + +fun AssertionContainer.grouping(description: String, representationProvider: () -> Any?, groupingActions: ExpectGrouping.() -> Unit): Assertion = + impl.grouping(this, description, representationProvider, groupingActions) + +fun AssertionContainer.group(description: String, representationProvider: () -> Any?, assertionCreator: Expect.() -> Unit): Assertion = + impl.group(this, description, representationProvider, assertionCreator) + + +@OptIn(ExperimentalNewExpectTypes::class) +private inline val AssertionContainer.impl: GroupingAssertions + get() = getImpl(GroupingAssertions::class) { DefaultGroupingAssertions() } diff --git a/misc/atrium-specs/build.gradle.kts b/misc/atrium-specs/build.gradle.kts index ffc0ae4cb1..bba9bb2110 100644 --- a/misc/atrium-specs/build.gradle.kts +++ b/misc/atrium-specs/build.gradle.kts @@ -10,11 +10,14 @@ val spekVersion: String by rootProject.extra val niokVersion: String by rootProject.extra val spekExtensionsVersion: String by rootProject.extra val mockitoKotlinVersion: String by rootProject.extra +val junitPlatformVersion: String by rootProject.extra kotlin { sourceSets { commonMain { dependencies { + api(kotlin("test")) + api(prefixedProject("core")) // exclude this dependency in case you want to use another translation api(prefixedProject("translations-en_GB")) @@ -34,6 +37,8 @@ kotlin { apiWithExclude("ch.tutteli.niok:niok:$niokVersion") apiWithExclude("ch.tutteli.spek:tutteli-spek-extensions:$spekExtensionsVersion") apiWithExclude("com.nhaarman.mockitokotlin2:mockito-kotlin:$mockitoKotlinVersion") + api(kotlin("test-junit5")) + apiWithExclude("org.junit.platform:junit-platform-commons:$junitPlatformVersion") } } diff --git a/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/defaultBulletPoints.kt b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/defaultBulletPoints.kt index 5dcc80c4af..0ae34dda4b 100644 --- a/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/defaultBulletPoints.kt +++ b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/defaultBulletPoints.kt @@ -15,6 +15,7 @@ const val explanatoryBulletPoint = "» " const val warningBulletPoint = "❗❗ " const val informationBulletPoint = "ℹ " const val hintBulletPoint ="\uD83D\uDCA1 " +const val groupingBulletPoint = "# " val indentRootBulletPoint = " ".repeat(rootBulletPoint.length) val indentListBulletPoint = " ".repeat(listBulletPoint.length) @@ -26,6 +27,7 @@ val indentSuccessfulBulletPoint = " ".repeat(successfulBulletPoint.length) val indentFailingBulletPoint = " ".repeat(failingBulletPoint.length) val indentWarningBulletPoint = " ".repeat(warningBulletPoint.length) +val indentGroupingBulletPointIndent = " ".repeat(groupingBulletPoint.length) val defaultBulletPoints = mapOf( RootAssertionGroupType::class to rootBulletPoint, @@ -36,6 +38,8 @@ val defaultBulletPoints = mapOf( PrefixFailingSummaryAssertion::class to failingBulletPoint, ExplanatoryAssertionGroupType::class to explanatoryBulletPoint, WarningAssertionGroupType::class to warningBulletPoint, - InformationAssertionGroupType::class to informationBulletPoint + InformationAssertionGroupType::class to informationBulletPoint, + HintAssertionGroupType::class to hintBulletPoint, + GroupingAssertionGroupType::class to groupingBulletPoint ) diff --git a/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/integration/GroupingTest.kt b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/integration/GroupingTest.kt new file mode 100644 index 0000000000..cf754b5ae0 --- /dev/null +++ b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/integration/GroupingTest.kt @@ -0,0 +1,82 @@ +@file:Suppress("FunctionName") + +package ch.tutteli.atrium.specs.integration + +import ch.tutteli.atrium.api.fluent.en_GB.* +import ch.tutteli.atrium.api.verbs.internal.expect +import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.specs.* +import kotlin.test.Test + +abstract class GroupingTest( + group: Fun3 Any?, Expect.() -> Unit> +) { + val groupFun: Expect.(String, () -> Any?, Expect.() -> Unit) -> Expect = group.lambda + + //TODO 1.1.0 add subjectLess and assertionCreator tests + @Test + fun all_sub_expectation_hold__does_not_throw() { + expect(1).groupFun("my group name", {}) { + toEqual(1) + toBeLessThan(10) + toBeGreaterThan(0) + } + } + + @Test + fun sub_expectations_fail__reports_only_failing() { + expect { + expect(1).groupFun("my group name", { 123 }) { + toEqual(2) + toBeLessThan(10) + toBeGreaterThan(4) + } + }.toThrow { + message { + toContainRegex( + "${groupingBulletPoint}my group name: 123.*$lineSeparator" + + "${indentGroupingBulletPointIndent}$rootBulletPoint$toEqualDescr: 2.*$lineSeparator" + + "${indentGroupingBulletPointIndent}$rootBulletPoint$toBeGreaterThanDescr: 4" + ) + notToContain("$rootBulletPoint$toBeLessThanDescr: 10") + } + } + } + + @Test + fun sub_group_holds__does_not_throw() { + expect(1).groupFun("my group name", { 123 }) { + groupFun("other name", { "hello" }) { + toEqual(1) + toBeLessThan(10) + toBeGreaterThan(0) + } + } + } + + + @Test + fun sub_group_fails__only_reports_failing() { + expect { + expect(1).groupFun("my group name", { 123 }) { + toEqual(2) + toBeLessThan(10) + + groupFun("other name", { "hello" }) { + toBeLessThan(10) + toBeGreaterThan(4) + } + } + }.toThrow { + message { + toContainRegex( + "${groupingBulletPoint}my group name: 123.*$lineSeparator" + + "${indentGroupingBulletPointIndent}$rootBulletPoint$toEqualDescr: 2.*$lineSeparator" + + "${indentGroupingBulletPointIndent}${groupingBulletPoint}other name: \"hello\".*$lineSeparator" + + "${indentGroupingBulletPointIndent}${indentGroupingBulletPointIndent}$rootBulletPoint$toBeGreaterThanDescr: 4" + ) + notToContain("$rootBulletPoint$toBeLessThanDescr: 10") + } + } + } +} diff --git a/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/testUtils.kt b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/testUtils.kt index e4b25d120a..aaf84ee475 100644 --- a/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/testUtils.kt +++ b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/testUtils.kt @@ -6,6 +6,7 @@ import ch.tutteli.atrium.creating.Expect import ch.tutteli.atrium.logic.utils.expectLambda import ch.tutteli.atrium.translations.DescriptionAnyExpectation import ch.tutteli.atrium.translations.DescriptionBasic +import ch.tutteli.atrium.translations.DescriptionComparableExpectation import kotlin.jvm.JvmName import kotlin.reflect.* @@ -286,6 +287,8 @@ val toEqualDescr = DescriptionAnyExpectation.TO_EQUAL.getDefault() val toBeDescr = DescriptionBasic.TO_BE.getDefault() val notToBeDescr = DescriptionBasic.NOT_TO_BE.getDefault() val toBeAnInstanceOfDescr = DescriptionAnyExpectation.TO_BE_AN_INSTANCE_OF.getDefault() +val toBeLessThanDescr = DescriptionComparableExpectation.TO_BE_LESS_THAN.getDefault() +val toBeGreaterThanDescr = DescriptionComparableExpectation.TO_BE_GREATER_THAN.getDefault() expect val lineSeparator: String diff --git a/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/verbs/VerbSpec.kt b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/verbs/VerbSpec.kt index 986deb0664..60f8cda9bb 100644 --- a/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/verbs/VerbSpec.kt +++ b/misc/atrium-specs/src/commonMain/kotlin/ch/tutteli/atrium/specs/verbs/VerbSpec.kt @@ -1,17 +1,15 @@ package ch.tutteli.atrium.specs.verbs import ch.tutteli.atrium.api.fluent.en_GB.* -import ch.tutteli.atrium.core.ExperimentalNewExpectTypes import ch.tutteli.atrium.core.polyfills.fullName +import ch.tutteli.atrium.creating.ErrorMessages import ch.tutteli.atrium.creating.Expect -import ch.tutteli.atrium.creating.ExperimentalComponentFactoryContainer +import ch.tutteli.atrium.creating.ExpectGrouping import ch.tutteli.atrium.logic._logic import ch.tutteli.atrium.logic.changeSubject import ch.tutteli.atrium.logic.creating.RootExpectBuilder +import ch.tutteli.atrium.specs.* import ch.tutteli.atrium.specs.AssertionVerb -import ch.tutteli.atrium.specs.prefixedDescribeTemplate -import ch.tutteli.atrium.specs.toBeAnInstanceOfDescr -import ch.tutteli.atrium.specs.toEqualDescr import ch.tutteli.atrium.translations.DescriptionAnyExpectation.TO_BE_AN_INSTANCE_OF import ch.tutteli.atrium.translations.DescriptionComparableExpectation import ch.tutteli.atrium.translations.DescriptionComparableExpectation.TO_BE_GREATER_THAN @@ -24,6 +22,9 @@ abstract class VerbSpec( forNonNullableCreator: Pair.() -> Unit) -> Expect>, forNullable: Pair Expect>, forThrowable: Pair Any?) -> Expect<() -> Any?>>, + forGrouping: Pair Unit) -> ExpectGrouping>, + createSubGroup: Pair Unit) -> ExpectGrouping>, + createSubExpect: Pair Expect>, describePrefix: String = "[Atrium] " ) : Spek({ @@ -114,11 +115,10 @@ abstract class VerbSpec( assert { assertionVerb(null).notToEqualNull { toEqual(1) } }.toThrow { - @Suppress("DEPRECATION") - (messageToContain( + messageToContain( toBeAnInstanceOfDescr, "Int", "$toEqualDescr: 1" - )) + ) } } } @@ -153,6 +153,134 @@ abstract class VerbSpec( } } } + prefixedDescribe("assertion verb which creates an ${ExpectGrouping::class}") { + val (_, assertionVerb) = forGrouping + val (_, group) = createSubGroup + val (_, expect) = createSubExpect + context("no expect defined via ${createSubExpect.name}") { + it("nothing defined throws and reports missing expect") { + assert { + assertionVerb("group description") {} + }.toThrow { + message { + toContain( + "group description:", + ErrorMessages.AT_LEAST_ONE_EXPECTATION_DEFINED.getDefault() + ": false", + ErrorMessages.FORGOT_DO_DEFINE_EXPECTATION.getDefault(), + ErrorMessages.HINT_AT_LEAST_ONE_EXPECTATION_DEFINED.getDefault() + ) + } + } + + } + it("only groups defined throws and reports each group with a missing expect") { + assert { + assertionVerb("group description") { + group("without expect") {} + + group("with expect") { + expect(2).toEqual(2) + } + + group("another without") {} + } + }.toThrow { + message { + toContain( + "without expect", + "another without" + ) + toContain.exactly(2).values( + ErrorMessages.AT_LEAST_ONE_EXPECTATION_DEFINED.getDefault() + ": false", + ErrorMessages.FORGOT_DO_DEFINE_EXPECTATION.getDefault(), + ErrorMessages.HINT_AT_LEAST_ONE_EXPECTATION_DEFINED.getDefault() + ) + notToContain("with expect") + } + } + } + } + + context("the first expect holds") { + it("does not throw an exception") { + assertionVerb("my lovely expectations") { + expect(1).toEqual(1) + } + } + context("a subsequent expect holds") { + it("does not throw an exception") { + assertionVerb("my lovely expectations") { + expect(1).toEqual(1) + expect(0).toBeLessThan(2) + } + } + } + context("a subsequent group of expect hold") { + it("does not throw an exception") { + assertionVerb("my lovely expectations") { + expect(1).toEqual(1) + group("some group") { + expect(1).toBeLessThan(2) + } + } + } + } + context("a subsequent expect fails") { + it("throws an AssertionError") { + assert { + assertionVerb("my lovely expectations") { + expect(1).toEqual(1) + expect(1).toBeLessThan(1) + } + }.toThrow { + message { + toContain("${TO_BE_LESS_THAN.getDefault()}: 1") + notToContain(toEqualDescr) + } + } + } + } + + context("multiple subsequent expect/group fail") { + it("evaluates all and then throws an AssertionError, reporting only failing") { + assert { + assertionVerb("my lovely expectations") { + expect(1).toEqual(1) // holds + expect(2).toEqual(3) + group("verifying Xy") { + expect(4).toBeLessThan(0) + expect(5).toEqual(6) + expect(7).toBeGreaterThan(1) // holds + } + expect(8).toEqual(9) + } + }.toThrow { + message { + toContain( + ": 2", + "$toEqualDescr: 3", + "# verifying Xy", + ": 4", + "${TO_BE_LESS_THAN.getDefault()}: 0", + ": 5", + "$toEqualDescr: 6", + + ": 8", + "$toEqualDescr: 9", + ) + notToContain( + ": 1", + "$toEqualDescr: 1", + ": 7", + "${TO_BE_GREATER_THAN.getDefault()}: 1", + ) + } + } + } + } + } + + } }) diff --git a/misc/atrium-verbs-internal/src/commonMain/kotlin/ch.tutteli.atrium.api.verbs.internal/atriumVerbs.kt b/misc/atrium-verbs-internal/src/commonMain/kotlin/ch.tutteli.atrium.api.verbs.internal/atriumVerbs.kt index 6864042952..646cc1ab09 100644 --- a/misc/atrium-verbs-internal/src/commonMain/kotlin/ch.tutteli.atrium.api.verbs.internal/atriumVerbs.kt +++ b/misc/atrium-verbs-internal/src/commonMain/kotlin/ch.tutteli.atrium.api.verbs.internal/atriumVerbs.kt @@ -3,22 +3,16 @@ package ch.tutteli.atrium.api.verbs.internal import ch.tutteli.atrium.assertions.Assertion import ch.tutteli.atrium.core.ExperimentalNewExpectTypes import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping import ch.tutteli.atrium.creating.ExperimentalComponentFactoryContainer import ch.tutteli.atrium.creating.RootExpect -import ch.tutteli.atrium.creating.feature.ExperimentalFeatureInfo -import ch.tutteli.atrium.logic._logic +import ch.tutteli.atrium.logic.* import ch.tutteli.atrium.logic.creating.RootExpectBuilder import ch.tutteli.atrium.reporting.AtriumErrorAdjuster +import ch.tutteli.atrium.reporting.Text import ch.tutteli.atrium.reporting.erroradjusters.NoOpAtriumErrorAdjuster -/** - * Creates an [Expect] for the given [subject]. - * - * @param subject The subject for which we are going to postulate expectations. - * - * @return The newly created [RootExpect]. - * @throws AssertionError in case an assertion does not hold. - */ + @OptIn(ExperimentalNewExpectTypes::class, ExperimentalComponentFactoryContainer::class) fun expect(subject: T): RootExpect = RootExpectBuilder.forSubject(subject) @@ -29,16 +23,31 @@ fun expect(subject: T): RootExpect = } .build() -/** - * Creates an [Expect] for the given [subject] and appends the assertions create by the given - * [assertionCreator]-lambda where the created [Assertion]s are added as a group and reported as a whole. - * - * @param subject The subject for which we are going to postulate expectations. - * @param assertionCreator expectation-group with a non-fail fast behaviour. - * - * @return The newly created [RootExpect]. - * @throws AssertionError in case an assertion does not hold. - */ fun expect(subject: T, assertionCreator: Expect.() -> Unit): Expect = expect(subject)._logic.appendAsGroup(assertionCreator) + +@OptIn(ExperimentalNewExpectTypes::class) +fun expectGrouped( + description: String = "my expectations", + configuration: RootExpectBuilder.OptionsChooser<*>.() -> Unit = {}, + groupingActions: ExpectGrouping.() -> Unit, +): ExpectGrouping = RootExpectBuilder.forSubject(Text.EMPTY) + .withVerb(description) + .withOptions { + configuration() + } + .build() + ._logic.appendAsGroup(groupingActions.toAssertionCreator()) + .toExpectGrouping() + + +fun ExpectGrouping.expect(subject: R): Expect = + expectWithinExpectGroup(subject).transform() + +fun ExpectGrouping.expect(subject: R, assertionCreator: Expect.() -> Unit): Expect = + expectWithinExpectGroup(subject).transformAndAppend(assertionCreator) + + +private fun ExpectGrouping.expectWithinExpectGroup(subject: R) = + _logic.manualFeature("I expected subject") { subject } diff --git a/misc/atrium-verbs-internal/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/internal/VerbSpec.kt b/misc/atrium-verbs-internal/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/internal/VerbSpec.kt index 4026b199d9..695c4e493d 100644 --- a/misc/atrium-verbs-internal/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/internal/VerbSpec.kt +++ b/misc/atrium-verbs-internal/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/internal/VerbSpec.kt @@ -1,10 +1,20 @@ package ch.tutteli.atrium.api.verbs.internal +import ch.tutteli.atrium.logic._logicAppend +import ch.tutteli.atrium.logic.grouping +import ch.tutteli.atrium.reporting.Text import ch.tutteli.atrium.specs.verbs.VerbSpec object ExpectSpec : VerbSpec( "expect" to { subject: Int -> expect(subject) }, "expect" to { subject: Int, assertionCreator -> expect(subject, assertionCreator) }, "expect" to { subject: Int? -> expect(subject) }, - "expect" to { act: () -> Any? -> expect { act() } } + "expect" to { act: () -> Any? -> expect { act() } }, + "expectGrouped" to { description, assertionCreator -> + expectGrouped(description, groupingActions = assertionCreator) + }, + "expectGrouped" to { description, assertionCreator -> + _logicAppend { grouping(description, Text.Companion.EMPTY_PROVIDER, groupingActions = assertionCreator) } + }, + "expect" to { subject -> expect(subject) }, ) diff --git a/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/AssertionVerb.kt b/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/AssertionVerb.kt index 5098caf444..8112a8b50c 100644 --- a/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/AssertionVerb.kt +++ b/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/AssertionVerb.kt @@ -8,4 +8,5 @@ import ch.tutteli.atrium.reporting.translating.Translatable */ enum class AssertionVerb(override val value: String) : StringBasedTranslatable { EXPECT("I expected subject"), + EXPECT_GROUPED("my expectations"), } diff --git a/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/expect.kt b/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/expect.kt index e33b109c21..703a8ab2fe 100644 --- a/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/expect.kt +++ b/misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/expect.kt @@ -1,12 +1,14 @@ package ch.tutteli.atrium.api.verbs import ch.tutteli.atrium.api.verbs.AssertionVerb.EXPECT +import ch.tutteli.atrium.core.ExperimentalNewExpectTypes import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping import ch.tutteli.atrium.creating.FeatureExpect import ch.tutteli.atrium.creating.RootExpect -import ch.tutteli.atrium.logic._logic +import ch.tutteli.atrium.logic.* import ch.tutteli.atrium.logic.creating.RootExpectBuilder -import ch.tutteli.atrium.logic.manualFeature +import ch.tutteli.atrium.reporting.Text /** * Creates an [Expect] for the given [subject]. @@ -53,15 +55,86 @@ fun Expect.expect(newSubject: R): FeatureExpect = * Creates an [Expect] for the given (unrelated) [newSubject] and appends the expectations the given * [assertionCreator]-lambda creates as group to it. * + * Consider to use [expectGrouped] instead of [expect] as expectation entry point if you want to state expectations + * about several unrelated subjects. [expectGrouped] fulfills exactly this purpose. + * * We recommend to use `its` or `feature` or another feature extractor if you want to extract a feature out of the * current subject. * * @param newSubject The new subject for which we are going to postulate expectations. * @param assertionCreator expectation-group with a non-fail fast behaviour. * @return The newly created [Expect]. - * @throws AssertionError in case an assertion does not hold. * * @since 1.0.0 */ fun Expect.expect(newSubject: R, assertionCreator: Expect.() -> Unit): Expect = _logic.manualFeature(EXPECT) { newSubject }.transformAndAppend(assertionCreator) + +/** + * Creates an [ExpectGrouping] which can be used to group multiple unrelated subjects. + * + * @param description Description of the root group. + * @param groupingActions Some action which defines what happens within the group (typically, creating some + * expectations via [expect] or nesting the grouping further). + * @param configuration, Optionally, you can define more options via [RootExpectBuilder.OptionsChooser] such + * as exchange components etc. + * + * @since 1.1.0 + */ +@OptIn(ExperimentalNewExpectTypes::class) +fun expectGrouped( + description: String = AssertionVerb.EXPECT_GROUPED.getDefault(), + configuration: RootExpectBuilder.OptionsChooser<*>.() -> Unit = {}, + groupingActions: ExpectGrouping.() -> Unit, +): ExpectGrouping = RootExpectBuilder.forSubject(Text.EMPTY) + .withVerb(description) + .withOptions { + configuration() + } + .build() + ._logic.appendAsGroup(groupingActions.toAssertionCreator()) + .toExpectGrouping() + + +/** + * Creates an [Expect] for the given [subject]. + * + * @param subject The new subject for which we are going to postulate expectations. + * @return The newly created [Expect]. + * + * @since 1.1.0 + */ +fun ExpectGrouping.expect(subject: R): Expect = + expectWithinExpectGroup(subject).transform() + +/** + * Creates an [Expect] for the given [subject] and appends the expectations the given + * [assertionCreator]-lambda creates as group to it. + * + * @param subject The new subject for which we are going to postulate expectations. + * @param assertionCreator has to create at least one expectation where all are wrapped into an expectation-group + * with a non-fail fast behaviour. + * @return The newly created [Expect]. + * + * @since 1.1.0 + */ +fun ExpectGrouping.expect(subject: R, assertionCreator: Expect.() -> Unit): Expect = + expectWithinExpectGroup(subject).transformAndAppend(assertionCreator) + + +private fun ExpectGrouping.expectWithinExpectGroup(subject: R) = + _logic.manualFeature(EXPECT) { subject } + +/** + * In order to have one way only, use the function provided by the API such as `group`. + * + * You should basically only have one top `expectGrouped` as entry point and then only use functionality from the API. + * + * @since 1.1.0 + */ +@Deprecated("use `group` instead", ReplaceWith("this.group(description, representationProvider, groupingActions)")) +fun ExpectGrouping.expectGrouped( + description: String, + representationProvider: () -> Any = Text.EMPTY_PROVIDER, + groupingActions: ExpectGrouping.() -> Unit +): ExpectGrouping = _logicAppend { grouping(description, representationProvider, groupingActions) } diff --git a/misc/atrium-verbs/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/VerbSpec.kt b/misc/atrium-verbs/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/VerbSpec.kt index 9ab1745e10..a7dfde7764 100644 --- a/misc/atrium-verbs/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/VerbSpec.kt +++ b/misc/atrium-verbs/src/commonTest/kotlin/ch/tutteli/atrium/api/verbs/VerbSpec.kt @@ -6,5 +6,17 @@ object ExpectSpec : VerbSpec( "expect" to { subject: Int -> expect(subject) }, "expect" to { subject: Int, assertionCreator -> expect(subject, assertionCreator) }, "expect" to { subject: Int? -> expect(subject) }, - "expect" to { act -> expect { act() } }) + "expect" to { act -> expect { act() } }, + "expectGrouped" to { description, assertionCreator -> + expectGrouped(description, groupingActions = assertionCreator) + }, + "expectGrouped" to { description, assertionCreator -> + @Suppress( + // here we don't have access to the API, hence using this deprecated function is OK + "DEPRECATION" + ) + expectGrouped(description, groupingActions = assertionCreator) + }, + "expect" to { subject -> expect(subject) }, +) diff --git a/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt b/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt index 601d823fb5..b054fa59a9 100644 --- a/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt +++ b/misc/tools/readme-examples/src/main/kotlin/readme/examples/DataDrivenSpec.kt @@ -1,17 +1,14 @@ package readme.examples -//@formatter:off -//snippet-expectLambda-start -import ch.tutteli.atrium.logic.utils.expectLambda -//snippet-expectLambda-end -//@formatter:on + import readme.examples.utils.expect +import readme.examples.utils.expectGrouped import ch.tutteli.atrium.api.fluent.en_GB.* +import ch.tutteli.atrium.creating.ExpectationCreator import org.spekframework.spek2.Spek - /** * The tests and error message are written here and automatically placed into the README via generation. * The generation is done during the project built. To trigger it manually, you have to run: @@ -35,27 +32,29 @@ class DataDrivenSpec : Spek({ test("ex-data-driven-1") { //snippet-data-driven-1-insert - expect("calling myFun with...") { + expectGrouped { mapOf( 1 to 'a', 2 to 'c', 3 to 'e' ).forEach { (arg, result) -> - feature { f(::myFun, arg) }.toEqual(result) + group("calling myFun with $arg") { + expect(myFun(arg)).toEqual(result) + } } } } test("ex-data-driven-2") { - //snippet-expectLambda-insert - - expect("calling myFun with ...") { - mapOf( - 1 to expectLambda { toBeLessThan('f') }, - 2 to expectLambda { toEqual('c') }, - 3 to expectLambda { toBeGreaterThan('e') } + expectGrouped { + mapOf>( + 1 to { toBeLessThan('f') }, + 2 to { toEqual('c') }, + 3 to { toBeGreaterThan('e') } ).forEach { (arg, assertionCreator) -> - feature({ f(::myFun, arg) }, assertionCreator) + group("calling myFun with $arg") { + expect(myFun(arg), assertionCreator) + } } } } @@ -67,17 +66,47 @@ class DataDrivenSpec : Spek({ test("ex-data-driven-3") { //snippet-data-driven-3-insert - expect("calling myNullableFun with ...") { - mapOf( - Int.MIN_VALUE to expectLambda { toContain("min") }, + expectGrouped { + mapOf?>( + Int.MIN_VALUE to { toContain("min") }, -1 to null, 0 to null, - 1 to expectLambda { toEqual("1") }, - 2 to expectLambda { toEndWith("2") }, - Int.MAX_VALUE to expectLambda { toEqual("max") } + 1 to { toEqual("1") }, + 2 to { toEndWith("2") }, + Int.MAX_VALUE to { toEqual("max") } ).forEach { (arg, assertionCreatorOrNull) -> - feature { f(::myNullableFun, arg) }.toEqualNullIfNullGivenElse(assertionCreatorOrNull) + group("calling myFun with $arg") { + expect(myNullableFun(arg)).toEqualNullIfNullGivenElse(assertionCreatorOrNull) + } + } + } + } + + test("ex-data-driven-nesting") { + val x1 = 1 + val x2 = 3 + val y = 6 + + expectGrouped { + group("first group") { + expect(x1).toEqual(2) + group("sub-group") { + expect(x2).toBeGreaterThan(5) + } + } + group("second group") { + expect(y) { + group("sub-group 1") { + toBeGreaterThan(0) + toBeLessThan(5) + } + group("sub-group 2") { + notToEqual(6) + } + } } } } }) + + diff --git a/misc/tools/readme-examples/src/main/kotlin/readme/examples/utils/expect.kt b/misc/tools/readme-examples/src/main/kotlin/readme/examples/utils/expect.kt index 38b116be78..552a6dea44 100644 --- a/misc/tools/readme-examples/src/main/kotlin/readme/examples/utils/expect.kt +++ b/misc/tools/readme-examples/src/main/kotlin/readme/examples/utils/expect.kt @@ -3,22 +3,41 @@ package readme.examples.utils import ch.tutteli.atrium.api.fluent.en_GB.ExperimentalWithOptions import ch.tutteli.atrium.api.fluent.en_GB.withOptions import ch.tutteli.atrium.creating.Expect +import ch.tutteli.atrium.creating.ExpectGrouping import ch.tutteli.atrium.creating.ExperimentalComponentFactoryContainer import ch.tutteli.atrium.creating.build import ch.tutteli.atrium.logic._logic import ch.tutteli.atrium.reporting.text.TextObjectFormatter import ch.tutteli.atrium.reporting.text.impl.AbstractTextObjectFormatter import ch.tutteli.atrium.reporting.translating.Translator +import ch.tutteli.atrium.api.verbs.expect as atriumsExpect @OptIn(ExperimentalWithOptions::class, ExperimentalComponentFactoryContainer::class) fun expect(t: T): Expect = - ch.tutteli.atrium.api.verbs.expect(t).withOptions { + atriumsExpect(t).withOptions { withSingletonComponent(TextObjectFormatter::class) { c -> ReadmeObjectFormatter(c.build()) } } +@OptIn(ExperimentalWithOptions::class, ExperimentalComponentFactoryContainer::class) +fun expectGrouped( + groupingActions: ExpectGrouping.() -> Unit, +): ExpectGrouping = + ch.tutteli.atrium.api.verbs.expectGrouped( + configuration = { + withSingletonComponent(TextObjectFormatter::class) { c -> ReadmeObjectFormatter(c.build()) } + }, + groupingActions = groupingActions + ) + fun expect(t: T, assertionCreator: Expect.() -> Unit): Expect = expect(t)._logic.appendAsGroup(assertionCreator) +fun ExpectGrouping.expect(subject: R): Expect = atriumsExpect(subject) + +fun ExpectGrouping.expect(subject: R, assertionCreator: Expect.() -> Unit): Expect = + atriumsExpect(subject, assertionCreator) + + class ReadmeObjectFormatter(translator: Translator) : AbstractTextObjectFormatter(translator) { override fun identityHash(indent: String, any: Any): String =