DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Enterprise AI Trend Report: Gain insights on ethical AI, MLOps, generative AI, large language models, and much more.

2024 Cloud survey: Share your insights on microservices, containers, K8s, CI/CD, and DevOps (+ enter a $750 raffle!) for our Trend Reports.

PostgreSQL: Learn about the open-source RDBMS' advanced capabilities, core components, common commands and functions, and general DBA tasks.

AI Automation Essentials. Check out the latest Refcard on all things AI automation, including model training, data security, and more.

Related

  • Spring Boot - Unit Test your project architecture with ArchUnit
  • Top 10 Advanced Java and Spring Boot Courses for Full-Stack Java Developers
  • Exploring Hazelcast With Spring Boot
  • Testcontainers With Kotlin and Spring Data R2DBC

Trending

  • Spring Strategy Pattern Example
  • Test Parameterization With JUnit 5.7: A Deep Dive Into @EnumSource
  • Effective Communication Strategies Between Microservices: Techniques and Real-World Examples
  • Power BI: Transforming Banking Data
  1. DZone
  2. Coding
  3. Frameworks
  4. Be Punctual! Avoiding Kotlin’s lateinit In Spring Boot Testing

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.

By 
Severn Everett user avatar
Severn Everett
·
Feb. 15, 24 · Tutorial
Like (3)
Save
Tweet
Share
5.3K Views

Join the DZone community and get the full member experience.

Join For Free

A 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:

Java
 
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:

  1. Declare jdbcOperations as var, assign a garbage value to it in its declaration, then assign the “real” value to the field in beforeAll(): 
Kotlin
 
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():

Kotlin
 
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:

Kotlin
 
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:

Java
 
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 immutable val, 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 use lateinit 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:

Kotlin
 
@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:

Kotlin
 
@Service
class FooService {
   @Autowired
   private lateinit var fooRepo: FooRepo
   @Value("\${foo.value}")
   private lateinit var fooValue: String
}


It can be updated to this code:

Kotlin
 
@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:

Kotlin
 
@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:

Kotlin
 
@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:

Kotlin
 
@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:

Kotlin
 
@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.

Spring Framework Java (programming language) Kotlin (programming language) Spring Boot unit test

Opinions expressed by DZone contributors are their own.

Related

  • Spring Boot - Unit Test your project architecture with ArchUnit
  • Top 10 Advanced Java and Spring Boot Courses for Full-Stack Java Developers
  • Exploring Hazelcast With Spring Boot
  • Testcontainers With Kotlin and Spring Data R2DBC

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: