Part 2: UI Test Automation in Elixir with Wallaby
Wallaby is a functional testing library for Phoenix and Elixir web applications. It allows you to write and run automated tests for your application’s UI, ensuring that it works as expected. With Wallaby, you can test the interactions and behaviors of your application’s UI components, including buttons, links, forms, and more. It is easy to use and integrates well with other testing tools, making it an excellent choice for testing UI in Elixir applications.
Setting Up Wallaby for Your Elixir Application
Setting up Wallaby for your Elixir application is straightforward. First, you need to add Wallaby to your project’s dependencies in your mix.exs file. Next, you need to create a test/support/conn_case.ex file, which will contain the setup and teardown code for your tests. Finally, you need to create a test file in the test directory and start writing your tests.
def deps do [ {:wallaby, "~> 0.30.0", runtime: false, only: :test} ] end
Unlike the cabbage dependency from earlier, the only difference is runtime: false.
The runtime: false option specifies that this dependency should only be used during the test environment and not during runtime. This is because wallaby is a testing tool, and should not be included in the application’s runtime environment.
Configure the driver:
# Chrome - default config :wallaby, driver: Wallaby.Chrome, screenshot_on_failure: true, otp_app: :hospital_management, screenshot_dir: "test/screenshots/"
Wallaby.Chrome – sets the default driver, which means that it will use the Chrome web browser to interact with the application being tested.
screenshot_on_failure: true – specifies that a screenshot of the browser window should be taken and saved to disk when a test fails.
otp_app: :hospital_management – specifies the OTP application name for the project, which is used to find the config.exs file for the project.
screenshot_dir: “test/screenshots/” – specifies the directory where the screenshots should be saved.
Then ensure that Wallaby is started in your test_helper.exs:
{:ok, _} = Application.ensure_all_started(:wallaby)
This code uses the Application module in Elixir to ensure that the :wallaby OTP application is started before continuing execution. The :wallaby application is responsible for starting the browser driver that will be used by Wallaby to simulate user interactions with a web application.
The Application.ensure_all_started/1 function is called with the argument :wallaby, which is the name of the OTP application to be started. If the application is already running, this function has no effect, but if it is not running, it will be started.
The function returns a tuple containing either {:ok, pid} or {:error, reason} depending on whether the application was successfully started or not. In this case, the code is using pattern matching to capture only the :ok atom from the tuple and ignores the second element of the tuple.
If the :wallaby application fails to start for any reason, the function will return a tuple with the :error atom as the first element, which will cause the pattern matching to fail and an exception will be raised. This is why it’s important to use pattern matching, in this case, to ensure that the application was started successfully before continuing execution.
Make sure that the sandbox is enabled:
config :hospital_management_web, :sandbox, Ecto.Adapters.SQL.Sandbox
A sandbox is a way to create a temporary database environment for testing purposes. In this case, the Ecto.Adapters.SQL.Sandbox module is being used to set up the sandbox. The config/3 function from the Kernel module is being called to configure the :sandbox environment for the :hospital_management_web application.
The configuration options for the sandbox can be specified as a keyword list. In this case, the only option being set is Ecto.Adapters.SQL.Sandbox, which tells Ecto to use the Ecto.Adapters.SQL.Sandbox module to create the sandbox. The :sandbox environment is typically used for testing, so this configuration will be included in a config/test.exs file for the application.
Finally, in your test_helper.exs you can provide some configuration to Wallaby. At minimum, you need to specify a :base_url, so Wallaby knows how to resolve relative paths.
Application.put_env(:wallaby, :base_url, HospitalManagementWeb.Endpoint.url)
This is setting a configuration option for the Wallaby testing framework in the current Elixir application. Specifically, it uses the Application.put_env/3 function to set the :base_url option for the :wallaby application.
The :base_url option is used to specify the base URL for the web application that is being tested with Wallaby. In this case, the value being set for the option is the result of calling the HospitalManagementWeb.Endpoint.url function. This function is likely defined in the Phoenix web framework and returns the URL for the current endpoint.
By setting the :base_url option for the :wallaby application, Wallaby will use this URL as the base URL for all of its tests. This means that any relative URLs used in the tests will be relative to this base URL.
Writing Test Scenarios with Wallaby
Writing test scenarios with Wallaby is straightforward. Wallaby supports a wide range of test scenarios, including clicking buttons, filling out forms, and checking that elements are displayed correctly. With Wallaby, you can easily test the interactions and behaviors of your application’s UI components, ensuring that they work as expected for your users.
In this scenario, the patient is registered, and their access to the UI is achieved using login credentials. The appointment scheduling page will show the fake test data available to the patient to select from. The confirmation of the appointment will be with fake test data, and the patient should receive a confirmation of the appointment details via a mock email or SMS service rather than an actual email or SMS service.
Feature: Hospital Management Service This feature describes the capability of the Hospital Management Service to allow patients to schedule appointments via the user interface (UI). Scenario: Schedule an appointment online Given the patient has access to the Hospital Management application When the patient navigates to the appointment scheduling page And selects the date and time for their appointment And selects the doctor they wish to see And confirms the details of their appointment Then the appointment should be successfully scheduled And the patient should receive a confirmation of the appointment details via email or SMS.
Let’s implement this scenario with Elixir:
defgiven ~r/^the patient has access to the Hospital Management application$/, %{session: session} = _state do # Perform any necessary setup, such as logging in or creating a test patient account session = session |> visit("/") |> fill_in("username", with: "testuser") |> fill_in("password", with: "testpassword") |> click_button("Log In") |> assert_text("Welcome, Test User") {:ok, %{session: session}} end
It first navigates to the homepage, fills in the “username” and “password” fields, clicks the “Log In” button, and then checks that the page displays the expected welcome message. Finally, it returns an {:ok, %{session: session}} tuple with the updated session.
defwhen ~r/^the patient navigates to the appointment scheduling page$/, %{session: session} = state do session = session |> visit("/appointments/new") |> assert_title("Schedule an Appointment") {:ok, %{session: session}} end
In the implementation, the session is first used to visit the “/appointments/new” page, and then assert that the title of the page is “Schedule an Appointment”. The assert_title/2 function is used for this purpose, which takes two arguments: the session, and the expected title. Finally, the updated session is returned as part of the response, along with the state, wrapped in a tuple: “{:ok, %{session: session}}“.
defwhen ~r/^the patient selects the date and time for their appointment$/, %{ appointment_date: appointment_date, appointment_time: appointment_time, session: session } = state do session = session |> fill_in("appointment_date", with: appointment_date) |> fill_in("appointment_time", with: appointment_time) {:ok, %{session: session}} end
This step uses the fill_in function to populate the “appointment_date” and “appointment_time” fields on the appointment scheduling form with the values provided in the appointment_date and appointment_time variables. These values were extracted from the Gherkin step using a regex and passed as part of the state. The updated session is returned as part of the state.
defwhen ~r/^the patient selects the doctor they wish to see$/, %{doctor_name: doctor_name, session: session} = state do session = session |> select(doctor_name, from: "doctor_id") {:ok, %{session: session}} end
This step is part of an appointment scheduling process, and it handles the action of selecting a doctor that the patient wishes to see. The doctor_name and session are passed as parameters to this function from the previous step.
The doctor_name is the name of the doctor that the patient wishes to see, and it is used to select the corresponding doctor from a dropdown menu using the select function provided by Wallaby. The from parameter is used to identify the dropdown menu element where the doctor’s name is selected.
The updated session with the selected doctor is returned as part of the state in the response.
defwhen ~r/^the patient confirms the details of their appointment$/, %{session: session} = state do session = session |> click_button("Confirm Appointment") |> assert_text("Appointment scheduled successfully.") {:ok, %{session: session}} end
This step corresponds to the “And confirms the details of their appointment” step. In this step, we are simulating the patient confirming the details of their appointment by clicking on the “Confirm Appointment” button in the UI. After clicking the button, we assert that the confirmation message “Appointment scheduled successfully” is displayed in the UI by calling the assert_text function. The session parameter is passed along with the state, and we are returning the updated state containing the updated session value.
defthen ~r/^the appointment should be successfully scheduled$/, %{session: session} = state do # Assertion check to verify that the appointment is properly saved and displayed on the patient's appointment list session |> assert_text("Appointment scheduled successfully.") {:ok, %{session: session}} end
This step verifies that the appointment scheduling was successful by checking if the text “Appointment scheduled successfully.” is present on the page. This assertion confirms that the appointment creation was successful and no error occurred during the process. If the text is not present, the test will fail, and the test runner will display an appropriate error message.
defthen ~r/^the appointment should be successfully scheduled$/, %{session: session} = state do # Verify that the appointment is properly saved and displayed on the patient's appointment list assert session |> visit("/appointments") |> has_table_row?(expected_row: ["Doctor Name", "Appointment Date and Time", "Appointment Type"]) {:ok, %{session: session}} end
This step verifies that the appointment is properly saved and displayed on the patient’s appointment list. It first visits the “/appointments” page, then asserts that a table row with expected values (“Doctor Name”, “Appointment Date and Time”, “Appointment Type”) exists on the page using the has_table_row? function. If the assertion passes, it returns {:ok, %{session: session}} with the updated session.
Pros and Cons:
Pros:
- Improved collaboration: Utilizing Gherkin tests for UI testing with Wallaby facilitates better collaboration with non-technical stakeholders through its easy-to-read plain text format.
- Clear and concise requirements: Gherkin’s “Given-When-Then” format in frontend tests provides clear and well-outlined requirements for each scenario, aiding in comprehensive test coverage and understanding.
- Modular tests: Gherkin tests can be modular in the frontend, allowing for reuse across different UI testing scenarios and projects, resulting in time and effort savings.
- Supports test automation: Gherkin tests in the frontend can be automated using tools like Cucumber and Wallaby, enabling automated execution of repetitive tests and improved test coverage.
Cons:
- Additional complexity: Implementing Gherkin tests in the frontend can introduce additional complexity, such as defining step definitions, managing feature files, and keeping tests in sync with application changes.
- Steep learning curve: Writing and maintaining Gherkin tests for frontend/UI testing may require a certain level of technical expertise, which can be a barrier for non-technical team members.
- Limited flexibility: Frontend Gherkin tests are constrained to the scenarios and steps specified in the feature files, which may limit testing capabilities for certain complex UI scenarios.
- Maintenance overhead: As the frontend evolves, Gherkin tests must be updated to align with the changes, potentially requiring significant effort and time for extensive frontend applications.
- Smaller community: Elixir and Wallaby have a smaller community compared to more mainstream programming languages and libraries, which might result in limited support and resources for frontend Gherkin testing.
With Wallaby, you can write and run automated tests for your application’s UI, ensuring that it works as expected. Wallaby is easy to use, integrates well with other testing tools, and supports a wide range of test scenarios, making it an excellent choice for testing UI in Elixir applications.
In summary, implementing Gherkin tests in an Elixir application can bring several benefits such as improved collaboration, clear requirements, and test automation. However, it can also add additional complexity and maintenance overhead and may require a certain level of technical expertise. Ultimately, the decision to use Gherkin with Elixir should be based on the specific needs and requirements of the project.
In case you missed part 1 of this blog, you can read it here.