loading...
Cover image for E2E testing with Selenium and Kotlin

E2E testing with Selenium and Kotlin

chrisvasqm profile image Christian Vasquez Updated on ・9 min read

BE AWARE

This will be a looong post. So, grab a cup of coffee/tea and hang on tight!

Introduction

What is E2E?

End-To-End (E2E) testing is a technique used to test an entire flow as if we were an actual user by simulating their actions (clicks, pressing certain keys, typing into a field, etc) on the browser.

What is Selenium?

Selenium is an automation framework for web applications.

What is Kotlin?

Kotlin is a statically typed programming language made by Jetbrains that can be used for web, mobile and desktop applications on both the frontend and backend. We will be using it on the JVM.

What is love?

Baby don't hurt me...

Sorry, I had to do it!

Requirements

IDE

I'll be using the IntelliJ IDEA IDE, also made by Jetbrains.

From their download page, select the Community Edition (free).

IntelliJ IDEA download page

Selenium

Go to Selenium's download page and download the .jar file under the "Selenium Standalone Server" section.

Selenium download page

Selenium download link

NodeJS

This can be done via their website or via your operating system's terminal/command line (steps will be different depending on your OS).

After that, in order to verify that your NodeJS was installed successfully, run the following command in your terminal/command line:

npm -v

You should be able to see the version number, mine is 5.6.0 as I write this post.

Browser Drivers

With the npm installed on our machine, we can now install the browser driver that we need. For this case we will be using Google Chrome (here's the documentation if you want to use a different one by searching for "driver" on the right hand navigation bar).

Chromedriver

In order to install the chromedriver you will have to run the following command on your terminal/command line:

npm install chromedriver

Now double check that the installation was done successfully by running:

chromedriver -v

Awesome, but...

Where's the code, tho?

Where?!

Go ahead and open up IntelliJ IDEA and follow the basic setup steps.

Don't worry, take your time, I can wait.

Meet me at the "Welcome to IntelliJ IDEA" screen.

Welcome to IntelliJ IDEA

Select Create New Project, and choose Kotlin > Kotlin/JVM.

Create new project

Choose a project name, I chose selenium-kotlin.

Project name

Then, hit the Finish button.

Head over to the left pane (project explorer)

Project explorer

Project structure

We will create a few packages to match this structure:

selenium-kotlin    
│
└─── .idea
|    |
|    └─── ...
|
└─── src
|    │
|    └─── main
|    |    |
|    |    └─── page
|    |
|    └─── tests
|
└─── selenium-kotlin.iml

Now, we need to mark the main folder as a Sources root and the tests folder as a Test Sources root. You can achieve this by right clicking the package and selecting Mark Directory as > ...

Add Selenium to the project

Go to the File menu and select the Project Structure....

File menu

Select the Modules.

Project settings

Add the selenium jar to the project.

Add dependency

Look for the .jar file we downloaded earlier. It might be in your ./Downloads folder, unless you saved it somewhere else.

Click the OK button on the bottom right of the Project Structure window and you should now be able to see selenium under the External Libraries in your Project Explorer.

Create your first Page Object Model

We can finally start coding!

Page Object Model is a design pattern that helps us organize where all the WebElement are based on to which "Page" they belong to. This helps us reduce code duplication across different parts of our codebase.

Head over to your src/main/kotlin/page folder. In this package, we will create classes that represent a given page in our web applications.

GitHub Home Page

Like the one above that would be represented by GitHubHomePage.kt. So, go ahead and make it. Right click on the page folder, select New > Kotlin File/Class.

Your file will be (almost) empty:

package page

Go ahead and add the class definition:

class GitHubHomePage {

}

The first thing we need to do with our page is to add an open() method. Since we will want to visit it with a direct URL during our test.

class GitHubHomePage {

    fun open() {
        // Go to GitHub's home page
    }

}

In order to do this, we will need a WebDriver object, this is a class provided by Selenium that represents our browsers (Chrome, Firefox, Safari, Internet Explorer, etc) and all it's methods to control the browser.

We will need to pass it in inside our GitHubHomePage's constructor, and then use it inside our open() method by calling the WebDriver's method called get().

class GitHubHomePage(private val driver: WebDriver) {

    fun open() {
        driver.get("https://github.com/")
    }

}

As you can see, the get() method takes in a String value that will be GitHub's main page.

Note: the get() method actually calls navigate().to(), but get() is shorter :)

Now we can run our first test really quick to open a browser and then visit GitHub's website.

Create your first test

Place your cursor on top of the GitHubHomePage class name, use the Find Actions menu by pressing SHIFT + CTRL + A (for Windows) or SHIFT + CMD + A (for MacOS).

