Hey JVM! ¿What this tests actually mean?

Posted on

Let’s start by talking about unit-tests and their motif behind why we test. We can name a few known by everyone that we continuously repeat time after time: “Assure the quality of our codebase”, “Fault tolerant”, “Enables posterior changes”, but one goal behind tests we often underestimate or maybe we don’t understand it at all, I’m talking about documentation, yes the good old RTFM of a codebase and a system itself.

Once upon a time I heard from a coworker — who recently was changing from project and also technologies, specifically from python to swift — who said to me:

The first action I take when assessing a new codebase to understand the business, the technologies and frameworks involved, its architecture is…have a look at the tests

And it turns out that this quote, it has more sense today as I have been changing from project, my idea it’s not diminished what a good onboarding process has to cover in terms of gaining business/project context. So we are going to focus this post in apply the good practices that enable better documentation/specification of an application and how we can use the concepts brought by other testing frameworks that changed the mindset around testing.



Defining our goal

Write better tests on the JVM that serve as a good documentation of your app and also that give us a comprehensible error feedback.



Option 1: Just with JUnit.

Almost every Java project (also Kotlin) comes with JUnit configured as a Test engine. Sadly we don’t have many tools to help us to achieve our goal (☝️ ). Let’s see how we can improve the better understanding of our test purpose. Here an example:



Let’s take the next test as a starting point.

@Test
public void shouldAddMultipleNumbersAndProduceCorrectResult() {
    //given
    Calculator calculator = new Calculator();

    //when
    Integer total = calculator.sum(1, 2);

    //then
    assertEquals(3, total);
}
Enter fullscreen mode

Exit fullscreen mode

  • 1.1. Name of the tests

A minor improvement towards our goal is, is writing a more intentional name that tell us right when you look what is it really doing this unit test:

@Test
public void addingTwoNumbersReturnsItsSum() {
    //given...  
    //when...    
    //then...
    assertEquals(3, total);
}
Enter fullscreen mode

Exit fullscreen mode

  • 1.2. Better validations “asserts”

Using assert expression closer to our human language, with the help of Hamcrest. When you compare previous test presented here with the next one, we can tell a little improvement was made doing this kind of assertion.

import static org.hamcrest.CoreMatchers.is;
@Test
public void addingTwoNumbersReturnsItsSum() {
    //given...  
    //when...    
    //then
    assertThat(total, is(3));
}
Enter fullscreen mode

Exit fullscreen mode



Option 2: The Kotlin “way”

Kotlin, we already know it came to make a revolution in many aspects in the way we write code on the JVM, it has several similarities in what groovy already tried (yes I know I’m old!)

old parrot

Kotlin, has taken it further, thanks to the support given by Google and Android, this made Kotlin go to several levels above in a short period of time. Assuming the previous improvements already made, lets continue from there, but now focusing in what Kotlin offers us.

  • 2.1. The “Backticks Hack”

Kotlin give us a simple way to write more readable tests out of the box, quoting its coding conventions:

… In tests (and only in tests), it’s acceptable to use method names with spaces enclosed in backticks …

So, we can write our test with the following structure:

@Test
fun `Adding TWO numbers in a Calculator should return their total`() {
    //given...  
    //when...    
    //then...
}
Enter fullscreen mode

Exit fullscreen mode

Much more clear the real purpose behind this test, right? Maybe it’s debatable, but I can assure you, that when the time to read this test in a log of a thousand tests, It will be easier for you, even when you have to read this as a code documentation for our calculator.



Option 3: Introducing Specs and Spek

I know many of us are familiarized with concept “specification tests”, is a technique for doing BDD, that lets test our code and also serve as documentation for the rest of our teammates. (I’m not talking about our inner demons ). I’m talking about future developers or the product/business people.

This way of writing tests is very popular in other languages using frameworks like RSpec, Jasmine and several more. The important part of this towards our goal (☝️ ) is that in Kotlin exists a complementary framework called Spek, developed by JetBrains itself. Lets see how this framework can help us with our goal.

import org.spekframework.spek2.style.specification.describe
import org.spekframework.spek2.Spek

object CalculatorSpec: Spek({
    describe("A calculator") {
        val calculator by memoized { Calculator() }
        context("addition") {
            it("returns the sum of its arguments") {
                assertThat(calculator.sum(1, 2), is(3))
            }
        }
    }
})
Enter fullscreen mode

Exit fullscreen mode

Context/Describe: Here we define a group or context of execution, it’s similar to a Suite, and help us in our case to group tests for the calculator, and the addition “context”.

It: used to make a specific and atomic assertion of our test, in other words is the specification for the correct addition of our calculator.

Now putting this into practice, we can imagine how we can approach adding a new feature to our calculator and at the same time having documented it. Let’s do an exercise.

We want to include the possibility to divide in our calculator, so we can see the description of this new feature and the use cases, we can describe it (describe) with a test in the same context (calculator).

object CalculatorSpec: Spek({
    context("A calculator") {
        val calculator by memoized { Calculator() }
        describe("addition") { /*..*/}
        describe("division") {
            it("returns the division of its arguments") {
                assertThat(calculator.divide(10, 2), is(5))
            }
            it("throws an exception when divisor is zero") {
                assertFailsWith(ArithmeticException::class) {
                   calculator.divide(1, 0)
                }
            }        
        }
    }
})
Enter fullscreen mode

Exit fullscreen mode


Below is the required configurations to add Spek2 to your android project:

  • App: build.gradle
android {
    ...

    sourceSets.each {
        // In case we have our tests under kotlin folder
        it.java.srcDirs += "src/$it.name/kotlin"
    }

    testOptions {
        junitPlatform {
            filters {
                engines {
                    include 'spek2'
                }
            }

        }
        ...
    }
}

dependencies {
  testImplementation "org.junit.jupiter:junit-jupiter-api:5.5.1"
  testImplementation "org.junit.platform:junit-platform-runner:1.5.1"
  testImplementation "org.spekframework.spek2:spek-dsl-jvm:2.0.9"
  testImplementation "org.spekframework.spek2:spek-runner-junit5:2.0.9"  

  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.1'
}

//Apply plugin that lets run junit5 on android modules 
apply plugin: "de.mannodermaus.android-junit5"
Enter fullscreen mode

Exit fullscreen mode

  • Root: build.gradle
buildscript {
    dependencies { 
      classpath "de.mannodermaus.gradle.plugins:android-junit5:1.6.0.0" 
    }
}
Enter fullscreen mode

Exit fullscreen mode


To close this post looking beyond the techniques, patterns that we use to write tests in our applications, one important thing is to think in our future “Me” that would take this code to do a support task, or maybe this new person will be one without context of the product, or maybe playing with the words could be ourselves. So we should always write empathetic code with the mindset to help us in the future.

References:
1 Spek: What is Spek?

Leave a Reply

Your email address will not be published.