DEV Community

Daniele Bottillo
Daniele Bottillo

Posted on

5 1

Kotlin sealed classes: how to test them with Truth

In my team at Deliveroo we've recently introduced Truth library for unit testing and we are very pleased with it: we like the syntax, the readability and the extendability.
For example when dealing with sealed classes, unit test are not really the best; let's assume to have a very simple sealed class:

sealed class Food {
    class Pizza(val slices: Int): Food()
    class Sushi(val pieces: Int): Food()
}

In a typical unit test, if you want to test that some food is pizza and have 4 slices, you could write something like this:

@Test
fun `food is pizza and have 4 slices`() {
    val underTest = getFood()

    assertThat(underTest is Food.Pizza).isTrue()
    assertThat((underTest as Food.Pizza).slices).isEqualTo(4)
}

That's not elegant! Using is and as makes the code ugly and less readable and until Kotlin Contracts are stable there is no much that we can do to improve unless we create a custom subject. The idea is to extend Truth to have a more fluent syntax:

assertThat(underTest).isPizza(slices = 4)

That would make more sense! Let's see how to create this custom assertion, Truth requires two components: a subject and a subject factory. The subject is where the assertion lives and the factory is used to expose your custom subject:

class FoodSubject(metadata: FailureMetadata, actual: Food) : Subject<FoodSubject, Food>(metadata, actual) {}
fun food(): Subject.Factory<FoodSubject, Food> {
    return Subject.Factory<FoodSubject, Food> { metaData, target -> FoodSubject(metaData, target) }
}

The constructor of the subject is where you receive the target of your test actual:Food and inside the subject you can define your own assertion:

fun isPizza(slices:Int) {
    if (actual() !is Food.Pizza) {
        failWithoutActual(Fact.simpleFact("expected to be a Pizza!"))
    } 
    val target = actual() as Food.Pizza
    if (target.slices != slices) {
        failWithActual(Fact.simpleFact("expected to be have '$slices' slices but have '${target.slices}' instead"))
    }
}

The actual() method returns the food test object on which you can check whatever you are interesting in, in this case the method is testing that the type is Pizza and that the number of slices are the same. To use this method from the unit test you can do this:

@Test
fun `food is pizza and have 4 slices`() {
    val underTest = generateFood()

    assertWithMessage("food should be pizza with 4 slices")
            .about(food())
            .that(underTest)
            .isPizza(slices = 4)
}

That's very close to what we wanted in the beginning! But there is still one small improvement that we can do it. The about(food()).that(underTest) can be extracted to an utility method in the subject definition:

fun assertThat(@Nullable food: Food): FoodSubject {
    return assertAbout(food()).that(food)
}

This will change the test to:

@Test
fun `food is pizza and have 4 slices`() {
    val underTest = generateFood()
    assertThat(underTest).isPizza(slices = 4)
}

Which is exactly what we want :)

You can find the full example of GitHub: https://github.com/dbottillo/Blog/tree/truth_assertion

Happy testing!

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more