Even when our application passes all unit tests, it doesn’t mean that it will work as a whole system. One way of making sure that it works as a whole, is testing it from the user’s perspective. The easiest way of performing this type of test is manually testing your application. With large applications this can be very tedious and ineffective, since changing one backend feature can break the other one. Here comes test automation.
Selenium
Selenium is a suite of tools that allows its users to automate in-browser tests. It consists of three parts:
- WebDriver – a tool that interacts with browser
- IDE – a browser addon that allows testers to record test manually
- Grid – a tool that allows for running tests in parallel in different environments
Selenium also comes as a library in plethora of different languages including popular ones like Java, C#, Python. It also is included as a part of ScalaTest + Play library (as it’s based on ScalaTest + Selenium library).
Configuration
Before we will create some simple tests, we need to configure a few things. First we need to create a test spec, that extends a few traits:
PlaySpec
GuiceOneServerPerSuite
OneBrowserPerTest
– ensures that all tests will be run in separate browsersBeforeAndAfterEach
WithInMemoryDatabase
– it’s a custom trait that configures in memory database
Since we want to use a separate in memory database for our tests, we need to to override fakeApplication()
method, which provides an Application
instance. We want to configure it to use our database and not the default one. It should look similar to this:
override def fakeApplication(): Application = { GuiceApplicationBuilder() .in(Mode.Test) .configure("db.default.url" -> database.url, "db.default.driver" -> "org.h2.Driver", "db.default.username" -> "", "db.default.password" -> "", "toggle.rule-deployment.log-rule-id" -> true) .build() }
The database
value is provided by WithInMemoryDatabase
trait. It’s an instance of our database.
Next we will implement some helper function. It will parse a Call
to absolute URL string. Since we will run these test locally we can use localhost
as domain. This function should look like one presented below.
def absoluteURL(url: Call) = s"http://localhost:$port${url.toString}"
Configuring Selenium
Although Selenium comes pre-configured with ScalaTest + Play, we will need to make a few tweaks. In this configuration Selenium by default uses HtmlUnitDriver which has some problems with JS Rest Parameters and Bootstrap 5. This can be easily solved by changing the browser engine. We will be using Chrome engine.
The first thing that we’ll need to do is to add WebDriverManager, which takes care of most of configuration for us. We will need to add its dependency to build.sbt
"io.github.bonigarcia" % "webdrivermanager" % "5.2.0"
Next we need to override createWebDriver()
which is provided by OneBrowserPerTest
trait. Our version should look like this one:
override def createWebDriver(): WebDriver = { WebDriverManager.chromedriver().setup() val options = new ChromeOptions options.addArguments("--headless", "--window-size=1920,1200") new ChromeDriver(options) }
First line will setup ChromeDriver with the default options. Next we create our own options:
--headless
– runs Chrome in a windowless mode--window-size=1920,1200
First test
Now that we have everything configured, we can create first test. We will test if the user is redirected to the index page, after a successful login.
"Login view" must { "redirect to index on successful login" in { go to absoluteURL(routes.AuthController.login) emailField("email").value = users_email pwdField("password").value = users_password submit() eventually(pageTitle mustBe "Main page") } }
First we will go to
the login page. To make sure that our test will be passed even if we change the login URL, we use Call
from routes
and the previously defined helper function. Next we want to find an email input with attribute name=”email” on the page and fill it with the correct email address that corresponds with some user in our test database. Next we want to find a password field with attribute name=”password” and fill it with the user’s password. Next we want to submit this form. Since we need to wait for this form to be processed by the server, we need to use eventually()
. This will try to run a block of code, until it ends with success or until a predefined time passes. In our case, the block of code will check if page’s title is equal to “Main page” – since that’s the title of our index page.
We can create more automated tests that will check if certain text is present on a page, if some part is in the appropriate color or if the paginated list has an exact number of items. Since we do this in the browser we not only test if all specific functions run correctly, but we also check if all layers communicate properly and – what’s most important – if the user will see what we wanted him to see.