Skip to content

robertfmurdock/testmints

Repository files navigation

Logo testmints Build Maven Central

Gradle Libraries Update Gradle Wrapper Update

Latest Release GitHub commit activity

Do your tests have a great personality but have bad breath? Just looking for a way to freshen up your tests after a big meal of refactoring?

Testmints is here for you.

Testmints is a low-cal solution to bring a tiny bit of sugar to your Kotlin tests, hopefully improving readability and just generally making things less smelly.

Don't like the flavor? Submit a PR, or just don't use em. :-D

Requirements

All sugar here is multiplatform compliant and works great in combination with the standard kotlin.test library. The intention is to also minimize its dependencies as much as possible, to maximize portability.

Installation

Just want to dive right in? Here's the gradle snippets to get going. Basically, make sure you've loaded maven central and then reference the library. I don't recommend using the gradle plus setting unless you like to live on the edge.

repositories {
    mavenCentral()
}

kotlin {
    sourceSets {
        val commonTest by getting {
            dependencies {
                implementation("com.zegreatrob.testmints:standard:+")
            }
        }
    }
 }

Standard

Lots of people attempt to organize their tests in a setup, exercise, verify style, sometimes using the language given, when, then. You'll frequently see these tests written as such:

@Test
fun plusOne() {
    // Setup, or Given
    val input: Int = Random.nextInt()
    val expected = input + 1
    // Exercise, or When
    val result = input.plusOne()
    // Verify, or Then
    assertEquals(expected, result)
}

This works alright, but the comments are a little smelly. Some people might simply delete these comments, and use blank lines to indicate a section break. The trouble is though, that the comments are actually communicating something valuable to the reader - the name and intent of the section! And if you use blank lines to divide using these meaningful sections, then you can't use them again to break things up for any other reason.

As tests get more complex, this can be confusing, and programmers reading the test may find it difficult to identify what is exactly being tested. And if they can't figure the structure of the test out correctly, it'll be more difficult for them to maintain and understand the original intent behind the test as things change.

Let's chew on the standard mints and see what we can do!

@Test
fun plusOne() = setup(object {
    val input: Int = Random.nextInt()
    val expected = input + 1
}) exercise {
    input.plusOne()
} verify { result ->
    assertEquals(expected, result)
}

Whoa whoa whoa. So, we've replaced the comments with functions! This forces us to organize the test into sections. We store the data required for the setup on an anonymous kotlin object, to which the exercise and verify sections have "this" access. The return value from the exercise section is passed onto the verify section as a parameter.

By using this style, we've retained all the benefits of the comments, and formalized them, and, hopefully, made the test more expressive as a result in about the same amount of space (about 9 - 10 lines).

If you're an IntelliJ user (like me), you can download live templates for setting up these tests faster here. After you download the file, you can import it using File | Import Settings. If you don't see import settings, you're probably sharing your IDE settings and you'll have to disable sync for a moment in order to do the import.

Async

For tests that use Kotlin's coroutine system, the regular mints won't work - for those you'll need the alternate module: com.zegreatrob.testmints:async

@Test
fun plusOne() = asyncSetup(object {
    val input: Int = Random.nextInt()
    val expected = input + 1
}) exercise {
    input.suspendPlusOne() // suspend functions can be called in the all closures
} verify { result ->
    assertEquals(expected, result)
}

In all test closures, suspend functionality can be used. All jobs started in the exercise lambda will end before the verify lambda starts.

If you want access to any of the involved scopes (there is a test scope, a setup scope, and an exercise scope), you can add a ScopeMint to your context object:

@Test
fun plusOne() = asyncSetup(object : ScopeMint() {
    val input: Int = Random.nextInt()
    val testTarget = TestTarget(exerciseScope)
}) exercise {
    testTarget.spawnWorkOnExerciseScope()
} verify { 
    assertEquals(true, testTarget.workComplete)
}

You'll probably have to play with it a bit to get the hang of it, but give it a shot. So long as you can inject the "exercise scope" into your test target, you won't have to add waits for the work to be done.

Be warned: when using the async mints, you MUST use the form:

@Test
fun plusOne() = asyncSetup ...

and not

@Test
fun plusOne() { 
   asyncSetup ...
} 

In order to write tests safely on all platforms... especially KotlinJs.

I Still Don't Get It...

For more on the thinking, read these two essays:

The Quality of Test

An essay about what the right priorities should be when writing tests.

The Flavor of Mint

An essay about an attempt to embed those priorities in the test code itself.

Where did minenzyme, minreact, or react-data-loader go?

Those now live in the JsMints repo!, under new version numbers. Those were never really intended to be part of the testmints multiplatform suite, so it made sense to give them their own home.

But wait! There's more!

If you like the testmints lifecycle, you may also benefit from testmints lifecycle logging! There's now a Gradle plugin that will configure logging for each stage of your test - "setup start", "setup end", etc.

Why would you want that? Well, figuring out what's not working in a test is sometimes less obvious than we'd like. With log messages indicating where things went wrong, you can more clearly see when what you thought was a exercise problem actually is a setup problem.

For more information, see the readme.

Acknowledgements

Logo courtesy of Abs King!

Thanks for all the feedback on the library to anybody who's tried it. I hope it continues to be useful, and or inspires even better ideas!