Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing
Make writing code in Kotlin an even more rewarding and fun experience as you explore the benefits of converting Java code to Kotlin and conversion solutions.
Join the DZone community and get the full member experience.
Join For FreeA sign of a good understanding of a programming language is not whether one is simply knowledgeable about the language’s functionality, but why such functionality exists. Without knowing this “why," the developer runs the risk of using functionality in situations where its use might not be ideal - or even should be avoided in its entirety! The case in point for this article is the lateinit keyword in Kotlin. Its presence in the programming language is more or less a way to resolve what would otherwise be contradictory goals for Kotlin:
- Maintain compatibility with existing Java code and make it easy to transcribe from Java to Kotlin. If Kotlin were too dissimilar to Java - and if the interaction between Kotlin and Java code bases were too much of a hassle - then adoption of the language might have never taken off.
- Prevent developers from declaring class members without explicitly declaring their value, either directly or via constructors. In Java, doing so will assign a default value, and this leaves non-primitives - which are assigned a null value - at the risk of provoking a
NullPointerException
if they are accessed without a value being provided beforehand.
The problem here is this: what happens when it’s impossible to declare a class field’s value immediately? Take, for example, the extension model in the JUnit 5 testing framework. Extensions are a tool for creating reusable code that conducts setup and cleanup actions before and after the execution of each or all tests. Below is an example of an extension whose purpose is to clear out all designated database tables after the execution of each test via a Spring bean that serves as the database interface:
public class DBExtension implements BeforeAllCallback, AfterEachCallback {
private NamedParameterJdbcOperations jdbcOperations;
@Override
public void beforeAll(ExtensionContext extensionContext) {
jdbcOperations = SpringExtension.getApplicationContext(extensionContext)
.getBean(NamedParameterJdbcTemplate.class);
clearDB();
}
@Override
public void afterEach(ExtensionContext extensionContext) throws Exception {
clearDB();
}
private void clearDB() {
Stream.of("table_one", "table_two", "table_three").forEach((tableName) ->
jdbcOperations.update("TRUNCATE " + tableName, new MapSqlParameterSource())
);
}
}
(NOTE: Yes, using the @Transactional
annotation is possible for tests using Spring Boot tests that conduct database transactions, but some use cases make automated transaction roll-backs impossible; for example, when a separate thread is spawned to execute the code for the database interactions.)
Given that the field jdbcOperations
relies on the Spring framework loading the proper database interface bean when the application is loaded, it cannot be assigned any substantial value upon declaration. Thus, it receives an implicit default value of null
until the beforeAll()
function is executed. As described above, this approach is forbidden in Kotlin, so the developer has three options:
- Declare
jdbcOperations
asvar
, assign a garbage value to it in its declaration, then assign the “real” value to the field inbeforeAll()
:
class DBExtension : BeforeAllCallback, AfterEachCallback {
private var jdbcOperations: NamedParameterJdbcOperations = StubJdbcOperations()
override fun beforeAll(extensionContext: ExtensionContext) {
jdbcOperations = SpringExtension.getApplicationContext(extensionContext)
.getBean(NamedParameterJdbcOperations::class.java)
clearDB()
}
override fun afterEach(extensionContext: ExtensionContext) {
clearDB()
}
private fun clearDB() {
listOf("table_one", "table_two", "table_three").forEach { tableName: String ->
jdbcOperations.update("TRUNCATE $tableName", MapSqlParameterSource())
}
}
}
The downside here is that there’s no check for whether the field has been assigned the “real” value, running the risk of invalid behavior when the field is accessed if the “real” value hasn’t been assigned for whatever reason.
2. Declare jdbcOperations
as nullable and assign null
to the field, after which the field will be assigned its “real” value in beforeAll()
:
class DBExtension : BeforeAllCallback, AfterEachCallback {
private var jdbcOperations: NamedParameterJdbcOperations? = null
override fun beforeAll(extensionContext: ExtensionContext) {
jdbcOperations = SpringExtension.getApplicationContext(extensionContext)
.getBean(NamedParameterJdbcOperations::class.java)
clearDB()
}
override fun afterEach(extensionContext: ExtensionContext) {
clearDB()
}
private fun clearDB() {
listOf("table_one", "table_two", "table_three").forEach { tableName: String ->
jdbcOperations!!.update("TRUNCATE $tableName", MapSqlParameterSource())
}
}
}
The downside here is that declaring the field as nullable is permanent; there’s no mechanism to declare a type as nullable “only” until its value has been assigned elsewhere. Thus, this approach forces the developer to force the non-nullable conversion whenever accessing the field, in this case using the double-bang (i.e. !!
) operator to access the field’s update()
function.
3. Utilize the lateinit
keyword to postpone a value assignment to jdbcOperations
until the execution of the beforeAll()
function:
class DBExtension : BeforeAllCallback, AfterEachCallback {
private lateinit var jdbcOperations: NamedParameterJdbcOperations
override fun beforeAll(extensionContext: ExtensionContext) {
jdbcOperations = SpringExtension.getApplicationContext(extensionContext)
.getBean(NamedParameterJdbcOperations::class.java)
clearDB()
}
override fun afterEach(extensionContext: ExtensionContext) {
clearDB()
}
private fun clearDB() {
listOf("table_one", "table_two", "table_three").forEach { tableName: String ->
jdbcOperations.update("TRUNCATE $tableName", MapSqlParameterSource())
}
}
}
No more worrying about silently invalid behavior or being forced to “de-nullify” the field each time it’s being accessed! The “catch” is that there’s still no compile-time mechanism for determining whether the field has been accessed before it’s been assigned a value - it’s done at run-time, as can be seen when decompiling the clearDB()
function:
private final void clearDB() {
Iterable $this$forEach$iv = (Iterable)CollectionsKt.listOf(new String[]{"table_one", "table_two", "table_three"});
int $i$f$forEach = false;
NamedParameterJdbcOperations var10000;
String tableName;
for(Iterator var3 = $this$forEach$iv.iterator(); var3.hasNext(); var10000.update("TRUNCATE " + tableName, (SqlParameterSource)(new MapSqlParameterSource()))) {
Object element$iv = var3.next();
tableName = (String)element$iv;
int var6 = false;
var10000 = this.jdbcOperations;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("jdbcOperations");
}
}
}
Not ideal, considering what’s arguably Kotlin’s star feature (compile-time checking of variable nullability to reduce the likelihood of the “Billion-Dollar Mistake”) - but again, it’s a “least-worst” compromise to bridge the gap between Kotlin code and the Java-based code that provides no alternatives that adhere to Kotlin’s design philosophy.
Use Wisely!
Aside from the above-mentioned issue of conducting null checks only at run-time instead of compile-time, lateinit
possesses a few more drawbacks:
- A field that uses
lateinit
cannot be an immutableval
, as its value is being assigned at some point after the field’s declaration, so the field is exposed to the risk of inadvertently being modified at some point by an unwitting developer and causing logic errors. - Because the field is not instantiated upon declaration, any other fields that rely on this field - be it via some function call to the field or passing it in as an argument to a constructor - cannot be instantiated upon declaration as well. This makes
lateinit
a bit of a “viral” feature: using it on field A forces other fields that rely on field A to uselateinit
as well.
Given that this mutability of lateinit
fields goes against another one of Kotlin’s guiding principles - make fields and variables immutable where possible (for example, function arguments are completely immutable) to avoid logic errors by mutating a field/variable that shouldn’t have been changed - its use should be restricted to where no alternatives exist. Unfortunately, several code patterns that are prevalent in Spring Boot and Mockito - and likely elsewhere, but that’s outside the scope of this article - were built on Java’s tendency to permit uninstantiated field declarations. This is where the ease of transcribing Java code to Kotlin code becomes a double-edged sword: it’s easy to simply move the Java code over to a Kotlin file, slap the lateinit
keyword on a field that hasn’t been directly instantiated in the Java code, and call it a day. Take, for instance, a test class that:
- Auto-wires a bean that’s been registered in the Spring Boot component ecosystem
- Injects a configuration value that’s been loaded in the Spring Boot environment
- Mocks a field’s value and then passes said mock into another field’s object
- Creates an argument captor for validating arguments that are passed to specified functions during the execution of one or more test cases
- Instantiates a mocked version of a bean that has been registered in the Spring Boot component ecosystem and passes it to a field in the test class
Here is the code for all of these points put together:
@SpringBootTest
@ExtendWith(MockitoExtension::class)
@AutoConfigureMockMvc
class FooTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Value("\${foo.value}")
private lateinit var fooValue: String
@Mock
private lateinit var otherFooRepo: OtherFooRepo
@InjectMocks
private lateinit var otherFooService: OtherFooService
@Captor
private lateinit var timestampCaptor: ArgumentCaptor<Long>
@MockBean
private lateinit var fooRepo: FooRepo
// Tests below
}
A better world is possible! Here are ways to avoid each of these constructs so that one can write “good” idiomatic Kotlin code while still retaining the use of auto wiring, object mocking, and argument capturing in the tests.
Becoming “Punctual”
Note: The code in these examples uses Java 17, Kotlin 1.9.21, Spring Boot 3.2.0, and Mockito 5.7.0.
@Autowired/@Value
Both of these constructs originate in the historic practice of having Spring Boot inject the values for the fields in question after their containing class has been initialized. This practice has since been deprecated in favor of declaring the values that are to be injected into the fields as arguments for the class’s constructor. For example, this code follows the old practice:
@Service
class FooService {
@Autowired
private lateinit var fooRepo: FooRepo
@Value("\${foo.value}")
private lateinit var fooValue: String
}
It can be updated to this code:
@Service
class FooService(
private val fooRepo: FooRepo,
@Value("\${foo.value}")
private val fooValue: String,
) {
}
Note that aside from being able to use the val
keyword, the @Autowired
annotation can be removed from the declaration of fooRepo
as well, as the Spring Boot injection mechanism is smart enough to recognize that fooRepo
refers to a bean that can be instantiated and passed in automatically. Omitting the @Autowired
annotation isn’t possible for testing code: test files aren't actually a part of the Spring Boot component ecosystem, and thus, won’t know by default that they need to rely on the auto-wired resource injection system - but otherwise, the pattern is the same:
@SpringBootTest
@ExtendWith(MockitoExtension::class)
@AutoConfigureMockMvc
class FooTest(
@Autowired
private val mockMvc: MockMvc,
@Value("\${foo.value}")
private val fooValue: String,
) {
@Mock
private lateinit var otherFooRepo: OtherFooRepo
@InjectMocks
private lateinit var otherFooService: OtherFooService
@Captor
private lateinit var timestampCaptor: ArgumentCaptor<Long>
@MockBean
private lateinit var fooRepo: FooRepo
// Tests below
}
@Mock/@InjectMocks
The Mockito extension for JUnit allows a developer to declare a mock object and leave the actual mock instantiation and resetting of the mock’s behavior - as well as injecting these mocks into the dependent objects like otherFooService
in the example code - to the code within MockitoExtension
. Aside from the disadvantages mentioned above about being forced to use mutable objects, it poses quite a bit of “magic” around the lifecycle of the mocked objects that can be easily avoided by directly instantiating and manipulating the behavior of said objects:
@SpringBootTest
@ExtendWith(MockitoExtension::class)
@AutoConfigureMockMvc
class FooTest(
@Autowired
private val mockMvc: MockMvc,
@Value("\${foo.value}")
private val fooValue: String,
) {
private val otherFooRepo: OtherFooRepo = mock()
private val otherFooService = OtherFooService(otherFooRepo)
@Captor
private lateinit var timestampCaptor: ArgumentCaptor<Long>
@MockBean
private lateinit var fooRepo: FooRepo
@AfterEach
fun afterEach() {
reset(otherFooRepo)
}
// Tests below
}
As can be seen above, a post-execution hook is now necessary to clean up the mocked object otherFooRepo
after the test execution(s), but this drawback is more than made up for by otherfooRepo
and otherFooService
now being immutable as well as having complete control over both objects’ lifetimes.
@Captor
Just as with the @Mock
annotation, it’s possible to remove the @Captor
annotation from the argument captor and declare its value directly in the code:
@SpringBootTest
@AutoConfigureMockMvc
class FooTest(
@Autowired
private val mockMvc: MockMvc,
@Value("\${foo.value}")
private val fooValue: String,
) {
private val otherFooRepo: OtherFooRepo = mock()
private val otherFooService = OtherFooService(otherFooRepo)
private val timestampCaptor: ArgumentCaptor<Long> = ArgumentCaptor.captor()
@MockBean
private lateinit var fooRepo: FooRepo
@AfterEach
fun afterEach() {
reset(otherFooRepo)
}
// Tests below
}
While there’s a downside in that there’s no mechanism in resetting the argument captor after each test (meaning that a call to getAllValues()
would return artifacts from other test cases’ executions), there’s the case to be made that an argument captor could be instantiated as an object within only the test cases where it is to be used and done away with using an argument captor as a test class’s field. In any case, now that both @Mock
and @Captor
have been removed, it’s possible to remove the Mockito extension as well.
@MockBean
A caveat here: the use of mock beans in Spring Boot tests could be considered a code smell, signaling that, among other possible issues, the IO layer of the application isn’t being properly controlled for integration tests, that the test is de-facto a unit test and should be rewritten as such, etc. Furthermore, too much usage of mocked beans in different arrangements can cause test execution times to spike. Nonetheless, if it’s absolutely necessary to use mocked beans in the tests, a solution does exist for converting them into immutable objects. As it turns out, the @MockBean
annotation can be used not just on field declarations, but also for class declarations as well. Furthermore, when used at the class level, it’s possible to pass in the classes that are to be declared as mock beans for the test in the value array for the annotation. This results in the mock bean now being eligible to be declared as an @Autowired
bean just like any “normal” Spring Boot bean being passed to a test class:
@SpringBootTest
@AutoConfigureMockMvc
@MockBean(value = [FooRepo::class])
class FooTest(
@Autowired
private val mockMvc: MockMvc,
@Value("\${foo.value}")
private val fooValue: String,
@Autowired
private val fooRepo: FooRepo,
) {
private val otherFooRepo: OtherFooRepo = mock()
private val otherFooService = OtherFooService(otherFooRepo)
private val timestampCaptor: ArgumentCaptor<Long> = ArgumentCaptor.captor()
@AfterEach
fun afterEach() {
reset(fooRepo, otherFooRepo)
}
// Tests below
}
Note that like otherFooRepo
, the object will have to be reset in the cleanup hook. Also, there’s no indication that fooRepo
is a mocked object as it’s being passed to the constructor of the test class, so writing patterns like declaring all mocked beans in an abstract class and then passing them to specific extending test classes when needed runs the risk of “out of sight, out of mind” in that the knowledge that the bean is mocked is not inherently evident. Furthermore, better alternatives to mocking beans exist (for example, WireMock and Testcontainers) to handle mocking out the behavior of external components.
Conclusion
Note that each of these techniques is possible for code written in Java as well and provides the very same benefits of immutability and control of the objects’ lifecycles. What makes these recommendations even more pertinent to Kotlin is that they allow the user to align more closely with Kotlin’s design philosophy. Kotlin isn’t simply “Java with better typing:" It’s a programming language that places an emphasis on reducing common programming errors like accidentally accessing null pointers as well as items like inadvertently re-assigning objects and other pitfalls. Going beyond merely looking up the tools that are at one’s disposal in Kotlin to finding out why they’re available in the form that they exist will yield dividends of much higher productivity in the language, less risks of trying to fight against the language instead of focusing on solving the tasks at hand, and, quite possibly, making writing code in the language an even more rewarding and fun experience.
Opinions expressed by DZone contributors are their own.
Comments