Building a REST Service with Kotlin, Spring Boot, MongoDB, and JUnit

This year I’ve been ramping up on Kotlin by migrating some existing Spring- & Java-based REST services. When Spring 5.0 released this summer with improved Kotlin support, I dove in to explore the new options.

However, as I searched online, I became frustrated at the lack of a comprehensive tutorial on getting started with the technologies I had chosen: Spring Boot, Spring Data, MongoDB, and JUnit. Most tutorials touched on a single piece of the puzzle, but each used different idioms, making it difficult to assemble a complete service. In particular, few tutorials show how to integrate test-driven development into writing a Kotlin Spring service. And many tutorials focus on direct-wiring REST APIs to database access, but are hard to adapt to REST APIs with significant service logic.

This led me to assemble a sample REST service, which I present here. It is by no means comprehensive in terms of functionality, but does cover the core use cases for a “real” REST API.

  • The project uses test-driven development. In particular, the REST APIs have unit tests, and both internal services and the REST APIs use the Fongo (“Fake Mongo”) in-memory MongoDB implementation. This means they do not require a running MongoDB instance, and failing unit tests have no impact on persistent data. JUnit is used for unit testing.
  • Two Spring projects typically used in REST APIs are used here: Spring Boot and Spring Data.
  • Gradle is used to automate the build process.
  • A typical domain-driven development approach is used, separating model, repository, service, and controller classes.

The step-by-step construction of the project is broken out in separate folders in the GitHub project repository containing the project source code.

Tools and Technologies

The tutorial here uses IntelliJ IDEA, Gradle 4.2, and Postman for coding, building, and REST API testing. I’m running on Windows 10 using PowerShell, though there is no scripting and very little command line work, so other shells should work as well.

All software is written in Kotlin 1.1.51, with Spring 5.0 and Spring Boot 2.0 (still a milestone release at the time of this writing). MongoDB 3.4 Community Server is used for persistence.

From here, I’ll assume you have JDK 8.0, IntelliJ, Gradle, MongoDB, and Postman installed.

Player Score Service

This REST service isn’t much: it keeps track of player scores and can produce a very simple leader board. My goal is to illustrate the principles without cluttering up the code with too much domain-specific logic.

  • Players are tracked by handles, which can be arbitrary strings of letters and numbers. I don’t perform validation, however (a no-no; maybe I’ll add it in a future version of the tutorial).
  • A POST operation is used to add points to a player’s score; the player handle is part of the URI.
  • A GET operation to the leader board URI returns the top-3 player handles in rank order.
  • The player collection in the MongoDB database has the data about each player, keyed by player handle. This includes the score history. I made this document more complex than your usual relational database table to show how to work with structured documents in a MongoDB.
  • Everything stored, received, and returned by the service is formatted as JSON.

Some obvious gaps include the lack of an API to get player information and history, validation, use of domain types, etc. I kept them out in order to focus on the REST, database, and testing infrastructure.

Also, note that I’m omitting the package and import statements from the code below, to focus on the important parts. Look to the GitHub repository if you need to see the actual import statements. Also in the interest of compact presentation, I’m not writing the documentation comments for classes and functions, which really should be there.

Step 1: Spring Initialzr

The Spring Initalizr bootstraps a new Spring application. Do note that I’ve changed the value of every box from its default.

Spring Initializr setup
  • Select Gradle Project, Kotlin, and Spring Boot 2.0.
  • Pick an appropriate group; I’m using my own domain name wolniewicz.com.
  • The artifact name is the name of our REST service: playerscore.
  • The two dependencies: MongoDB and Web.

Generating the project downloads a .zip file with the initial project structure.

Unzip the project and import it into IntelliJ. This is also a good time to add it to your repository: I’m using GitHub and SmartGit. After importing, the project should look something like this:

Project viewed in IntelliJ with folders expanded

Step 2: Model and Repository

Player Model

I start with the player model object, in model/Player.kt.

@TypeAlias("player")
data class Player(@Id val handle: String,
val totalScore: Int = 0,
val history: List<ScoreEvent> = listOf()) {
operator fun plus(score: Int) =
Player(handle, totalScore + score, history + ScoreEvent(score))
}

data class ScoreEvent(val time: String,
val points: Int) {
constructor(points: Int) : this(dateFormat.format(Date()), points)

companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
}
}

A Player has three elements: the player’s handle (also its unique identifier), the total score, and the history of score events. Each score event is just a timestamp and the points scored.

This is not a particularly good data model: normally we wouldn’t want to carry around a heavyweight, ever-changing block of data like the score history inside an otherwise lightweight structure like Player. And yes, I know totalScore could be calculated instead of stored. And actually using dates instead of strings for time would be more useful for clients of the model. The purpose here is to provide a simple example without a lot of extra code.

I’ve made some choices specifically to simplify the JSON document structure as it will appear in the MongoDB:

  • The Spring Data MongoDB package will map the identifier to a field called “_id” (required by MongoDB). Using the Id annotation allows us to specify the field to use as the identifier without using the name “id” for that field.
  • A field called “_class” is also auto-injected by Spring Data MongoDB. By default this is the fully-qualified Java class name. I personally don’t like to embed the package name in persistent data, since that may change if code is refactored; TypeAlias lets us specify the name which should be used. This definitely makes the JSON smaller and easier to read.

To understand alternative options here, I recommend reading the reference documentation for Spring Data MongoDB, section 7.6. This is an area where your needs may differ substantially from my approach.

I’ve limited myself to Spring Data annotations. There are some MongoDB-specific annotations, such as Document, which provide additional functionality at the cost of tying your code to MongoDB. I’m choosing to restrict myself to generic Spring Data functionality, so I can switch to another data store in the future if needed.

Player Repository

The player repository, repository/PlayerRepository.kt, is where the real magic of Spring Data kicks in.

interface PlayerRepository : CrudRepository<Player, String> {
fun findTop3ByOrderByTotalScoreDesc() : List<Player>
}

Extending CrudRepository is sufficient to deliver basic read / write / delete operations. As with the annotations, there is a MongoRepository base interface which can be used to access additional, MongoDB-specific functionality, at the cost of increasing your code’s binding to MongoDB.

The single function defines the query to get the top 3 players ranked by total score, which we will use when implementing the leader board. The Defining Query Methods section of the Spring Data MongoDB reference discusses options for query names supported for MongoDB. No implementation is required; Spring Data will provide one for you.

Step 3: Player Service

PlayerService

Next we implement Player actions in service/PlayerService.kt. These implement the methods we will use when writing controllers. Our goal is to keep domain logic out of the controller code.

There is nothing wrong with having controllers talk directly to repositories directly, but I prefer to provide a service interface even for basic repository calls, such as the leaders method below. In my experience, direct calls to repositories tends to result in small bits of business logic showing up in controllers. Furthermore, Spring Data repositories are constrained in method names, while a service interface can map to method names more meaningful in the context of the application domain.

interface PlayerService {
fun leaders() : List<Player>
fun score(handle: String, points: Int) : Int
}

@Service("playerService")
class PlayerServiceImpl : PlayerService {
@Autowired
lateinit var playerRepository: PlayerRepository

override fun leaders(): List<Player> =
playerRepository.findTop3ByOrderByTotalScoreDesc()

override fun score(handle: String, points: Int) : Int {
val player = playerRepository
.findById(handle).orElse(Player(handle))
+ points
playerRepository.save(player)
return player.totalScore
}
}

Of course, in true test-driven development fashion, I actually wrote the unit tests below against stubbed versions of the interface implementation, then wrote the implementations to pass the unit tests.

In-Memory MongoDB Unit Testing

To speed up unit testing and avoid issues with persistent databases living beyond unit tests — or worse, having a database you want to keep corrupted by a unit test — a good practice is to use an in-memory database configured from scratch in unit tests. For MongoDB, the Fake Mongo, or Fongo, project provides this.

Configuring the unit tests to use Fongo requires three steps:

  1. Modify build.gradle to add the test package dependencies.
  2. Set up the test configuration for Spring Boot to use Fongo.
  3. Configure the Fongo database in the unit tests.

Note that we can also delete the Initializr-generated PlayerscoreApplicationTests.kt file; we will be writing our own unit tests.

First we add the necessary test dependencies to build.gradle in the dependencies block:

testCompile('org.jetbrains.kotlin:kotlin-test')
testCompile('org.jetbrains.kotlin:kotlin-test-junit')
testCompile('com.github.fakemongo:fongo:2.1.0')

The first two lines add the standard Kotlin library testing infrastructure for JUnit. The third line adds the Fongo package. All three are only used during testing, thus testCompile instead of compile.

Important note: at this point you will need to refresh your Gradle project in IntelliJ to force it to pull in the new libraries. If you see errors in the next step about not seeing the Fongo library, this is why.

Second we create a custom configuration for use during testing, TestConfiguration.kt.

@Configuration
class TestConfiguration : AbstractMongoConfiguration() {
@Autowired
lateinit var env: Environment

override fun getDatabaseName() =
env.getProperty("mongo.db.name", "test")

override fun mongoClient(): MongoClient {
logger.info("Instantiating Fongo with name $databaseName.")
return Fongo(databaseName).mongo
}

companion object {
val logger: Logger =
LoggerFactory.getLogger(TestConfiguration::class.java)
}
}

Here we’ve overridden the mongoClient bean to instantiate Fongo. The database name is read from the environment, but if it isn’t defined, “test” is used instead.

I’m using SLF4J interfaces for logging here. Spring will default to Logback, but feel free to use your preferred logging package if you wish.

Third, we need to set up individual unit tests which need the underlying database to use Fongo. Details can be found at github.com/fakemongo/fongo.

I prefer to abstract this kind of functionality into a base class, so writing actual unit tests is easier and faster. Here is PlayerScoreTestWithFongo.kt.

@RunWith(SpringRunner::class)
@SpringBootTest
abstract class PlayerScoreTestWithFongo(val initializeTestData: Boolean = true) {
@get:Rule
val fongoRule = FongoRule()

@Autowired
lateinit var playerRepository: PlayerRepository

@Before
fun setupTestDatabase() {
if (initializeTestData) {
playerRepository.save(TEST_PLAYER_1)
playerRepository.save(TEST_PLAYER_2)
playerRepository.save(TEST_PLAYER_3)
playerRepository.save(TEST_PLAYER_4)
playerRepository.save(TEST_PLAYER_5)
}
}

companion object {
val TEST_PLAYER_1 = Player("alice", 20)
val TEST_PLAYER_2 = Player("bob", 15)
val TEST_PLAYER_3 = Player("charlie", 25)
val TEST_PLAYER_4 = Player("dawn", 30)
val TEST_PLAYER_5 = Player("ed", 10)
}
}

Notable details:

  • The RunWith and SpringBootTest annotations are standard for a unit test class with Spring Boot, and in fact come right out of the PlayerscoreApplicationTests file auto-generated by Spring Initializr, which we deleted.
  • The class is abstract, which prevents JUnit from attempting to run the base class as a unit test directly.
  • The initializeTestData flag lets me control how the in-memory database is configured. In this way, different unit tests can share the base class but still customize their configuration setup. I usually use a more full-featured, builder-pattern-based test configuration, but am keeping it simple here.
  • FongoRule triggers the usage of Fongo for this unit test.
  • If the unit test requested test data, then the setupTestDatabase method creates 5 test players.

PlayerServiceTest

With all of the above infrastructure, we can write a very normal unit test for PlayerService, in service/PlayerServiceTest.kt. Virtually all of the code in the unit test is about the test of the service functionality; only the base class for the unit test is required to make full use of Fongo.

class PlayerServiceTest : PlayerScoreTestWithFongo() {
@Autowired
lateinit var playerService: PlayerService

@Test
fun testLeaders() {
logger.info("Begin testLeaders")

// Verify that the leaders are as expected.
val leaders = playerService.leaders()
assertEquals(3, leaders.size, "There should be 3 leaders.")
assertEquals(TEST_PLAYER_4, leaders[0],
"The first leader should be dawn.")
assertEquals(TEST_PLAYER_3, leaders[1],
"The second leader should be charlie.")
assertEquals(TEST_PLAYER_1, leaders[2],
"The third leader should be alice.")

logger.info("End testLeaders")
}

@Test
fun testScore() {
logger.info("Begin testScore")

playerRepository.save(Player(TEST_PLAYER_HANDLE))

// Score 10 points.
playerService.score(TEST_PLAYER_HANDLE, 10)
val player = playerRepository.findById(TEST_PLAYER_HANDLE).get()
assertEquals(10, player.totalScore,
"Total score should be 10 after the first scoring event.")
assertEquals(1, player.history.size,
"The history should have a single element.")
assertEquals(10, player.history[0].points,
"The recorded points should be 10.")

// Score 5 more points.
playerService.score(TEST_PLAYER_HANDLE, 5)
val player2 =
playerRepository.findById(TEST_PLAYER_HANDLE).get()
assertEquals(15, player2.totalScore,
"Total score should be 15 after the second scoring event.")
assertEquals(2, player2.history.size,
"The history should have a single element.")
assertEquals(10, player2.history[0].points,
"The first recorded points should be 10.")
assertEquals(5, player2.history[1].points,
"The second recorded points should be 5.")

logger.info("End testScore")
}

companion object {
val logger: Logger =
LoggerFactory.getLogger(PlayerServiceTest::class.java)
const val TEST_PLAYER_HANDLE = "testPlayer"
}
}

At this point, the unit tests should run and pass.

Step 4: Controllers

We are ready to build and test the controllers. As usual with Spring, we want to limit the code in the controllers to just that required for the RESTful interface. Domain logic is best kept in the services and the model classes.

