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).
Selenium
Go to Selenium's download page and download the .jar file under the "Selenium Standalone Server" section.
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?
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.
Select Create New Project
, and choose Kotlin > Kotlin/JVM
.
Choose a project name, I chose selenium-kotlin
.
Then, hit the Finish
button.
Head over to the left pane (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...
.
Select the Modules
.
Add the selenium jar to the project.
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.
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 callsnavigate().to()
, butget()
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:
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.
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...
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 🤞
We finally made it!
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.