Kotlin is a new programming language, which is many ways is the successor to Java. The compiled code runs on the JVM, it is supported by Java IDEs, and its syntax is very similar to Java. It is the officially preferred language for Android app development, and a friend of mine is even rewriting his team's backend API in Kotlin.
Kotlin has many features which clean up some of the longstanding issues with Java, and also just gives a fresh start, without needing to support legacy decisions and APIs.
What many readers may not know, is that Kotlin code can be easily integrated into a Java project. A Java project can have some code files in Kotlin, a Kotlin program can import Java libraries, and with a little extra work, Java programs can import Kotlin libraries. This allows for a gradual transition, rather than the risks of a complete rewrite.
In this article, we'll take a simple Appium Java test, and convert it to Kotlin in just a few minutes!
In my specific case, I started with our code sample from two weeks ago. I'm using IntelliJ IDEA, and our project was already set up to use Gradle. The only difficulty I ran into is that the IDE's Kotlin plugins only work with gradle version 4.1 and up. Our project was using gradle version 4.0, so I had to update first, by running the following code:
./gradlew wrapper --gradle-version 4.10.3
I then added a new file next to our existing tests, but gave it a name ending in .kt
(for Kotlin) instead of .java
.
As soon as I had finished renaming the file, the IDE displayed a banner prompting me to configure Kotlin. When I clicked on it, the IDE automatically added some boilerplate to the build.gradle
file to add Kotlin support.
I pasted the following "hello world" code into the file, and then restarted the IDE, clearing all caches.
fun main() {
println("hello world")
}
As soon as the IDE came back online, this code was parsed successfully and a green "play button" arrow was displayed next to the main
function defined in the code. Clicking this icon to run, the words "hello world" were printed to the console.
Great! We have Kotlin code running in our project, now let's demonstrate a simple JUnit test.
All it takes is importing JUnit, adding the usual @Test
annotation to our method, and putting our method into a class.
import org.junit.Assert.assertEquals
import org.junit.Test
class Tests {
@Test
fun mainTest() {
assertEquals("hello", "hello")
}
}
Now, running the code by clicking on the green play button icons, runs the tests and output is displayed the way unit tests are normally viewed in the IDE.
Having demonstrated to ourselves that we can run a basic test, let's convert our Appium test from Java to Kotlin. This is a chance to highlight a built-in feature which really amazed me.
We go into the Java test file, highlight all the code, COPY. Now in our Kotlin file, PASTE:
And 95% of the work is done for us, leaving us with perfectly readable code.
The test file won't actually compile though. We picked a good example that brought up a case that involves some manual intervention. The firstTest
static variable from our original class has been converted to a "companion object" which is how Kotlin implements static class members. Kotlin is a different language after all.
The compiler complains about our use of firstTest
. This is related to the most obvious difference between Java and Kotlin at first glance, and also my favorite feature so far. Kotlin uses a concept called "nullable types", where a variable of any class cannot be null. In order to have a null value, the variable type must be set to the "nullable" version of the type, indicated by following the class name with a ?
. So a String
variable can never be null, while a String?
variable can be null or a String. This applies for every class. If you define a method which takes a String
parameter, you never need to check if it is null before you use it, because it's guaranteed to not be null.
There's a lot more to this concept, including some interesting syntax and shortcuts for null-checking, and there are plenty of articles explaining it in detail online, as well as the Kotlin documentation. This concept has existed for a long while, and is able to prevent many null reference exceptions and other issues. Both Swift and Kotlin implement it.
Our copy-pasted code fails because we are attempting to use the not operator !
on a variable of type Boolean?
and the compiler is complaining that the Boolean?
could be null, and it isn't able to calculate the results of !null
. Since the code sets a value for isFirst
at the declaration site, we can convert it to the non-nullable type Boolean
, and our code now compiles.
And tests pass!
The Java Appium client worked from the Kotlin code with no problem.
Our test was pretty simple, but I hope you consider learning Kotlin and experiment with converting your code. As time moves on, we will see if Kotlin adoption grows, but hopefully this will allay your fears about migrating. It can be done easily and one file at a time.
Below is the full Kotlin test file, which can also be found in our repository of examples.
import org.junit.Test
import io.appium.java_client.MobileElement
import io.appium.java_client.ios.IOSDriver
import org.junit.After
import org.junit.Before
import org.openqa.selenium.remote.DesiredCapabilities
import java.io.IOException
import java.net.URL
import junit.framework.TestCase.assertEquals
// Converted test "Edition077_Tuning_WDA_Startup" from Java to Kotlin just by copy-pasting.
// the `firstTest` variable was manually changed from type Boolean? to Boolean, in order to compile.
// A few comments have been cleaned up, just to make this example easier to use and read.
class Edition078_Converting_A_Test_To_Kotlin {
private val APP : String = "https://github.com/cloudgrey-io/the-app/releases/download/v1.9.0/TheApp-v1.9.0.app.zip";
private var driver: IOSDriver<*>? = null
@Before
@Throws(IOException::class)
fun setUp() {
val caps = DesiredCapabilities()
caps.setCapability("platformName", "iOS")
caps.setCapability("platformVersion", "12.2")
caps.setCapability("deviceName", "iPhone Xs")
caps.setCapability("automationName", "XCUITest")
caps.setCapability("app", APP)
caps.setCapability("noReset", true)
caps.setCapability("udid", "009D8025-28AB-4A1B-A7C8-85A9F6FDBE95")
caps.setCapability("bundleId", "io.cloudgrey.the-app")
if ((!firstTest)) {
caps.setCapability("webDriverAgentUrl", "http://localhost:8100")
}
driver = IOSDriver<MobileElement>(URL("http://localhost:4723/wd/hub"), caps)
}
@After
fun tearDown() {
firstTest = false
try {
driver!!.quit()
} catch (ign: Exception) {
}
}
@Test
fun testA() {
assertEquals(1, 1)
}
@Test
fun testB() {
assertEquals(1, 1)
}
@Test
fun testC() {
assertEquals(1, 1)
}
@Test
fun testD() {
assertEquals(1, 1)
}
@Test
fun testE() {
assertEquals(1, 1)
}
@Test
fun testF() {
assertEquals(1, 1)
}
companion object {
private var firstTest: Boolean? = true
}
}