Functional Test Refactoring: Introduction
A quick, reliable, and efficient way to improve the test design of your automated test scripts
Automated Test scripts shall be treated as production code, as David Thomas and Andy Hunt wrote in the classic “The Pragmatic Programmer” book.
Automated test scripts are often poorly designed, inefficient, hard to understand and maintain, i.e., the same issues we found in code. With coding, there is Code Refactoring, a process to enhance existing code without changing its external behaviour.
My interest in web test automation started in 2005, at that time, I was a lead Java programmer. I performed code refactoring and developed (and maintained) automated UI tests daily. I gradually realized that, just like code refactoring for code, there are the same needs for automated functional (UI) tests.
I came up with a process, a set of refactorings, and a tool for enhancing the quality of automated functional test scripts. I named it “Functional Test Refactoring” (~2007).
Some manual tester may wonder: will functional test refactoring be too technical for me? The answer is NO. In fact, every test refactoring is easy to understand and apply, each team member (including business analysts) of a software team can perform functional test refactorings.
Functional Test Refactoring
Functional Test Refactoring is a step-by-step process to refine test scripts to be
easy to read
concise
easy to maintain
Automated test engineers may use recorders to assist in creating automated test steps (however, I don’t recommend it, I have always been doing it manually to choose a better locator, with extremely high efficiency. See my article: ‘Why I created a Selenium Recorder but rarely used it myself?’), but shall perform refactorings immediately. Otherwise, leading to tech debts.
Where is Functional Test Refactoring coming from?
I came up with this idea in 2007, and it took me a couple of years to implement the refactoring support in my testing tool: iTest2 (later renamed to TestWise). Functional Test Refactoring was made publicly by my article: “Refactoring Automated Functional Test Scripts with iTest2” on InfoQ, 2009–07–22.
In the presentation “Trends in Agile Testing” (2009) by Lisa Crispin, co-author of the Agile Testing book, iTest2 was listed as the first testing tool in the “Testing Tool Spectrum” slide (#24). Lisa used the same slide in her other talk, “Overview of Agile Testing”.
Also, in 2009, iTest2 was demonstrated (not by me) at Agile 2009 AA-FFT in Chicago, and listed in Ward Cunningham (co-author of Agile Manifesto)’s Functional Testing Tools list.
In October 2013, I demonstrated functional test refactorings (my presentation title: “Refactoring Automated Functional Tests”) at the STARWEST conference (‘one of the longest-running and most respected conferences on software testing and quality assurance’ in the world) with very good feedback.
As you can see, Functional Testing Refactoring has been proven by some of the best testing/Agile experts in the world.
Test Refactoring Steps
The test refactoring steps are pretty much the same as the ones for code refactoring.
Before refactoring, your test script must be valid first!
Identify bad test scripts
Bad design in a test script is just like ‘code smell’ in code refactoring, for conciseness, I call it ‘test smell’. Generally speaking, identifying ‘test smell’ is easy, such as duplicated test steps. The best way to learn is to pair-test with a test automation coach, one day shall be enough to master the core.
Improve ( small step at a time)
Once identified the ‘test smell’, a test engineer shall know how to improve it. However, efficiency is an important factor here, as testers need to perform refactorings (such as ‘Extract to a Page Function’) very frequently. Refactoring tool support in Testing IDEs can help with that.
Re-run the test
Make sure your change is correct, and the behavior of test execution is not affected. Immediately rerunning the test after refactoring changes can prevent introducing defects to your test scripts. Novice often forgets this step.
Tool Support
If it is hard to do, people just don’t do it often.
Just like code refactoring, tool support is essential. Code refactoring only became meaningful after JetBrains first added refactoring support in its JAVA IDE: Intellij IDEA (around 2001). Now, all major programming IDEs support code refactoring.
Efficient
Reduce chances of introducing mistakes
Easy to reach the convention
In fact, refactoring support has been named the top feature in the next-generation functional testing tools.
Next-Gen Functional Testing Tool
Agile Alliance workshop on Next-Generation Functional Testing (Oct 2007), Envision “We are lacking IDE that facilitate things like:”
* refactoring test elements
* command completion
* …
TestWise, the functional testing IDE I created, is the first (and still the only one based on my knowledge, if you do know another, please let me know) testing tool that supports “refactoring test elements”.
Common Functional Testing Refactorings
The six functional test refactorings supported by TestWise are
Extract Page Function
Introduce Page Object
Rename
I will explain each of these six refactorings with video demonstrations in separate articles. Stay tuned.
These may be other functional test refactorings, but I haven’t thought of a new one since 2009. If you have a suggestion, please let me know.
Example Test (before and after refactoring)
Before
it "(Recorded version) Book Flight" do
driver.find_element(:id, "username").send_keys("agileway")
driver.find_element(:id, "password").send_keys("testwise")
driver.find_element(:name, "commit").click
driver.find_element(:xpath, "//input[@name='tripType' and @value='oneway']").click
Selenium::WebDriver::Support::Select.new(driver.find_element(:name, "fromPort")).select_by(:text, "Sydney")
Selenium::WebDriver::Support::Select.new(driver.find_element(:name, "toPort")).select_by(:text, "New York")
Selenium::WebDriver::Support::Select.new(driver.find_element(:id, "departDay")).select_by(:text, "02")
Selenium::WebDriver::Support::Select.new(driver.find_element(:id, "departMonth")).select_by(:text, "May 2016")
driver.find_element(:xpath,"//input[@value='Continue']").click
driver.find_element(:name, "passengerFirstName").send_keys("Bob")
driver.find_element(:name, "passengerLastName").send_keys("Tester")
driver.find_element(:xpath,"//input[@value='Next']").click
driver.find_element(:xpath, "//input[@name='card_type' and @value='master']").click
driver.find_element(:name, "holder_name").send_keys("Bob the Tester")
driver.find_element(:name, "card_number").send_keys("4242424242424242")
Selenium::WebDriver::Support::Select.new(driver.find_element(:name, "expiry_month")).select_by(:text, "04")
Selenium::WebDriver::Support::Select.new(driver.find_element(:name, "expiry_year")).select_by(:text, "2016")
driver.find_element(:xpath,"//input[@type='submit' and @value='Pay now']").click
wait = Selenium::WebDriver::Wait.new(:timeout => 15)
wait.until { driver.page_source.include?("Booking number") }
driver.find_element(:link_text, "Sign off").click
end
Apply refactorings
In upcoming articles, I will show the application of the following refactorings to the above test script.
Extract to Page Function
Introduce Page Object
Please note, every refactoring is done very quickly with refactoring support in a functional testing IDE, typically in seconds.
After
before(:each) do
sign_in("agileway", "testwise")
end
after(:each) do
sign_off
end
it "(Refactored version) Book Flight" do
flight_page = FlightPage.new(browser)
flight_page.select_oneway_trip
flight_page.select_from("Sydney")
flight_page.select_to("New York")
flight_page.select_departure_day("02")
flight_page.select_departure_month("May 2016")
flight_page.click_continue
passenger_page = PassengerPage.new(browser)
passenger_page.enter_first_name("Bob")
passenger_page.enter_last_name("Tester")
passenger_page.click_next
payment_page = PaymentPage.new(browser)
payment_page.select_card_type_master
payment_page.enter_holder_name("Bob the Tester")
payment_page.enter_card_number("4242424242424242")
payment_page.select_expiry_month("04")
payment_page.select_expiry_year("2016")
payment_page.click_pay_now
wait = Selenium::WebDriver::Wait.new(:timeout => 15)
wait.until { driver.page_source.include?("Booking number") }
end
The test script is a lot better, isn’t it? More importantly, it is much easier to maintain.
Refactorings are applicable to other languages
Functional Test Refactorings generally apply to all automated test scripts in different frameworks and languages. The above test script is Selenium WebDriver + RSpec. Below are two test scripts in Mocha (JS) and PyTest (Python), following the same Maintainable Automated Test Design.
Mocha (JavaScript)
test.beforeEach(function() {
this.timeout(timeOut);
driver.get(helper.site_url());
helper.login(driver, "agileway", "testwise");
});
test.it('[5] Book flight with payment', function() {
this.timeout(timeOut);
let flight_page = new FlightPage(driver);
flight_page.selectTripType("oneway")
flight_page.selectDepartFrom("New York")
flight_page.selectArriveAt("Sydney")
flight_page.selectDepartDay("02")
flight_page.selectDepartMonth("May 2016")
flight_page.clickContinue()
let passenger_page = new PassengerPage(driver)
passenger_page.enterFirstName("Bob")
passenger_page.enterLastName("Tester")
passenger_page.clickNext();
let payment_page = new PaymentPage(driver)
driver.findElement(By.xpath("//input[@name='card_type' and @value='visa']")).click();
driver.findElement(By.name("card_number")).sendKeys("4242424242424242");
driver.findElement(By.xpath("//input[@value='Pay now']")).click();
driver.sleep(10000)
driver.findElement(By.tagName("body")).getText().then(function(text) {
assert(text.contains("Booking number"))
});
});
I intentionally did not refactor the test steps in the last PaymentPage
, for you to compare.
PyTest (Python)
def test_payment_by_credit_card(self):
flight_page = FlightPage(self.driver)
flight_page.select_trip_type("oneway")
flight_page.select_depart_from("New York")
flight_page.select_arrive_at("Sydney")
flight_page.select_depart_day("04")
flight_page.select_depart_month("March 2016")
flight_page.click_continue()
time.sleep(0.5)
passenger_page = PassengerPage(self.driver)
passenger_page.enter_first_name("Wendy")
passenger_page.enter_last_name("Tester")
passenger_page.click_next()
payment_page = PaymentPage(self.driver)
payment_page.select_card_type("visa")
payment_page.enter_holder_name("Bob the Tester")
payment_page.enter_card_number("4242424242424242")
payment_page.enter_expiry_month("04")
payment_page.enter_expiry_year("2016")
payment_page.click_pay_now()
self.wait_for_ajax_complete(10)
self.assertIn("Booking number", self.driver.page_source)