Week of Java: Part 5 - Testing Your Code

So far I have covered all the basics to set up, build and deploy your Serverless + Kotlin project using AWS Lambda as our FaaS platform. So I bet the next question that you have in mind is: How can I test it? In this article, I’ll present tools and methods to perform automated tests using a specification framework for Kotlin and, luckily, you will have a fully tested product at the end.

Beyond Unit Tests

Something that I’ve learned during my professional experience as a software engineer is that tests are not just to verify that 2 + 2 = 4. They’re also a valuable tool to document and understand how your code should behave and respond under certain conditions. That’s why there are different flavours to them: Smoke Tests, Behavioural Tests, Regression Tests, Usability Tests, etc.

In general terms, tests are executable specifications of your system that can be encompassed into a Specification Framework.

A Specification Framework is a tool or method that helps us define an explicit, concise, and unambiguous test plan for our system

In the case of Kotlin, some authors have called this way of testing Bacon Driven Development, thanks to its most famous specification framework: Spek. Note: Spek in Dutch means bacon (now you understand the pun).

Developing Tests with Spek

One of the first things that we need to understand from Spek, is that it’s just a specification framework. For that reason, we can use any assertion library such as JUnit, Kotlin Test, Kluent, HamKrest, Expekt, etc. In this article, I’m going to use Spek + JUnit and Mockito as our Mock framework. 

Note: The use of JUnit is to avoid introducing too many new frameworks/libraries to the reader. However, I recommend using KotlinTest as your assertion library for your future projects. Stephen Samuel has written some good articles about KotlinTest such as Data Driven Testing With Kotlin and Kotlin Test Pro Tips.

Gradle Dependencies

The current stable version for Spek is the 1.x version with a 2.x version in development. I’m going to use version 1.1.5, which is the latest stable and supported one. Please include the next lines of code into your gradle file:

// Include the JUnit Dependency to import the JUnit plugin
buildscript {
 ...
 dependencies {
    ...
    classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
 }
}
...
apply plugin: 'org.junit.platform.gradle.plugin'// Includes Spek as part the JUnit Engines
junitPlatform {
 filters {
   engines {
     include 'spek'
   }
 }
}// Downloads Spek
repositories {
 ...
 maven { url "http://dl.bintray.com/jetbrains/spek" }
}dependencies {
 ...  // Compiles JUnit, Mockito Core and Mockito-Kotlin
 testCompile 'org.mockito:mockito-core:2.+'
 testCompile 'com.nhaarman:mockito-kotlin:1.5+'
 testCompile 'org.jetbrains.spek:spek-api:1.1.5'
 testCompile 'org.junit.platform:junit-platform-runner:1.0.0'  // Includes Spek engine
 testRuntime 'org.jetbrains.spek:spek-junit-platform-engine:1.1.5'}

Note: At time of writing, don’t try to include JUnit4, you will face multiple problems with your IDE. The official documentation states: As mentioned in the IDE Support section, the IDEA plugin won’t work if you’re using the JUnit 4 runner.

Designing Your Specification

Some of the most beautiful and useful concepts that Behavioral Driven Development (BDD) has introduced to the tech community are the Test Narrative and the Acceptance Criteria/Scenarios. Authors like Bob C. Martin defines the last one as the Given-When-Then convention, where:

  • Given a specific context
  • When some action or event is being triggered
  • Then an expected outcome should be obtained

Some BDD frameworks such as JBehave, RSpec, Mocha, Jasmine, Cucumber, among others, embrace the use of this template. Spek is not an exception to this rule. The main difference is that it doesn’t force you to use a concrete assertion framework or additional behavioral files (e.g Cucumber or JBehave).

Spek embraces the use of two different styles:

  • Given → On → It : In this case we define a context (given), specific action (on) and the test itself (it).
  • Describe → It: This second style defines a context (describe) and the actual test (it).

Additionally, Spek provides the concept of fixtures. Each one can run arbitrary code during different moments of the test life cycle. Some of those fixtures are:

  • beforeGroup
  • beforeEachTest
  • afterEachTest
  • afterGroup

Note: For further details visit the official Spek documentation website.

Testing Our Handler

Now that we already understand the basics, let’s drop some code! For our specific context, the first component that we should always test is the ApplicationHandler. Remember that handlers are the main point of entry to our Lambda Functions. Thus, if by chance we raise errors at this level, it is likely that our complete function will fail.

As I explained in the previous article, A Multi-layer core for your function: A request-dispatcher and micro HTTP router implementation, it is possible to implement a Client-Dispatcher-Server to reuse the same Handler for multiple functions. So maybe we should test that given an event, we can route to a specific function defined in our routes.yml file and return the correct HTTP Status code.

To do that, let’s create a file in src > test > kotlin > com.myblockbuster > TestHandler like this:

package com.myblockbuster

import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.whenever
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.given
import org.jetbrains.spek.api.dsl.it
import org.jetbrains.spek.api.dsl.on
import org.junit.Assert.assertEquals
import org.mockito.Mockito
import com.amazonaws.services.lambda.runtime.Context
import com.myblockbuster.core.EmptyModel
import com.myblockbuster.core.RouterException
import com.myblockbuster.core.dispatchers.RequestDispatcher

class TestHandler: Spek({
    given("a new event") {
        var handler: Handler? = null
        var context: Context? = null
        var dispatcher: RequestDispatcher? = null
        var map: Map<String, Any>? = null

        beforeEachTest {
            handler = Handler()
            context = Mockito.mock(Context::class.java)
            map = mapOf<String, Any>("path" to "test")
            dispatcher = Mockito.mock(RequestDispatcher::class.java)
            handler?.requestDispatcher = dispatcher!!

        }
        on("correct path") {
            it("should return a status code of 204 if the response body is empty") {
                whenever(dispatcher?.locate(any())).thenReturn(EmptyModel())
                val response = context?.let { handler?.handleRequest(map as Map<String, Any>, it) }
                assertEquals(204, response?.statusCode)
            }
            it("should return a status code of 200 if the response body is not empty") {
                whenever(dispatcher?.locate(any())).thenReturn(TestModel())
                val response = context?.let { handler?.handleRequest(map as Map<String, Any>, it) }
                assertEquals(200, response?.statusCode)
            }

        }
        on("non-existent path") {
            it("should return a status code of 404") {
                whenever(dispatcher?.locate(any())).thenThrow(RouterException(""))
                val response = context?.let { handler?.handleRequest(map as Map<String, Any>, it) }
                assertEquals(404, response?.statusCode)
            }
        }
    }
})

The previous TestHandler class will execute 3 different tests given an input event under different actions/conditions. Let’s start with the class declaration. We are extending from a Spek class and using its own DSL via lambda functions (thanks to type-safe builders).

Furthermore, inside the first given block, we’re using the beforeEachTest fixture to define an environment for our future tests. To mock the Lambda Context object and the dispatcher behavior we can always use Mockito.

As you can observe, I decided to use the given → on → it style, which in my opinion is the closest one to the given → when → then convention. For this particular case, we want to test the routing correctness through the HTTP status codes. So if an input path exists it should return 204 for an empty payload and 200 for a correctly payload. On the other hand, if an input path doesn't exist we should return a 404 code.

Finally, to run our tests in your IDE try to right click > Run Tests on the TestHandler.kt file or execute the command gradle test in your console. You should see something like this:

Execution of Spek Tests in IntelliJ

SOURCE CODE

If you want to see the complete example, the source code is available at this Github repo.

Final Thoughts

Specification frameworks are a nice way to complement, improve, and make our unit or integration tests more readable. In an industry where people don’t see a benefit to the documentation, using a specification framework is a nice way to describe the inputs, context and outputs of your code using something that developers understand… you guessed it! Code.

Originally posted at Medium

Try Serverless Console

Monitor, observe, and trace your serverless architectures.
Real-time dev mode provides streaming logs from your AWS Lambda Functions.

Subscribe to our newsletter to get the latest product updates, tips, and best practices!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.