Chia sẻ Test Context trong Cucumber là một cách để chia các bước thành nhiều lớp và chia sẻ trạng thái giữa các bước. Điều này có thể giúp tránh trùng lặp và làm cho mã dễ đọc và dễ bảo trì hơn.
Chia các Cucumber steps giữa nhiều lớp có thể là một ý tưởng hay. Nhưng kịch bản trong Cucumber là một loạt các bước được thực hiện lần lượt. Mỗi bước trong kịch bản có thể có một số trạng thái có thể được yêu cầu bởi một bước khác trong kịch bản. Nói cách khác là mỗi bước phụ thuộc vào các bước trước đó. Điều này có nghĩa là chúng ta phải có khả năng chia sẻ trạng thái giữa các bước.
Ngoài ra, khi bạn mở rộng các bước thử nghiệm hoặc tệp tính năng của mình (feature), việc giữ tất cả các bước trong một lớp step definitions duy nhất sẽ trở nên không thực tế và khó bảo trì lâu dài, vì vậy bạn cần sử dụng nhiều lớp step definitions. Bây giờ bạn có một vấn đề mới là các đối tượng bạn tạo trong một lớp chứa các bước cũng sẽ cần thiết trong các lớp chứa các bước khác.
Trong trường hợp của chúng ta cũng vậy, cho đến bây giờ chúng ta chỉ có một kịch bản có vài bước. Vì vậy, chúng ta giữ tất cả các bước trong cùng một tệp Step Definitions. Nhưng sớm hay muộn chúng ta sẽ tăng số lượng kịch bản. Và cuối cùng chúng ta cũng sẽ phát triển các tệp bước của mình bổ sung ngày càng nhiều hơn.
Vì vậy chúng ta phải chia sẻ Bối cảnh thử nghiệm/Bối cảnh kịch bản/Trạng thái thử nghiệm với tất cả tệp Step Definitions. Đây là lý do tại sao Cucumber hỗ trợ bộ phụ thuộc Dependency Injection (DI) - nó chỉ yêu cầu một bộ chứa DI khởi tạo các lớp định nghĩa bước của bạn và kết nối chúng một cách chính xác.
#Note: Dependency Injection là một design pattern cho phép chúng ta loại bỏ sự phụ thuộc cứng nhắc giữa các phần tử.
Một trong những phương pháp để chia sẻ bối cảnh thử nghiệm trong Cucumber là sử dụng PicoContainer, đây là một bộ chứa nội xạ phụ thuộc (dependency injection) có thể khởi tạo và kết nối các lớp định nghĩa bước của bạn.
PicoContainer là một bộ chứa ánh xạ phụ thuộc có thể khởi tạo và kết nối các đối tượng bằng cách xem xét các hàm tạo của chúng. Nó là một khung đơn giản và nhẹ có thể được sử dụng để chia sẻ trạng thái giữa các bước trong Cucumber.
PicoContainer là một thư viện do Paul Hammant viết vào năm 2003-2004. Sử dụng nó rất đơn giản với các lí do:
Chỉ cần chỉ định cho nó lớp nào thì nó sẽ khởi tạo từng lớp ấy, các lớp được kết nối chính xác với nhau thông qua các hàm tạo của chúng. Cucumber quét các lớp của bạn với các định nghĩa bước trong chúng, chuyển chúng tới PicoContainer, sau đó yêu cầu nó tạo các phiên bản mới cho mọi tình huống.
Như vậy thì bài toán chia sẻ dữ liệu giữa các class sẽ được giải quyết.
Chúng ta sẽ thực hiện các bước dưới đây để chia sẻ trạng thái dữ liệu qua các bước:
Chúng ta thêm thư viện với version giống bản Cucumber đang dùng nhé. Hiện tại là 7.11.1
<!-- https://mvnrepository.com/artifact/io.cucumber/cucumber-picocontainer -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>7.11.1</version>
<scope>test</scope>
</dependency>
Mình đặt tên class là TestContext và để trong package hooks luôn.
Tại đây các bạn sẽ khai báo khởi tạo driver cho nó và các khởi tạo Page class trong POM.
package com.anhtester.hooks;
import com.anhtester.driver.DriverFactory;
import com.anhtester.driver.DriverManager;
import com.anhtester.pages.CategoryPage;
import com.anhtester.pages.CommonPage;
import com.anhtester.pages.LoginCMSPage;
import com.anhtester.pages.LoginCRMPage;
import com.anhtester.utils.LogUtils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ThreadGuard;
public class TestContext {
private WebDriver driver;
private CommonPage commonPage;
private LoginCRMPage loginCRMPage;
private LoginCMSPage loginCMSPage;
private CategoryPage categoryPage;
public TestContext() {
ThreadGuard.protect(new DriverFactory().createDriver());
LogUtils.info("Driver in TestContext: " + getDriver());
}
public CommonPage getCommonPage() {
if (commonPage == null) {
commonPage = new CommonPage();
}
return commonPage;
}
public LoginCRMPage getLoginCRMPage() {
if (loginCRMPage == null) {
loginCRMPage = new LoginCRMPage();
}
return loginCRMPage;
}
public LoginCMSPage getLoginCMSPage() {
if (loginCMSPage == null) {
loginCMSPage = new LoginCMSPage();
}
return loginCMSPage;
}
public CategoryPage getCategoryPage() {
if (categoryPage == null) {
categoryPage = new CategoryPage();
}
return categoryPage;
}
public WebDriver getDriver() {
return DriverManager.getDriver();
}
}
Chổ này mình tạo cái class DriverFactory để quản lý cái create driver, bỏ trạng thái static ra vì nó sẽ làm sai lệch trạng thái chia sẻ sau này khi chạy Parallel Execution.
package com.anhtester.driver;
import com.anhtester.constants.ConstantGlobal;
import com.anhtester.helpers.PropertiesHelpers;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
public class DriverFactory {
public WebDriver createDriver() {
WebDriver driver = setupBrowser(PropertiesHelpers.getValue("BROWSER"));
DriverManager.setDriver(driver);
return driver;
}
public WebDriver createDriver(String browserName) {
WebDriver driver = setupBrowser(browserName);
DriverManager.setDriver(driver);
return driver;
}
private WebDriver setupBrowser(String browserName) {
WebDriver driver;
switch (browserName.trim().toLowerCase()) {
case "chrome":
driver = initChromeDriver();
break;
case "firefox":
driver = initFirefoxDriver();
break;
case "edge":
driver = initEdgeDriver();
break;
default:
System.out.println("Browser: " + browserName + " is invalid, Launching Chrome browser default...");
driver = initChromeDriver();
}
return driver;
}
private WebDriver initChromeDriver() {
WebDriver driver;
System.out.println("Launching Chrome browser...");
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.setHeadless(ConstantGlobal.HEADLESS);
options.addArguments("--remote-allow-origins=*");
driver = new ChromeDriver(options);
driver.manage().window().maximize();
return driver;
}
private WebDriver initEdgeDriver() {
WebDriver driver;
System.out.println("Launching Edge browser...");
WebDriverManager.edgedriver().setup();
EdgeOptions options = new EdgeOptions();
options.setHeadless(ConstantGlobal.HEADLESS);
options.addArguments("--remote-allow-origins=*");
driver = new EdgeDriver(options);
driver.manage().window().maximize();
return driver;
}
private WebDriver initFirefoxDriver() {
WebDriver driver;
System.out.println("Launching Firefox browser...");
WebDriverManager.firefoxdriver().setup();
FirefoxOptions options = new FirefoxOptions();
options.setHeadless(ConstantGlobal.HEADLESS);
driver = new FirefoxDriver(options);
driver.manage().window().maximize();
return driver;
}
public void closeDriver() {
if (DriverManager.getDriver() != null) {
DriverManager.quit();
}
}
}
Chúng ta bỏ cái khởi tạo browser của BaseTest trong Cucumber Hooks tại @Before kẻo nó bị trùng lặp. Và gọi hàm quit() driver trong DriverManager tại @After
@Before
public void beforeScenario() {
System.out.println("================ beforeScenario ================");
//BaseTest.createDriver();
}
@After
public void afterScenario(Scenario scenario) {
System.out.println("================ afterScenario ================");
DriverManager.quit();
}
Ví dụ mình lấy cái tệp steps của Categories.feature để chia ra làm 2 class step definitions để chứa các steps.
Feature: Manage Category
Background:
Given user logged in the CMS system with "Admin" role
Scenario: Add new Category
Given user has access to the Category page
When user has finished entering the category information
And click the Save button
Then the message "Category has been inserted successfully" displays
Scenario: Update the Category existing
Given user has access to the Category page
When user search a category existing "Anh Tester"
And user edit the category information
And click the Save button
Then the message "Category has been updated successfully" displays
Như vậy mình sẽ chia thành 2 file class chứa các steps là StepsCategory và CommonStepDefinitions
StepsCategory.java
package com.anhtester.stepdefinitions;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
public class StepsCategory {
@Given("user has access to the Category page")
public void userHasAccessToTheCategoryPage() {
}
@When("user has finished entering the category information")
public void userHasFinishedEnteringTheCategoryInformation() {
}
@And("click the Save button")
public void clickTheSaveButton() {
}
@When("user search a category existing {string}")
public void userSearchACategoryExisting(String categoryName) {
}
@And("user edit the category information")
public void userEditTheCategoryInformation() {
}
}
CommonStepDefinitions.java
package com.anhtester.common;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
public class CommonStepDefinitions {
@Given("user logged in the CMS system with {string} role")
public void userLoggedInTheCMSSystemWithRole(String roleName) {
}
@Then("the message {string} displays")
public void theMessageDisplays(String message) {
}
@And("user should see the notification displays")
public void userShouldSeeTheNotificationDisplays() {
}
}
Ở đây chúng ta tách 2 steps từ StepsCategory vào common steps là:
@Given("user logged in the CMS system with {string} role")
public void userLoggedInTheCMSSystemWithRole(String roleName) {}
@Then("the message {string} displays")
public void theMessageDisplays(String message) {}
Chúng ta chỉ cần thêm Constructor vào các class steps và chuyển TestContext làm Tham số cho hàm xây dựng sẽ giúp bạn giải quyết mọi khó khăn về việc truyền dữ liệu.
Trong đối tượng TestContext, chúng ta đã có sẵn các hàm cần thiết cho thử nghiệm.
StepsCategory.java
package com.anhtester.stepdefinitions;
import com.anhtester.hooks.TestContext;
import com.anhtester.pages.CategoryPage;
import com.anhtester.pages.CommonPage;
import com.anhtester.pages.LoginCMSPage;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
public class StepsCategory {
TestContext testContext;
LoginCMSPage loginCMSPage;
CategoryPage categoryPage;
CommonPage commonPage;
public StepsCategory(TestContext testContext) {
this.testContext = testContext;
commonPage = testContext.getCommonPage();
loginCMSPage = testContext.getLoginCMSPage();
}
@Given("user has access to the Category page")
public void userHasAccessToTheCategoryPage() {
commonPage.clickMenuProducts();
categoryPage = commonPage.openCategoriesPage();
}
@When("user has finished entering the category information")
public void userHasFinishedEnteringTheCategoryInformation() {
categoryPage.clickAddNewButton();
categoryPage.inputDataCategory();
}
@And("click the Save button")
public void clickTheSaveButton() {
categoryPage.clickSaveButton();
}
@When("user search a category existing {string}")
public void userSearchACategoryExisting(String categoryName) {
categoryPage.searchCategory(categoryName);
}
@And("user edit the category information")
public void userEditTheCategoryInformation() {
commonPage.clickEditButton();
categoryPage.inputDataCategory();
}
}
CommonStepDefinitions.java
package com.anhtester.common;
import com.anhtester.hooks.TestContext;
import com.anhtester.pages.CategoryPage;
import com.anhtester.pages.CommonPage;
import com.anhtester.pages.LoginCMSPage;
import com.anhtester.pages.LoginCRMPage;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import org.testng.Assert;
public class CommonStepDefinitions {
TestContext testContext;
LoginCRMPage loginCRMPage;
LoginCMSPage loginCMSPage;
CategoryPage categoryPage;
CommonPage commonPage;
public CommonStepDefinitions(TestContext testContext) {
this.testContext = testContext;
commonPage = testContext.getCommonPage();
loginCMSPage = testContext.getLoginCMSPage();
}
@Given("user logged in the CMS system with {string} role")
public void userLoggedInTheCMSSystemWithRole(String roleName) {
loginCMSPage.loginAdminRole();
}
@Then("the message {string} displays")
public void theMessageDisplays(String message) {
Assert.assertEquals(commonPage.getMessageNotify(), message, "Message not match.");
}
@And("user should see the notification displays")
public void userShouldSeeTheNotificationDisplays() {
}
}
CategoryPage.java
package com.anhtester.pages;
import com.anhtester.utils.DataGenerateUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import static com.anhtester.keywords.WebUI.*;
public class CategoryPage {
private By buttonAddNewCategory = By.xpath("//span[normalize-space()='Add New category']");
private By inputCategoryName = By.xpath("//input[@id='name']");
private By inputOrderingNumber = By.xpath("//input[@id='order_level']");
private By inputMetaTitle = By.xpath("//input[@placeholder='Meta Title']");
private By inputMetaDescription = By.xpath("//textarea[@name='meta_description']");
private By buttonSave = By.xpath("//button[normalize-space()='Save']");
private By inputSearch = By.xpath("//input[@id='search']");
public void clickAddNewButton() {
clickElement(buttonAddNewCategory);
}
public void searchCategory(String categoryName) {
setTextAndKey(inputSearch, categoryName, Keys.ENTER);
}
public void inputDataCategory() {
setText(inputCategoryName, DataGenerateUtils.getProgrammingLanguage());
setText(inputOrderingNumber, String.valueOf(DataGenerateUtils.getOrderNumber()));
setText(inputMetaTitle, DataGenerateUtils.getProgrammingLanguage());
setText(inputMetaDescription, DataGenerateUtils.getFunnyName());
}
public void clickSaveButton() {
clickElement(buttonSave);
}
}
CommonPage.java
package com.anhtester.pages;
import com.anhtester.keywords.WebUI;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import static com.anhtester.keywords.WebUI.setTextAndKey;
public class CommonPage {
private By menuProducts = By.xpath("//span[normalize-space()='Products']");
private By menuCategory = By.xpath("//span[normalize-space()='Category']");
private By menuAddNewProduct = By.xpath("//span[normalize-space()='Add New Product']");
private By menuAllProducts = By.xpath("//span[normalize-space()='All products']");
private By messageNotify = By.xpath("//span[@data-notify='message']");
private By buttonEdit = By.xpath("(//a[@title='Edit'])[1]");
private By inputSearch = By.xpath("//input[@id='search']");
public void searchDataTable(String dataName) {
setTextAndKey(inputSearch, dataName, Keys.ENTER);
}
public void clickEditButton() {
WebUI.clickElement(buttonEdit);
}
public String getMessageNotify() {
return WebUI.getElementText(messageNotify);
}
public void clickMenuProducts() {
WebUI.clickElement(menuProducts);
}
public CategoryPage openCategoriesPage() {
WebUI.clickElement(menuCategory);
return new CategoryPage();
}
}
Tại class test runner TestRunnerCategoryCMS chúng ta thêm đoạn DataProvider để nó có thể thực thi Parallel Execution cho từng Scenario trong một Feature file.
package com.anhtester.runners;
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@CucumberOptions(
features = "src/test/resources/features/Categories",
glue = {"com.anhtester.stepdefinitions",
"com.anhtester.common",
"com.anhtester.hooks"
},
plugin = {"pretty", "html:target/cucumber-html-report.html"},
tags = ""
)
@Test
public class TestRunnerCategoryCMS extends AbstractTestNGCucumberTests {
//Parallel Execution Scenario
@Override
@DataProvider(parallel = true)
public Object[][] scenarios() {
return super.scenarios();
}
}
Kết quả thực thi:
✳️ Source: https://github.com/anhtester/CucumberTestNG022023/releases/tag/lesson_9_10_PicoContainer_ThucHanh
Anh Tester
Đường dẫu khó chân vẫn cần bước đi
Đời dẫu khổ tâm vẫn cần nghĩ thấu