spring boot and kotlin wedding :) testing crud operation using spring web(rest layer) and Spring Data Jpa with Kotlin
This is an example project trying to learn Kotlin using Spring mvc and Spring data Jpa. When i finish the project i will give my own opinion about this new language (for me is new because I am just reading how to write code with it) and how difficult is to learn kotlin if you are a java (it’s my case)
-
Use Spring data jpa H2 in memory Database
-
Creating DTOs (POJOs/POCOs) is very easy (no library is needed as in Java). Properties in Kotlin classes can be declared either as mutable using the var keyword, or as read-only using the val keyword.
@Entity data class UserEntity (@Id @GeneratedValue val id: Long? = null, val name:String, val age:Int, val favoriteNumber: String) @Entity data class PetEntity (@Id @GeneratedValue val id: Long? = null, val name:String, val age:Int)
-
Inheritance: All classes in Kotlin have a common superclass Any, that is the default superclass for a class with no supertypes. To declare an explicit supertype, place the type after a colon in the class header:
@Service class UserServiceImpl (val userRepository: UserRepository) :UserService
-
Use mapping between layers using (https://stackoverflow.com/questions/39199426/better-way-to-map-kotlin-data-objects-to-data-objects) If you need to write a function that can be called without having a class instance but needs access to the internals of a class (for example, a factory method), you can write it as a member of an object declaration inside that class. Even more specifically, if you declare a companion object inside your class, you can access its members using only the class name as a qualifier.
companion object { fun createFromUserModel(user: com.cromero.api.service.User) = User( id = user.id, name = user.name, age = user.age, favoriteNumber = user.favoriteNumber, color = Color.assignColorPerAge(user.age) ) }
-
Create integration test layer using mockmvc and rest template
@Test fun `controller Must return User`() { //save an user val user = UserEntity(name = "pepe",age = 33,favoriteNumber = "25") userRepository.save(user) mockMvc.perform(get("/user/${user.name}")).andExpect(status().isOk) .andExpect(jsonPath("\$.name").value(user.name)) .andExpect(jsonPath("\$.age").value(user.age)) .andExpect(jsonPath("\$.favoriteNumber").value(user.favoriteNumber)) } @Test fun `controller Must return User using rest template`() { //save an user val user = UserEntity(name = "manolo",age = 33,favoriteNumber = "54") userRepository.save(user) //get the user by id val result = restTemplate.getForEntity<User>("/user/${user.name}") //user asserts from database assertEquals(result.statusCode,HttpStatus.OK) assertTrue(result.body?.name == user.name) assertTrue(result.body?.age == user.age) assertTrue(result.body?.favoriteNumber == user.favoriteNumber) }
-
Test when "pattern matching" and Enum classes with static methods using companion object
// When expression is exhaustive, else is a must with integer value. It is better to use Enum or Sealed //classes instead if it would be possible because do not need else branch fun assignColorPerAge(age: Int) = when (age) { in Int.MIN_VALUE..0 -> BLACK in 1..10 -> BLUE in 20..40 -> GREEN in 40..99 -> RED in 100..Int.MAX_VALUE -> BLACK else -> BLACK } // Example of code using Enum. Else branch is not a must with enum and sealed class when (val color=createFromUserModel.color) { Color.BLACK-> "Your black color description is ${color.description}" Color.RED -> "Your red color description is ${color.description}" Color.GREEN -> "Your green color description is ${color.description}" Color.BLUE -> "Your blue color description is ${color.description}" }
-
Test kotlin amazing null properties: using Safe Calls operator (?.) and the Elvis Operator (?:)
userService.findByName(name)?.let { User.createFromUserModel(it) }?: throw UserNotFoundException("user not found")
-
Added A convenient and performant logging library wrapping slf4j with Kotlin extensions. https://github.com/MicroUtils/kotlin-logging
private val LOGGER = KotlinLogging.logger {} // using Kotlin's String templates: String literals may contain template expressions, // i.e. pieces of code that are evaluated and whose results are concatenated into the string LOGGER.info("User $savedEntity was successfully created")
-
Testing Kotlin Collections filters and orders methods: Java .stream is not needed and it is not a must to make splicit call to .collect(Collectors.toList()); (collections are stronger comparing to Java)
/** * Find all users containing {letters} in its name. It returns all users if letters is null */ @GetMapping(value = ["/findAllContainsLetters", "/findAllContainsLetters/{letters}"]) fun findAllContainsLetter(@PathVariable("letters") letters: String?) = userService.findAll().filter { user: com.cromero.api.service.User -> letters?.let { cad: String -> user.name.toUpperCase().contains(cad.toUpperCase()) } ?: true }.sortedBy { it.name }
-
Ranges can be used in many cases: for, if, case etc…
if (createFromUserModel.age in 0..18) { "is not legal age" } else { "is legal age" } //Additional examples for (i in 1..100) { ... } // closed range: includes 100 for (i in 1 until 100) { ... } // half-open range: does not include 100 for (x in 2..10 step 2) { ... } for (x in 10 downTo 1) { ... } if (x in 1..10) { ... }
Spring loves Kotlin :) using several features making easier our code:
-
Injecting beans by constructor. example injecting our service bean in our controller:
@RestController @RequestMapping("/user") class UserController (val userService: UserService) @Service class UserServiceImpl (val userRepository: UserRepository,val customProperties: CustomProperties) :UserService
-
Use your data class defining your custom application properties (new in Spring boot 2.2):
//using data class and Kotling default values inside properties. //mandatory properties and optional using nullable properties @ConstructorBinding @ConfigurationProperties("com.cromero.application") data class CustomProperties( val name: String?="app with no name", val description: String, val database: Database) { data class Database( val host: InetAddress?=InetAddress.getByName("127.0.0.1"), val port: Integer, val connectTimeout : Duration=Duration.ofMillis(1000)) } // application.properties values server.port=8082 management.endpoint.health.show-details=always com.cromero.application.name=users application com.cromero.application.description=application demo using kotlin and Spring boot 2.2 com.cromero.application.database.host=123.23.23.2 com.cromero.application.database.port=88 com.cromero.application.database.connectTimeout=500ms