Kotlin. Elegance that We Missed in Java
For me, as a software engineer with experience in Java, Kotlin was definitely the language of interest. It is also JVM language, but promised to be improved, concise, readable and beautiful.
So, what are the advantages of Kotlin?
-
Null safety - null references are detected at compile time.
-
Fluency - fluent code is readable and easier (and cheaper) to maintain.
-
Coroutines - a great way to implement non-blocking calls and gracefully handle exceptions.
-
Multiplatform - Kotlin can be used to write native macOS, iOS, and Windows applications; JavaScript and Android applications.
-
Interoperability - we can reuse existing Java code by intermixing Java and Kotlin.
In this article I will take a short look on Kotlin world on example of simple Spring Boot RESTfull application. Using Spring Boot with Kotlin makes an application even more elegant. Combination of those two gives us “double expressiveness”.
This simple CRUD KotlinTutor application will implement next API:
POST /kotlinquestions/ - Add new Kotlin question.
GET /kotlinquestions/ - Retrieves all Kotlin questions.
To generate skeleton of Kotlin Spring Boot application Spring initializer can be used. After generation, we got an entry point and can run an application.
@SpringBootApplication
class KotlinTutorApplication
fun main(args: Array<String>) {
runApplication<KotlinTutorApplication>(*args)
}
Kotlin code a bit simpler than Java. No semicolon. The main() function in the Kotlin version is a top-level function, instead of being a member of the class.
Testing
For testing purposes I will be using Kotest and Mockk. Kotlin code can be tested also with jUnit. Despite my great love to jUnit I wanted to try new testing tools and follow Kotlin natural style.
After adding necessary dependencies, we can start with the first test.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<exclusion>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.kotest.extensions</groupId>
<artifactId>kotest-extensions-spring</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-runner-junit5-jvm</artifactId>
<version>5.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>springmockk</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
Kotest has 10 different styles of test layout. I will go for original Kotlin ShouldSpec because I am used to add “should” to test names. To activate Spring extension we need to override extensions() function. it is analog to jUnit’s @ExtendWith.
@SpringBootTest
class KotlinTutorApplicationTest : FreeSpec() {
override fun extensions() = listOf(SpringExtension)
init {
"Should load context" {
}
}
}
Controller
Next, we will move to creating controller. We will start with WebMvcTest to check that endpoints work correctly.
@WebMvcTest(QuestionController::class)
class QuestionControllerTest(var mockMvc: MockMvc) : ShouldSpec() {
override fun extensions() = listOf(SpringExtension)
@MockkBean(relaxed = true)
private lateinit var questionsService: KotlinQuestionService
init {
should("return all questions") {
val question1 = kotlinQuestion()
val question2 = KotlinQuestion(1, "question2", "answer2", 3)
every { questionsService.getAllQuestions() } returns listOf(question1, question2)
mockMvc.perform(MockMvcRequestBuilders.get("/kotlinquestions"))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("\$.[0].question").value(question1.question))
.andExpect(jsonPath("\$.[0].answer").value(question1.answer))
.andExpect(jsonPath("\$.[0].questionType").value(question1.questionType))
}
should("post question") {
val question = kotlinQuestion()
val objectMapper = ObjectMapper()
val json = objectMapper.writeValueAsString(question)
mockMvc.perform(
MockMvcRequestBuilders.post("/kotlinquestions")
.contentType(MediaType.APPLICATION_JSON)
.content(json)
)
.andExpect(status().isOk)
verify { questionsService.addQuestion(question)
}
}
}
private fun kotlinQuestion() = KotlinQuestion(0, "question1", "answer1", 4)
}
We can see that code looks more concise than Java:
- Property KotlinQuestionService of a class QuestionController can be listed in its declaration.
- getQuestions() function body can an expression. Its return type is inferred.
@RestController
@RequestMapping("/kotlinquestions")
class QuestionController(val service: KotlinQuestionService) {
@GetMapping
fun getQuestions() = ResponseEntity.ok(service.getAllQuestions())
@PostMapping
fun addQuestion(@RequestBody kotlinQuestion: KotlinQuestion): ResponseEntity<String> {
service.addQuestion(kotlinQuestion)
return ResponseEntity.ok("Question is created")
}
}
Entity
Kotlin generates getters automatically, so we do need to write them. And, since we marked KotlinQuestion as a data class, we get methods equals(), hashCode(), and toString() also for free. As a result we got entity class without any noise.
@Entity
data class KotlinQuestion(
@Id @GeneratedValue val id: Long,
val question: String,
val answer: String,
val questionType: Int
)
Service layer
Service layer might not be necessary for such simple application, but we will create 2 services to see how Service is created with Kotlin and try out more tests with Mockk.
class KotlinQuestionServiceTest : ShouldSpec() {
@MockK(relaxed = true)
private lateinit var questionRepository: QuestionRepository
@MockK(relaxed = true)
private lateinit var verificationService: VerificationService
@InjectMockKs
lateinit var kotlinQuestionService: KotlinQuestionService
override fun beforeTest(testCase: TestCase) {
MockKAnnotations.init(this)
}
override fun afterTest(testCase: TestCase, result: TestResult) {
clearAllMocks()
}
init {
should("save question when verified") {
val kotlinQuestion = createQuestion()
every { verificationService.verifyQuestion(kotlinQuestion) } returns true
every { questionRepository.save(kotlinQuestion) } returns kotlinQuestion
kotlinQuestionService.addQuestion(kotlinQuestion)
verify { questionRepository.save(kotlinQuestion) }
}
should("throw an exception when not verified") {
val kotlinQuestion = createQuestion()
every { verificationService.verifyQuestion(kotlinQuestion) } returns false
verify(exactly = 0) { questionRepository.save(kotlinQuestion) }
}
}
private fun createQuestion(): KotlinQuestion {
return KotlinQuestion(1, "Question1", "Answer1", 2)
}
}
@Service
class KotlinQuestionService(val questionRepository: QuestionRepository, val verificationService: VerificationService) {
fun getAllQuestions(): Iterable<KotlinQuestion> = questionRepository.findAll()
fun addQuestion(question: KotlinQuestion) {
val verified = verificationService.verifyQuestion(question)
if (verified) {
questionRepository.save(question)
} else {
throw InvalidPropertiesFormatException("Question is not valid")
}
}
}
class VerificationServiceTest : ShouldSpec() {
init {
should("return true when type in a range 1 to 5") {
val verResult = VerificationService().verifyQuestion(createQuestion(4))
verResult shouldBe true
}
should("return false when 0") {
val verResult = VerificationService().verifyQuestion(createQuestion(0))
verResult shouldBe false
}
}
private fun createQuestion(type: Int): KotlinQuestion {
return KotlinQuestion(1, "Question1", "Answer1", type)
}
}
When statement makes conditional code more elegant. Especially for processing values of different types.
@Service
class VerificationService {
fun verifyQuestion(kotlinQuestion: KotlinQuestion) =
when (kotlinQuestion.questionType) {
in 1..5 -> true
10 -> true
else -> false
}
}
Spring and Kotlin fit very well together. Spring developers support Kotlin as a first class citizen. Hence, we can create the same application with less code.
Complete implementation with test cases can be found on GitHub.
Sources: