Skip to main content

A Tester's First Taste of Kotlin

· 11 min read
Attila Fazekas
Kolibrium maintainer

Introduction: Why Kotlin is a Great Fit for Test Automation

Kotlin is a modern, pragmatic language that runs on the JVM and works seamlessly with the entire Java test ecosystem. For testers, that means you can keep using familiar tools like Selenium, JUnit, TestNG, Appium, RestAssured, or Playwright for JVM — but write your tests in a language that removes much of Java's ceremony and friction. Kotlin's concise syntax, expressive constructs, and built-in safety features let you focus more on the behavior you want to test and less on boilerplate.

Many testers begin with Java because that's what their automation framework or company uses; Kotlin builds on that foundation rather than replacing it. You write fewer lines of code, avoid accidental null-safety issues, get cleaner page objects, and can express test intent more clearly. These benefits apply strongly to test automation, where readability and maintainability matter just as much as correctness. In this post, you'll get a first taste of what Kotlin looks like in a typical Selenium test and see how many of Kotlin's strengths naturally surface even in a small example.


What You'll Need

To follow along with the examples in this post, you'll need:

  • JDK 11 or higher installed on your machine
  • An IDE with Kotlin support (IntelliJ IDEA is recommended, but VS Code with the Kotlin extension works too)
  • Gradle or Maven for dependency management

Gradle project build file (build.gradle.kts):

plugins {
kotlin("jvm") version "2.2.21"
}

group = "learning-kotlin"
version = "1.0-SNAPSHOT"

repositories {
mavenCentral()
}

dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:6.0.1")
testImplementation("org.seleniumhq.selenium:selenium-java:4.38.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
useJUnitPlatform()
}

kotlin {
jvmToolchain(21)
}

1. A First Example: A Small Selenium Test in Kotlin

Let's start with a small, realistic example to demonstrate what Kotlin looks like in a typical test automation scenario. Even in this short snippet, Kotlin's expressiveness and reduction of boilerplate become immediately visible. We'll use a simple Selenium test that logs into a page and performs a basic assertion. Don't worry if a few details are unfamiliar — we'll go through the interesting parts right after the listing.

package dev.kolibrium.learning.kotlin._01_a_taste_of_kotlin

import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver

class LoginPage(private val driver: WebDriver) {
private val usernameInput = By.name("user-name")
private val passwordInput = By.id("password")
private val loginButton = By.name("login-button")

fun login(
username: String = "standard_user",
password: String = "secret_sauce",
) = with(driver) {
findElement(usernameInput).sendKeys(username)
findElement(passwordInput).sendKeys(password)
findElement(loginButton).click()
}
}

class SeleniumTest {
private lateinit var driver: WebDriver

@BeforeEach
fun setUp() {
driver = ChromeDriver().apply {
get("https://www.saucedemo.com")
}
}

@AfterEach
fun tearDown() {
driver.quit()
}

@Test
fun `taste of Kotlin`() {
LoginPage(driver).login()
assert(driver.title == "Swag Labs")
}
}

A note on assertions: This example uses Kotlin's built-in assert() for simplicity. In real test suites, you'd typically use an assertion library like AssertJ, Hamcrest, or a Kotlin-native option like Kotest assertions, which provide better error messages and a richer API. We'll explore assertion strategies in a future post.


2. Key Kotlin Features You Already Saw — with Java Comparisons

Even in this small example, Kotlin brings a number of conveniences that make test code cleaner, more expressive, and more maintainable. To highlight the difference, each Kotlin feature is shown together with its equivalent Java version. Let's walk through them briefly — no deep dives yet, just enough to build intuition for what makes Kotlin feel different.

Primary constructors keep classes compact

class LoginPage(private val driver: WebDriver)

Why this matters: Kotlin lets you declare and store constructor parameters in one line. No need to define fields, assign them manually, or repeat boilerplate — perfect for slim page objects.

Read-only properties with val

private val usernameInput = By.name("user-name")

Why this matters: val means "this won't change." Tests are full of things that shouldn't change (locators, drivers, URLs), so val makes intent clear and reduces accidental mutations.

Default parameter values

fun login(username: String = "standard_user", password: String = "secret_sauce")

Why this matters: In Java you must manually overload methods. Kotlin gives you defaults directly in the function signature: this allows your tests to call login() without passing credentials every time. You can still override them when needed — best of both worlds.

Trailing commas for multiline arguments

fun login(
username: String = "standard_user",
password: String = "secret_sauce",
)

Why this matters: Cleaner diffs, easier to reorder parameters, less friction when updating test utilities.

Scope functions (with, apply)

Kotlin includes small helper functions called "scope functions" that make object configuration and method calls cleaner. The two you see in this example serve different purposes:

apply configures an object and returns it – great for setup code:

ChromeDriver().apply {
get("https://www.saucedemo.com")
}

with lets you call methods on an object without repeating its name — useful when you need to perform multiple operations but don't need to return the object:

with(driver) {
findElement(usernameInput).sendKeys(username)
findElement(passwordInput).sendKeys(password)
findElement(loginButton).click()
}

When to use which: Use apply when you're configuring an object and want to return it (often during initialization). Use with when you want to operate on an object without the repetition but don't need it returned.

Expression-body functions keep things concise

fun login(...) = with(driver) { ... }

Why this matters: For short functions, you can skip curly braces and the return keyword entirely. Test helpers become very compact.

lateinit fits naturally with test lifecycle methods

private lateinit var driver: WebDriver

Why this matters: Kotlin's type system distinguishes between nullable and non-nullable types. Without lateinit, you'd have two choices: initialize the driver immediately (not possible if setup happens in @BeforeEach), or declare it as WebDriver? (nullable), which would force you to handle null checks every time you use it. lateinit tells the compiler "I promise to initialize this before I use it," letting you keep the non-nullable type while deferring initialization to your setup method. We'll explore Kotlin's null-safety system in depth in a future post.

Backticked test names improve readability

@Test
fun `taste of Kotlin`() { ... }

Why this matters: Kotlin allows spaces and punctuation in function names using backticks, producing expressive, human-readable test names — great for test reports.

Note: With JUnit's @DisplayName annotation the same can be achieved in Java. As a result, the report will show "taste of Kotlin".

Seamless interoperability with Java testing libraries

@Test
fun loginTest() { ... }

Why this matters: Annotations, frameworks, libraries, and dependencies behave exactly the same. You continue using Selenium, JUnit, TestNG, RestAssured, Playwright, etc., without modification.

Three more subtle Kotlin conveniences

1. No semicolons

Kotlin does not require semicolons at the end of statements:

driver.quit()

A small but surprisingly pleasant reduction in noise.

2. Default visibility is public

Because Kotlin defaults to public, you don't need to write it explicitly:

class SeleniumTest { ... }

Less boilerplate, same meaning.

3. Multiple classes per file

You can define as many classes as you want in a single Kotlin file, as seen with LoginPage and SeleniumTest.


3. Why These Features Matter for Test Automation

Kotlin isn't just "nicer Java." The language design directly benefits the way testers work and the kind of code they maintain.

Less boilerplate = faster test writing

Page objects, test fixtures, utility functions, and models become smaller and clearer. Less time on ceremony means more time on actual test logic.

More readable tests

Backticked function names, default parameters, expression bodies, and named arguments allow tests to read almost like prose. Readability matters when your audience includes testers, developers, and sometimes non-technical stakeholders.

A safer language for growing codebases

Null-safety is built into the type system, helping prevent the kinds of runtime errors (and flaky tests) that often arise from uninitialized values.

Great for building your own testing DSLs

Kotlin's extension functions, operator overloading, and higher-order functions let teams build expressive, domain-specific testing helpers. You can shape your test APIs around your own workflows.

Works perfectly with existing Java tools

Since Kotlin compiles to JVM bytecode, no toolchain changes are necessary. Selenium? Works. JUnit 5? Works. RestAssured? Works. Playwright for JVM? Works. Testers can adopt Kotlin incrementally without disrupting existing setups.


4. Digging Deeper (Preview of What's Coming Later)

This first example only scratches the surface of what Kotlin can do. Throughout this series, we'll explore language features that make test automation even more expressive and robust:

  • Kotlin's type system and how to elegantly deal with null values
  • Kotlin's standard library (scope) functions that dramatically cut down utility code
  • Using extension functions to build reusable, fluent test helpers
  • Writing clean page objects with minimal ceremony
  • Building small DSL-like functions to express test intent better
  • Choosing and using assertion libraries effectively in Kotlin

Each topic will include real test examples and as we progress, we'll continuously refine our code by following Kotlin's best practices.


5. Closing Thoughts

Kotlin is a modern, concise, and highly expressive language that fits naturally into the workflows of test automation engineers. Because it builds on top of the JVM, you keep all the libraries and tools you already rely on — while gaining syntax and features that make test code easier to write and much easier to maintain. This first example is just a small taste of what Kotlin offers; the real value becomes even clearer as test suites grow and complexity increases.