Here I only demonstrate two interfaces: a GET to return the leaders board and a POST to add points to a player. Any realistic service would have more than this (you can’t even get a player’s current score!), but the additional methods don’t really add anything new, so I’m leaving them as an exercise for the reader :-)

GET Leaders

The Spring RestController and GetMapping annotations do all of the heavy lifting here, in controller/LeadersController.kt.

@RestController
class LeadersController {
@Autowired
lateinit var playerService: PlayerService

@GetMapping("/leaders")
fun getLeaders(): List<String> =
playerService.leaders().stream().map { it.handle }.toList()
}

Controller Unit Testing with MockMvc

As above, we move reusable portions of the controller test setup into a base class, controller/PlayerScoreTestWithFongoAndMockMvc.kt. We are using Spring’s MockMvc to mock HTTP requests and responses in unit tests.

@AutoConfigureMockMvc
abstract class PlayerScoreTestWithFongoAndMockMvc(
initializeTestData: Boolean = true) :
PlayerScoreTestWithFongo(initializeTestData) {
@Autowired
lateinit var mvc: MockMvc
}

Ok, the class names are getting very wordy, in true Spring fashion. I’d prefer something more compact in production code; your mileage may vary.

And here is the unit test for LeadersController, in controller/LeadersControllerTest.kt.

class LeadersControllerTest : PlayerScoreTestWithFongoAndMockMvc() {
@Test
fun getLeadersTest() {
logger.info("Begin getLeadersTest")

val expectedJson = """
|["${TEST_PLAYER_4.handle}",
|"${TEST_PLAYER_3.handle}",
|"${TEST_PLAYER_1.handle}"]
""".trimMargin()

mvc.perform(MockMvcRequestBuilders.get("/leaders"))
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(MockMvcResultMatchers.content()
.json(expectedJson))

logger.info("End getLeadersTest")
}

companion object {
val logger: Logger =
LoggerFactory.getLogger(LeadersControllerTest::class.java)
}
}

POST Player Score

To add points to a player’s score, we POST an integer value to “/player/{handle}/score”. The only trickiness here is the need to receive the request body as a string and convert it to an integer, since the Spring / Jackson JSON conversion won’t handle a Kotlin Int directly. Let’s put this in a separate controller from the leaders; controller/PlayerContoller.kt.

@RestController
class PlayerController {
@Autowired
lateinit var playerService: PlayerService

@PostMapping("/player/{handle}/score")
fun postPlayerScore(@PathVariable handle: String,
@RequestBody points: String) : String {
val score = playerService.score(handle, points.toInt())

return "$handle now has a total score of $score."
}
}

And the controller/PlayerControllerTest.kt unit test, very similar to that above, with the use of MockMvc’s post functionality.

class PlayerControllerTest : PlayerScoreTestWithFongoAndMockMvc() {
@Test
fun postPlayerScoreTest() {
logger.info("Begin postPlayerScoreTest")

val points = 5
val expectedTotalScore = TEST_PLAYER_1.totalScore + points
val expectedResult =
"${TEST_PLAYER_1.handle} now has a total score of " +
"$expectedTotalScore."

// Add 5 points to TEST_PLAYER_1's score.
mvc.perform(MockMvcRequestBuilders
.post("/player/${TEST_PLAYER_1.handle}/score")
.content(points.toString()))
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(MockMvcResultMatchers.content()
.string(expectedResult))

logger.info("End postPlayerScoreTest")
}

companion object {
val logger: Logger =
LoggerFactory.getLogger(PlayerControllerTest::class.java)
}
}

All of the unit tests should pass at this point. Below is the source code tree in its final form:

Source code tree in IntelliJ after step 4

Running the Service and Testing with Postman

To run the service and test it in Postman, I like to open 3 PowerShell windows.

In the first, run MongoDB:

In another, run the MongoDB shell:

In the last, use Gradle to build and run the service:

Testing with Postman is a simple way to interact with a local REST service. One easy thing to forget is setting the request body type to JSON for the POSTs.

Initially the leader board is empty.

Get the initial (empty) leaders board.

Let’s score points for Alice, Bob, and Charlie.

Alice scores 30 points.
Bob scores 15 points.
Charlie scores 25 points

And now the leaders board …

Leaders board showing ranked players.

Back in the MongoDB shell, let’s check the state of the database.

Final database state.

And that’s it! I hope you’ve found this all-in-one tutorial helpful in getting started with Kotlin and Spring 5.0. Please let me know if you find any errors above, have requests for changes, or would like to see similar tutorials on related topics.

Like what you read? Give Richard Wolniewicz a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.