A search bar dialog will show up and type in Create test, then hit ENTER.

A new dialog will show up. Select TestNG as your Testing Library from the dropdown, there will be a warning message saying "TestNG library not found in the module". Don't worry, just press the Fix button right next to it.

Another window will show up and make sure you leave it with the "Use 'testng' from IntelliJ IDEA distribution" radio button active, hit OK, and then mark the 2 checkboxes: setUp/@Before and tearDown/@After.

It should look like this:

Create test dialog

Then, hit OK again.

This will create the test class for you inside the src/tests/kotlin/page directory with the setUp() and tearDown() methods.

The setUp() method will always run before each test, while the tearDown() will always run after each test.

We will use them to setup our browser, we will use Chrome this time, and we will make sure to always close all the browsers when the tests are done.

Back to the code!

Open the browser

Inside the setUp() method, let's create an instance of the ChomeDriver class.

class GitHubHomePageTest {
    private lateinit var driver: WebDriver

    @BeforeMethod
    fun setUp() {
        driver = ChromeDriver()
    }

    @AfterMethod
    fun tearDown() {
        // Close all browsers
    }
}

Go to GitHub's home page

Now let's create an instance of the GitHubHomePage class and call it's open() method.

class GitHubHomePageTest {
    private lateinit var driver: WebDriver
    private lateinit var gitHubHomePage: GitHubHomePage

    @BeforeMethod
    fun setUp() {
        driver = ChromeDriver()
        gitHubHomePage = GitHubHomePage(driver)
        gitHubHomePage.open()
    }

    @AfterMethod
    fun tearDown() {
        // Close all browsers
    }
}

Close the browser when your are done

Head over to the tearDown() method, and call de quit() method from the driver instance.

class GitHubHomePageTest {
    private lateinit var driver: WebDriver
    private lateinit var gitHubHomePage: GitHubHomePage

    @BeforeMethod
    fun setUp() {
        driver = ChromeDriver()
        gitHubHomePage = GitHubHomePage(driver)
        gitHubHomePage.open()
    }

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

Almost there!

Let's add a Test Function, remember how to use the Generate menu?

SHIFT + CTRL + A/SHIFT + CMD + A.

Type in Generate and hit ENTER.

Select the Test Function.

Give it a name like emptyTest and put a placeholder comment for now. We just want to see if we can open the browser and go to the home page.

The easiest way to run our emptyTest is by clicking the green play button on the left side of the editor pane. Right next to the line #21.

Run test method

EmptyTest passed

yeah boi

Let's make a real test

How about testing if we can look for a GitHub username and access their profile?

Sounds fairly easy, right?

Refactor emptyTest

The first thing we'll need to do is to rename our emptyTest method.

How about... searchForUsername? Good.

Now, where is the search bar located? It's right there in the home page.

So let's add a method that will take a String, type it into the searchBox and submit it. It should look like:

fun searchFor(query: String) {
    val searchBox = driver.findElement(By.xpath("//input[@placeholder='Search GitHub']"))
    searchBox.sendKeys(query, Keys.ENTER)
}

Wait, wait, wait...

Where did all this findElement(), By, xpath, sendKeys() and Keys come from!?

Ok, let me explain:

findElement() is a method from driver that takes a By.<something> as an argument. If successful, it returns a WebElement; otherwise, it will throw a NoSuchElementException.

By.xpath() takes in a String argument that will help us locate an element on the page, in this case we will get whatever <input> that has a placeholder attribute that matches "Search GitHub" exactly. Other examples are By.id, By.cssSelector, By.className, etc.

sendKeys() is a method from the WebElement class provided by Selenium that takes an argument of type CharSequence, which means we can pass in a char, String or any values from the Keys enum class, like the Keys.ENTER which will NOT type in the word "ENTER", but rather simulate that we pressed the ENTER key on our keyboard, and thus submitting the query in our example.

Cool, right? But we are not done yet

Now we gotta filter our results from the search to only see usernames and then click the username we are looking for.

Since this page has elements that are only part of the "searching" flow we will make another Page class and call it GitHubSearchPage (Remember to do it in the src/main/kotlin/page package).

class GitHubSearchPage(private val driver: WebDriver) {

}

Add the WebElement for the users filter and the filterByUsers() method that will click() it.

fun filterByUsers() {
    val usersFilter = driver.findElement(By.xpath("//a[text() = 'Users']"))
    usersFilter.click()
}

Add another WebElement for the user profile link and an enterUserProfile() method that will click() it.

fun enterUserProfile() {
    val userProfileLink = driver.findElement(By.xpath("//span[text() = 'Christian Vasquez']/../a/em"))
    userProfileLink.click()
}

I'll also add a open() method so we can go straight to the search page in case we need later.

The entire class should look like:

class GitHubSearchPage(private val driver: WebDriver) {

