Skip to content

Requirements

Robert Stoll edited this page Apr 24, 2021 · 3 revisions

This page shall show which requirements Atrium wants to fulfil, why and for whom.

Personas

We see roughly three Personas using Atrium:

  1. Newcomer, a developer which uses only built-in functionality provided by Atrium's API => new expectation functions are only created by composing other expectation functions. This user would come up with something like:

    fun <T: Date> Expect<T>.isBetween(lowerBoundInclusive: T, upperBoundExclusive: T) =
        isGreaterOrEquals(lowerBoundInclusive).and.isLessThan(upperBoundExclusive)
  2. Long-term User, a developer which uses Atrium but also invents new simple expectation functions. A user who uses Atrium already for a longer time. This user would come up with something like:

    fun Expect<Int>.isEven() = 
        createAndAppend("is", RawString.create("an even number")) { it % 2 == 0 }
  3. Library author, a developer which uses Atrium but also invents more complex expectation functions for own types. Could be a library author or someone which actually wants to understand Atrium fully to get most out of it. This user would come up with something like:

    fun <A, B> Expect<Either<A, B>>.isLeft(): Expect<A> = changeToLeft().getExpectOfFeature()
    fun <A, B> Expect<Either<A, B>>.isLeft(assertionCreator: Expect<A>.() -> Unit) =
        changeToLeft().addToInitial(assertionCreator)
    
    private fun <A, B> Expect<Either<A, B>>.changeToLeft(): ExtractedFeaturePostStep<Either<A, B>, A> =
        ExpectImpl.feature.extractor(this)
            .withDescription("value of Left")
            .withRepresentationForFailure(RawString.create("❗❗ is not a Left"))
            .withFeatureExtraction {
                if (it is Left) Some(it.a) else None
            }
            .withoutOptions()
            .build()

Requirements

R1: Separation of infix and fluent API

Must/shall: must
For whom: all
Reasons:

  • Mixing different styles lead to different code in the same codebase which we want to prevent in the first place (a user can depend on two APIs if desired).
  • the learning curve is higher as there are multiple ways of achieving the same
  • especially newcomers are left in uncertainty which version they should use (fluent or infix) and if there are differences between the two versions

R2: Global Implementation Replacement

Must/shall: must
For whom: all

Users should be able to replace certain parts of Atrium’s implementation. In particular, the Reporter, Translation and Assertions implementation should be replaceable. When the implementation is replaced globally, there is no “dangling” entry point that would use the un-replaced functionality. For example: If the user replaces the implementation of Expect<Any>.toBe, there is no version of Expect<Any>.toBe that could be used to access the old implementation.

Reasons:

  • there may be implementation decisions that make sense for the main contributors of Atrium but do not suit all users
  • having multiple implementations of the same “thing” available can lead to subtle bugs

R3: Combinable, Shareable Implementation Changes

Must/shall: shall
For whom: Library author

It should be possible for non-Atrium maintainers (third party if you like) to publish implementations which extend Atrium. They should also be able to provide alternative implementations (covered by R2) for components such as a Reporter but also for a single expectation function.

Reasons:

  • Multiple users might have the same need from Atrium, while we as maintainers of Atrium do not want to fulfil it in Atrium itself. Users should then be able to share alternative implementations.

R4: No Mutable State at Test Runtime

Must/shall: must
For whom: all

As soon as the library is being used, it should not be possible to change any state or implementations.

Reasons:

  • avoid behaviour that is difficult to predict
  • avoid race conditions that can, in particular, occur in parallel test execution

R5: Test Readability

Must/shall: must
For whom: Newcomer

All expectation functions should have predictable behaviour. By reading source code written with Atrium’s API, the average Kotlin developer who does not know Atrium can understand what is being tested on what.

Reasons:

  • code reviews are crucial to detect errors in test logic (and thereby potentially in the tested logic)

R6: Failure Explanations

Must/shall: shall
For whom: all

When possible, and to the extent possible, users should be able to understand what they need to do in order to fix an expectation failure by just reading the expectation’s failure description.

Reasons:

  • the more obvious the mistake is after reading an expectation’s failure description, the less time for manual debugging is needed and ergo the more useful Atrium becomes.

R7: Completeness

Must/shall: shall
For whom: all

Atrium’s users should expect that Atrium will offer a convenient way to specify expectations for most situations they will encounter. Atrium should hence offer expectation functions for anything one can reasonably expect to happen frequently when programming in Kotlin.

Reasons:

  • maintainers need to weigh usefulness against maintenance effort. But an expectation function should never be dismissed if it is reasonably likely to be useful
  • Having an expectation function at hand for almost every use case increases developers’ trust in Atrium.

R8: Minimalistic

Must/shall: must
For whom: all

Although Atrium shall offer

anything one can reasonably expect

as stated in R7: Completeness, it shall also be minimalistic in the sense of more is not better. This especially means, that the API should expose most frequently used functionality dominantly and less used functionality in a second step. For instance Expect<Iterable<Int>>.contains.inOrder.only.grouped.within.inAnyOrder is a valid use case but one which is most likely almost never used. Thus it is pushed to the contains API but not to the Expect API.

Reasons:

  • having an overwhelming number of functions in an API makes it especially hard for newcomers to find the "tools" they need
  • using a programming API is not much different than using any other API and thus should follow insights of Usability, UX, Interaction design etc. => this might be unfamiliar to developers but makes sense => flat learning curve, more intuitive etc.

R9: One way to the goal

Must/shall: must
For whom: all

Similar to R1 Separation of infix and fluent API. In an ideal world there would be just one way to write an expectation. Which means:

  • no negation operator
  • alias shouldn't be used in most cases
  • shortcuts are only justified if they are used very regularly
  • shortcuts and aliases should be described accordingly in KDOC and point to the other version
  • incorporate runtime checks to catch misuses and point to the other implementation
  • no or operator

Reasons:

  • the learning curve is higher as there are multiple ways of achieving the same
  • especially for newcomers it is hard to know if there is a distinction between two things if they are named differently. For instance, if one could write Expect<Iterable<Int>>.contains.atLeast(1).butAtMost(1)... a newcomer might be unsure if this is the same as Expect<Iterable<Int>>.contains.exactly(1)...

R101: Little Custom Build Logic

Must/shall: shall
For whom: Library Authors

The project should follow conventional setups and require as little custom build logic as possible.

Reasons:

  • build logic can be especially difficult to evolve and maintain
  • the more non-standard a project setup is, the harder it is for new developers to get going