Page Object เป็น design pattern ในการเขียนโค้ดสำหรับ automation testing. แนวคิดก็คือสร้าง Class ที่ทำหน้าที่เป็น interface ระหว่าง application และ test case.
ก่อนที่เราจะพูดเรื่อง Page Object, ผมขอเกริ่นนำก่อนว่า Page Object นี้เกิดมาเพื่อสิ่งใด? เริ่มต้นด้วยตัวอย่างการเขียนเทสแบบไม่ใช้ Page Object (จากนี้ไปจะเรียก Page Object Model ย่อๆว่า POM).
ปัญหาของการเขียนโค้ดโดยไม่ใช้ POM
บัวบานขอยกตัวอย่างการ Search flight ในหน้า Home ของ AirAsia.com. Test case จะทำตามลำดับดังนี้:
โค้ดของ Test case จะเป็นดังนี้:
public void SearchFlight_No_PageObject() { driver = new ChromeDriver(); // Open airasia.com driver.Navigate().GoToUrl(@"http://www.airasia.com/ot/en/home.page"); // Select Origin driver.FindElement(By.CssSelector(".stations-container > .expand-icon")).Click(); System.Threading.Thread.Sleep(1500); driver.FindElement(By.CssSelector("#fromFlyoutBody li[data-value='DMK']")).Click(); System.Threading.Thread.Sleep(1500); // Select Destination driver.FindElement(By.CssSelector("#toFlyoutBody li[data-value='SYD']")).Click(); System.Threading.Thread.Sleep(1500); // Select Depart Date driver.FindElement(By.Id("search_from_date")).SendKeys("26/09/2016"); driver.FindElement(By.Id("search_from_date")).SendKeys(Keys.Enter); System.Threading.Thread.Sleep(1500); // Select Return Date driver.FindElement(By.Id("search_to_date")).SendKeys("27/09/2016"); driver.FindElement(By.Id("search_to_date")).SendKeys(Keys.Enter); System.Threading.Thread.Sleep(1500); // Click Submit driver.FindElement(By.Id("searchButton")).Click(); // Assert Depart > Cities string DepartCities = driver.FindElement(By.CssSelector("#availabilityForm div.avail-header-cities")).Text; Assert.That(DepartCities, Is.EqualTo("(Bangkok - Don Mueang Sydney)")); }
ดูเผินๆโค้ดด้านบนก็สวยงามดีนัก แต่ถ้าเราทำแบบนี้ต่อไปเรื่อยๆ จนมีโค้ดสัก 10,000 บรรทัด, เราจะพบปัญหาดังต่อไปนี้:
- มีโค้ดซ้ำซ้อนกันมาก.
เนื่องจากเทสเคสมักจะมีจุดที่เหมือนๆกัน เช่นต้องเลือก Origin, Destination, ฯลฯ.
เราแก้ปัญหานี้ได้โดยการเขียนฟังก์ชันเพื่อเรียกใช้ซ้ำได้. แต่ถ้าเรามีไฟล์ test case หลายไฟล์ ก็จะมีปัญหาเรื่อง scope ของฟังก์ชัน, นอกจากนี้จะเกิดการงงมากๆว่าจะเรียกใช้ฟังก์ชันไหนดี เพราะเราอาจจะต้องสร้างฟังก์ชันหลายร้อยฟังก์ชันอยู่ในไฟล์เดียวกัน. - บำรุงรักษายากและเสียเวลามาก.
ลองนึกภาพว่า airasia เปลี่ยนวิธีเลือก Origin จากที่ให้กดจาก <a> มาเป็นเลือกจาก dropdown, เราต้องเปลี่ยนขั้นตอนในการเลือก Origin ซึ่งมีอยู่ในทุก test case. ถ้าเรามี test case เยอะๆ ก็ต้องมาไล่แก้จนปวดหัว. - อ่านโค้ดไม่รู้เรื่อง.
ในเวลาที่ automate tests ทำงานผิดปกติ, เราต้องมาไล่ดูว่าผิดที่ขั้นตอนไหนและขั้นตอนนั้นต้องการทำอะไร.
ลองอ่านโค้ดด้านล่างดูครับ รู้มั้ยว่าขั้นตอนนี้คือการทำอะไร?driver.FindElement(By.XPath("//div[@id='ui-datepicker-div']//td[not(contains(@class,'ui-state-disabled'))]/a[text()='26']")).Click();
ถ้าเราเพิ่งเขียนไม่กี่ชั่วโมง เราจะรู้ครับ. แต่ถ้าผ่านไปหนึ่งอาทิตย์ หนึ่งเดือน หรือหนึ่งปี, เราไม่มีทางรู้เลยว่าบรรทัดนี้ต้องการทำอะไรกันแน่.
- หลายๆคนมาช่วยกันทำงานได้ยาก.
ในกรณีที่เราทำงานกันหลายคนบนโปรเจคเดียวกัน, ถ้าเราไม่ออกแบบ package ให้เหมาะสม จะทำให้เกิดปัญหาเช่น มีฟังก์ชันที่ไม่เกี่ยวข้องกันเลยไปอยู่ในไฟล์เดียวกัน ซึ่งมักเป็นสาเหตุให้คนหลายๆคนต้องไปแก้ไฟล์เดียวกัน. สุดท้ายก็จะมีปัญหาตอน merge.
ปัญหา 4 ข้อด้านบนมักจะไม่เกิดขึ้น ถ้าซอฟต์แวร์คุณเล็กมากๆ และไม่มีการอัพเดทเลย. แต่ในโลกปัจจุบันเราไม่มีซอฟต์แวร์แบบนั้นหรอกนะฮะ ยิ่งทุกวันนี้เน้นออกอัพเดทกันทุกอาทิตย์หรือสองอาทิตย์, แก้โค้ด automate tests กันหลังอานเลยล่ะครับ.
โดยสรุปแล้ว เราควรออกแบบ automate tests ให้รองรับการเปลี่ยนแปลง และต้องแก้ไขปัญหา 4 ข้อด้านบน.
Page Object ช่วยอะไรได้?
เนื่องจาก application เวอชันใหม่นั้นมักจะไม่เปลี่ยนแปลง workflow ของการใช้งาน, test case และ application interface จึงมักจะเหมือนเดิมเสมอ. ส่วนที่จะถูกเปลี่ยนคือ behavior ภายในของแต่ละ interface. ยกตัวอย่างเช่น การเลือกวันเดินทางนั้นอาจมีการเปลี่ยนรูปร่างของปฏิทิน แต่ application interface ยังคงเดิมก็คือต้องใส่วันเดือนปี.
แนวคิดของ POM คือการสร้าง class ขึ่นมาเพื่อใช้เป็น interface ให้กับ application. ถ้ามีการเปลี่ยนแปลง application ก็ให้มาแก้ที่ class แทนที่จะไปแก้ที่ test case. โค้ดของ Test case ข้างล่างแสดงการเรียกใช้ class Home และ SearchDetails ที่สร้างตามแนวคิด POM. จะเห็นได้ว่าโค้ดอ่านง่ายและเข้าใจง่ายขึ้นกว่าเดิมมาก.
public void SearchFlight_PageObject() { driver = new ChromeDriver(); // Open airasia.com Home homepage = new Home(driver); // Select Origin homepage.FlightSearch.Origin.OpenMenu().SelecByValue("DMK"); // Select Destination homepage.FlightSearch.Destination.CountriesMenu.SelecByValue("SYD"); // Select Depart Date homepage.FlightSearch.DepartDate.EnterText("26/09/2016"); // Select Return Date homepage.FlightSearch.ReturnDate.ClearText().EnterText("27/09/2016"); // Click Submit SearchDetails searchDetailsPage = homepage.FlightSearch.Submit(); // Assert Depart > Cities Assert.That(searchDetailsPage.Depart.HeaderCities, Is.EqualTo("(Bangkok - Don Mueang Sydney)")); }
Class diagram ข้างล่างคือการออกแบบ Page Object สำหรับหน้า Home และหน้า Search Details.
การออกแบบ Page Objects
ให้มอง Application แบบ Object Oriented, กล่าวคือให้วิเคราะห์ Application แล้วกำหนดว่าอะไรคือProperty และ Method ของ Application. อาจจะเริ่มด้วยการเขียน use case diagram หรือ class diagram ก็ได้.
โดยทั่วไปเราจะเริ่มจากการสร้าง Class ของหน้า Home. ภายใน Home จะประกอบไปด้วย Property และ Method ที่มี type เป็น Reference type, Custom reference, หรือ Value Type. รูปข้างล่างแสดงการแบ่งขอบเขตของ Property ต่างๆในหน้า Home.