    fun open() {
        driver.get("https://github.com/search?")
    }

    fun filterByUsers() {
        val usersFilter = driver.findElement(By.xpath("//a[text() = 'Users']"))
        usersFilter.click()
    }

    fun enterUserProfile() {
        val userProfileLink = driver.findElement(By.xpath("//span[text() = 'Christian Vasquez']/../a/em"))
        userProfileLink.click()
    }

}

Let's start using our new GitHubSearchPage in our searchForUsername() test function.

@Test
fun searchForUsername() {
    gitHubHomePage.searchFor("chrisvasqm")
    gitHUbSearchPage = GitHubSearchPage(driver)
    gitHUbSearchPage.filterByUsers()
    gitHUbSearchPage.enterUserProfile()
}

Now let's check that we got redirected to the "www.github.com/chrisvasqm" URL correctly by adding the following line to our searchForUsername() method:

assertTrue(driver.currentUrl == "https://github.com/chrisvasqm")

The entire test function should look like this now:

@Test
fun searForUsername() {
    gitHubHomePage.searchFor("chrisvasqm")
    gitHUbSearchPage = GitHubSearchPage(driver)
    gitHUbSearchPage.filterByUsers()
    gitHUbSearchPage.enterUserProfile()

    assertTrue(driver.currentUrl == "https://github.com/chrisvasqm")
}

Let's try to run our test now...

BOOM!

We got a NoSuchElementException, but worry you must not.

What happened was that our userProfileLink WebElement would not be present right away after we click the usersFilter element.

In order to fix this we can use 2 classes provided by Selenium: PageFactory and AjaxElementLocatorFactory.

The way we do it is by using the init {...} block that Kotlin has (it's similar to a constructor, but used only for initialization).

init {
    PageFactory.initElements(AjaxElementLocatorFactory(driver, 15), this)
}

This line of code makes sure to initialize our properties so that it waits a maximum of 15 seconds before it throws a NoSuchElementException.

Which properties?

Good question!

Remember we were doing this?

val element = driver.findElement(By.xpath("VALUE"))

We will replace it with:

@FindBy(xpath = "VALUE")
private lateinit var element: WebElement

Whenever we use the PageFactory + AjaxElementLocatorFactory combo along with the @FindBy annotation, we will get the benefit of the timeout of 15 seconds I mentioned before.

Let's refactor our GitHubSearchPage class then

class GitHubSearchPage(private val driver: WebDriver) {

    @FindBy(xpath = "\"//a[text() = 'Users']\"")
    private lateinit var usersFilter: WebElement

    @FindBy(xpath = "//span[text() = 'Christian Vasquez']/../a/em")
    private lateinit var userProfileLink: WebElement

    init {
        PageFactory.initElements(AjaxElementLocatorFactory(driver, 15), this)
    }

    fun open() {
        driver.get("https://github.com/search?")
    }

    fun filterByUsers() {
        usersFilter.click()
    }

    fun enterUserProfile() {
        userProfileLink.click()
    }

}

Rerun the test 🤞

All tests passed

We finally made it!

doge

I hope this post taught you something new, if you didn't know about Selenium already.

I know I didn't go deep into why I did some things that are part of Kotlin itself, but that might be a topic for another post in the future ;)

Dunno, maybe this will push you into trying it out by yourself for other things!

Learn more

Here is another link that you may find useful to learn more about Selenium.

And in case you may want to learn about an alternative, here's a guide on how to do E2E testing with TestCafe using JavaScript and the VS Code editor.

Discussion

pic
Editor guide
Collapse
erdelyizsolt profile image
Erdelyi Zsolt

Hy Cristian,
thank you for your post is very good information for a beginner. Do you have a github repo for this project?
I tried to apply your model for my project but a get error all time. Thanks.

Collapse
chrisvasqm profile image
Christian Vasquez Author

Hey Erdelyi,

I'm glad you enjoyed it 😁.

Here's the link to the repository: github.com/chrisvasqm/intro-unit-t...

Let me know if you have any other issues.

Collapse
erdelyizs_ profile image
erdelyizs_

Thank you very much ( Erdelyi :)

Collapse
purusottamnath09 profile image
purusottamnath09

Follow this link to know more about Selenium ...
tequality.tech/course/selenium-web...