18、自动化测试实战:Selenium 与相关工具的综合应用

自动化测试实战:Selenium 与相关工具的综合应用

1. 配置文件与版本管理

在自动化测试中,配置文件起着至关重要的作用,它可以帮助我们管理各种工具和环境的版本信息。以下是 selenium.properties 文件的代码:

# Selenium Properties File
selenium.revision=3.7.1
geckodriver.revision=0.19.1
chromedriver.revision=2.33
iedriver.revision=11.0
firefox.revision=57.0
chrome.revision=62.0
ie.revision=11.0
gecko.driver.windows.path=drivers/geckodriver.exe
chrome.driver.windows.path=drivers/chromedriver.exe
ie.driver.windows.path=drivers/IEDriverServer.exe

这个文件定义了 Selenium 及其相关驱动的版本信息,以及不同浏览器驱动在 Windows 系统上的路径。通过这样的配置,我们可以确保测试环境的一致性和稳定性。

2. 测试报告生成

为了更好地展示测试结果,我们使用了 ExtentReports 来生成 HTML 测试报告。这部分涉及到两个重要的文件: ExtentTestNGIReporterListener.java extent-config.xml

2.1 ExtentTestNGIReporterListener.java
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.MediaEntityBuilder;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
import com.aventstack.extentreports.reporter.configuration.Protocol;
import com.aventstack.extentreports.reporter.configuration.Theme;
import org.testng.*;
import org.testng.xml.XmlSuite;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;

/**
 * @author Carl Cocchiaro
 *
 * ExtentReports HTML Reporter Class
 *
 */
public class ExtentTestNGIReporterListener implements IReporter {
    private String bitmapDir = Global_VARS.REPORT_PATH;
    private String seleniumRev = "3.7.1";
    private String docTitle = "SELENIUM FRAMEWORK DESIGN IN DATA-DRIVEN TESTING";
    private ExtentReports extent;

    @Override
    public void generateReport(List<XmlSuite> xmlSuites,
                               List<ISuite> suites,
                               String outputDirectory) {
        for (ISuite suite : suites) {
            init(suite);
            Map<String, ISuiteResult> results = suite.getResults();
            for (ISuiteResult result : results.values()) {
                try {
                    processTestResults(result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        extent.flush();
    }

    private void init(ISuite suite) {
        File directory = new File(Global_VARS.REPORT_PATH);
        if (!directory.exists()) {
            directory.mkdirs();
        }
        ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(Global_VARS.REPORT_PATH + suite.getName() + ".html");
        htmlReporter.config().setDocumentTitle(docTitle);
        htmlReporter.config().setReportName(suite.getName().replace("_", " "));
        htmlReporter.config().setChartVisibilityOnOpen(false);
        htmlReporter.config().setTheme(Theme.STANDARD);
        htmlReporter.config().setEncoding("UTF-8");
        htmlReporter.config().setProtocol(Protocol.HTTPS);
        htmlReporter.config().setTimeStampFormat("MMM-dd-yyyy HH:mm:ss a");
        htmlReporter.loadXMLConfig(new File(Global_VARS.REPORT_CONFIG_FILE));
        extent = new ExtentReports();
        extent.setSystemInfo("Browser", Global_VARS.DEF_BROWSER);
        extent.setSystemInfo("Environment", Global_VARS.DEF_ENVIRONMENT);
        extent.setSystemInfo("Platform", Global_VARS.DEF_PLATFORM);
        extent.setSystemInfo("OS Version", System.getProperty("os.version"));
        extent.setSystemInfo("Java Version", System.getProperty("java.version"));
        extent.setSystemInfo("Selenium Version", seleniumRev);
        extent.attachReporter(htmlReporter);
        extent.setReportUsesManualConfiguration(true);
    }

    private void processTestResults(ISuiteResult r) throws Exception {
        ExtentTest test = null;
        Status status = null;
        String message = null;
        Set<ITestResult> passed = r.getTestContext().getPassedTests().getAllResults();
        Set<ITestResult> failed = r.getTestContext().getFailedTests().getAllResults();
        Set<ITestResult> skipped = r.getTestContext().getSkippedTests().getAllResults();
        Set<ITestResult> configs = r.getTestContext().getFailedConfigurations().getAllResults();
        Set<ITestResult> tests = new HashSet<ITestResult>();
        tests.addAll(passed);
        tests.addAll(skipped);
        tests.addAll(failed);
        if (tests.size() > 0) {
            List<ITestResult> resultList = new LinkedList<ITestResult>(tests);
            class ResultComparator implements Comparator<ITestResult> {
                public int compare(ITestResult r1, ITestResult r2) {
                    return getTime(r1.getStartMillis()).compareTo(getTime(r2.getStartMillis()));
                }
            }
            Collections.sort(resultList, new ResultComparator());
            for (ITestResult result : resultList) {
                if (getTestParams(result).isEmpty()) {
                    test = extent.createTest(result.getMethod().getMethodName());
                } else {
                    if (getTestParams(result).split(",")[0].contains(result.getMethod().getMethodName())) {
                        test = extent.createTest(getTestParams(result).split(",")[0], getTestParams(result).split(",")[1]);
                    } else {
                        test = extent.createTest(result.getMethod().getMethodName(), getTestParams(result).split(",")[1]);
                    }
                }
                test.getModel().setStartTime(getTime(result.getStartMillis()));
                test.getModel().setEndTime(getTime(result.getEndMillis()));
                for (String group : result.getMethod().getGroups()) {
                    if (!group.isEmpty()) {
                        test.assignCategory(group);
                    } else {
                        int size = result.getMethod().getTestClass().toString().split("\\.").length;
                        String testName = result.getMethod().getRealClass().getName().toString().split("\\.")[size - 1];
                        test.assignCategory(testName);
                    }
                }
                switch (result.getStatus()) {
                    case 1:
                        status = Status.PASS;
                        break;
                    case 2:
                        status = Status.FAIL;
                        break;
                    case 3:
                        status = Status.SKIP;
                        break;
                    default:
                        status = Status.INFO;
                        break;
                }
                if (status.equals(Status.PASS)) {
                    message = "<font color=#00af00>" + status.toString().toUpperCase() + "</font>";
                } else if (status.equals(Status.FAIL)) {
                    message = "<font color=#F7464A>" + status.toString().toUpperCase() + "</font>";
                } else if (status.equals(Status.SKIP)) {
                    message = "<font color=#2196F3>" + status.toString().toUpperCase() + "</font>";
                } else {
                    message = "<font color=black>" + status.toString().toUpperCase() + "</font>";
                }
                test.log(status, message);
                if (!getTestParams(result).isEmpty()) {
                    test.log(Status.INFO, "TEST DATA = [" + getTestParams(result) + "]");
                }
                if (result.getThrowable() != null) {
                    test.log(Status.INFO, "EXCEPTION = [" + result.getThrowable().getMessage() + "]");
                    if (!getTestParams(result).isEmpty()) {
                        if (result.getAttribute("testBitmap") != null) {
                            test.log(Status.INFO, "SCREENSHOT", MediaEntityBuilder.createScreenCaptureFromPath(bitmapDir + result.getAttribute("testBitmap")).build());
                        }
                        test.log(Status.INFO, "STACKTRACE" + getStrackTrace(result));
                    }
                }
            }
        }
    }

    private Date getTime(long millis) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(millis);
        return calendar.getTime();
    }

    private String getTestParams(ITestResult tr) throws Exception {
        TestNG_ConsoleRunner runner = new TestNG_ConsoleRunner();
        return runner.getTestParams(tr);
    }

    private String getStrackTrace(ITestResult result) {
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        result.getThrowable().printStackTrace(printWriter);
        return "<br/>\n" + writer.toString().replace(System.lineSeparator(), "<br/>\n");
    }
}

该类实现了 IReporter 接口,用于生成详细的测试报告。其主要流程如下:
1. generateReport 方法 :遍历所有测试套件,调用 init 方法初始化报告,然后处理每个套件的测试结果,最后刷新报告。
2. init 方法 :创建报告目录,配置 HTML 报告的属性,设置系统信息,并将 HTML 报告附加到 ExtentReports 实例中。
3. processTestResults 方法 :收集测试结果,对结果进行排序,根据测试参数创建测试用例,设置测试用例的开始和结束时间,分配测试类别,根据测试状态设置颜色,并记录测试信息。

2.2 extent-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<extentreports>
    <configuration>
        <theme>standard</theme>
        <encoding>UTF-8</encoding>
        <protocol>https</protocol>
        <documentTitle></documentTitle>
        <reportName>
            <![CDATA[
            ]]>
        </reportName>
        <testViewChartLocation>bottom</testViewChartLocation>
        <reportHeadline></reportHeadline>
        <dateFormat>MM-dd-yyyy</dateFormat>
        <timeFormat>HH:mm:ss</timeFormat>
        <scripts>
            <![CDATA[
                $(document).ready(function() {
                 $('.waves-effect:nth-child(3) a:nth-child(1) i:nth-child(1)').click();
                });
            ]]>
        </scripts>
        <styles>
            <![CDATA[
            .extent {font-size: 12px; font-family: Helvetica Neue, Helvetica, Arial, sans-serif;}
            .nav-wrapper {background: linear-gradient(to left, white 0%, #1a75ff 100%);}
            .side-nav.fixed.hide-on-med-and-down {background: linear-gradient(to top, white 0%, #1a75ff 100%);}
            .logo-container {background: linear-gradient(to bottom,white 0%, #1a75ff 100%);}
            .brand-logo {background: linear-gradient(to right, blue 0%, #1a75ff 100%);}
            .label.suite-start-time {background: linear-gradient(to bottom, red 100%, red 100%);}
            .s2:nth-child(3) .card-panel:nth-child(1) {background-color: #00af00;}
            .s2:nth-child(4) .card-panel:nth-child(1) {background-color: #F7464A;}
            .status.pass {color: green;}
            .status.fail {color: red;}
            .status.skip {color: #1e90ff;}
            .test-status.skip {color: #1e90ff;}
            .test-status.right.skip {color: #1e90ff;}
            .label.others {background-color: #1e90ff;}
            .teal-text > i:nth-child(1) {color: #1e90ff;}
            .category-content .category-status-counts:nth-child(3) {background-color: #1e90ff;}
            .yellow.darken-2 {background-color: #1e90ff !important;}
            ]]>
        </styles>
    </configuration>
</extentreports>

这个 XML 文件用于配置报告的外观和行为,包括主题、编码、协议、日期和时间格式,以及自定义的 JavaScript 和 CSS 样式。

3. 页面对象模型(Page Object Model)

页面对象模型是一种设计模式,它将页面元素和操作封装在类中,提高了测试代码的可维护性和可复用性。这里我们有两个相关的类: PassionTeaCoBasePO.java PassionTeaCoWelcomePO.java

3.1 PassionTeaCoBasePO.java
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import static org.testng.Assert.assertEquals;

/**
 * @author Carl Cocchiaro
 *
 * Passion Tea Company Base Page Object Class
 *
 */
public abstract class PassionTeaCoBasePO<M extends WebElement> {
    protected String pageTitle = "";

    public PassionTeaCoBasePO() throws Exception {
        PageFactory.initElements(CreateDriver.getInstance().getDriver(), this);
    }

    @FindBy(css = "img[src*='01e56eb76d18b60c5fb3dcf451c080a1']")
    protected M passionTeaCoImg;
    @FindBy(css = "img[src*='ab7db4b80e0c0644f5f9226f2970739b']")
    protected M leafImg;
    @FindBy(css = "img[src*='cd390673d46bead889c368ae135a6ec2']")
    protected M organicImg;
    @FindBy(css = "a[href='welcome.html']")
    protected M welcome;
    @FindBy(css = "(//a[@href='menu.html'])[2]")
    protected M menu;
    @FindBy(css = "a[href='our-passion.html']")
    protected M ourPassion;
    @FindBy(css = "a[href='let-s-talk-tea.html']")
    protected M letsTalkTea;
    @FindBy(css = "a[href='check-out.html']")
    protected M checkOut;
    @FindBy(css = "//p[contains(text(),'Copyright')]")
    protected M copyright;

    protected abstract void setTitle(String pageTitle);
    protected abstract String getTitle();

    public void verifyTitle(String title) throws AssertionError {
        WebDriver driver = CreateDriver.getInstance().getDriver();
        assertEquals(driver.getTitle(), title, "Verify Page Title");
    }

    public void navigate(String page) throws Exception {
        WebDriver driver = CreateDriver.getInstance().getDriver();
        BrowserUtils.waitForClickable(By.xpath("//a[contains(text(),'" + page + "')]"), Global_VARS.TIMEOUT_MINUTE);
        driver.findElement(By.xpath("//a[contains(text(),'" + page + "')]")).click();
        BrowserUtils.waitFor(this.getTitle(), Global_VARS.TIMEOUT_ELEMENT);
    }

    public void loadPage(String url, int timeout) throws Exception {
        WebDriver driver = CreateDriver.getInstance().getDriver();
        driver.navigate().to(url);
        BrowserUtils.waitForURL(Global_VARS.TARGET_URL, timeout);
    }

    public void verifySpan(String pattern, String text) throws AssertionError {
        String getText = null;
        WebDriver driver = CreateDriver.getInstance().getDriver();
        getText = driver.findElement(By.xpath("//span[contains(text(),'" + pattern + "')]")).getText();
        assertEquals(getText, text, "Verify Span Text");
    }

    public void verifyHeading(String pattern, String text) throws AssertionError {
        String getText = null;
        WebDriver driver = CreateDriver.getInstance().getDriver();
        getText = driver.findElement(By.xpath("//h1[contains(text(),'" + pattern + "')]")).getText();
        assertEquals(getText, text, "Verify Heading Text");
    }

    public void verifyParagraph(String pattern, String text) throws AssertionError {
        String getText = null;
        WebDriver driver = CreateDriver.getInstance().getDriver();
        getText = driver.findElement(By.xpath("//p[contains(text(),'" + pattern + "')]")).getText();
        assertEquals(getText, text, "Verify Paragraph Text");
    }
}

这是一个抽象类,定义了 Passion Tea Company 页面的基本元素和操作方法,包括验证页面标题、导航到不同页面、加载页面以及验证页面文本等。

3.2 PassionTeaCoWelcomePO.java
import org.openqa.selenium.*;
import org.openqa.selenium.support.FindBy;
import static org.testng.Assert.assertEquals;

/**
 * @author Carl Cocchiaro
 *
 * Passion Tea Company Welcome Sub-class Page Object Class
 *
 */
public class PassionTeaCoWelcomePO<M extends WebElement> extends PassionTeaCoBasePO<M> {
    private static final String WELCOME_TITLE = "Welcome";
    private static final String MENU_TITLE = "Menu";
    protected static enum WELCOME_PAGE_IMG {
        PASSION_TEA_CO, LEAF, ORGANIC, TEA_CUP, HERBAL_TEA, LOOSE_TEA, FLAVORED_TEA
    }
    protected static enum MENU_LINKS {
        MENU, MORE_1, MORE_2, HERBAL_TEA, LOOSE_TEA, FLAVORED_TEA, SEE_COLLECTION1, SEE_COLLECTION2, SEE_COLLECTION3
    }

    public PassionTeaCoWelcomePO() throws Exception {
        super();
        setTitle(WELCOME_TITLE);
    }

    @FindBy(css = "img[src*='7cbbd331e278a100b443a12aa4cce77b']")
    protected M teaCupImg;
    @FindBy(xpath = "//h1[contains(text(),'We're passionate about tea')]")
    protected M caption;
    @FindBy(xpath = "//span[contains(text(),'For more than 25 years')]")
    protected M paragraph;
    @FindBy(css = "a[href='http://www.seleniumframework.com']")
    protected M seleniumFramework;
    @FindBy(xpath = "//span[.='Herbal Tea']")
    protected M herbalTea;
    @FindBy(xpath = "//span[.='Loose Tea']")
    protected M looseTea;
    @FindBy(xpath = "//span[.='Flavored Tea']")
    protected M flavoredTea;
    @FindBy(css = "img[src*='d892360c0e73575efa3e5307c619db41']")
    protected M herbalTeaImg;
    @FindBy(css = "img[src*='18f9b21e513a597e4b8d4c805321bbe3']")
    protected M looseTeaImg;
    @FindBy(css = "img[src*='d0554952ea0bea9e79bf01ab564bf666']")
    protected M flavoredTeaImg;
    @FindBy(xpath = "(//span[contains(@class,'button-content')])[1]")
    protected M flavoredTeaCollect;
    @FindBy(xpath = "(//span[contains(@class,'button-content')])[2]")
    protected M herbalTeaCollect;
    @FindBy(xpath = "(//span[contains(@class,'button-content')])[3]")
    protected M looseTeaCollect;

    @Override
    protected void setTitle(String pageTitle) {
        this.pageTitle = pageTitle;
    }

    @Override
    public String getTitle() {
        return this.pageTitle;
    }

    public void verifyImgSrc(WELCOME_PAGE_IMG img, String src) throws AssertionError {
        String getText = null;
        switch (img) {
            case PASSION_TEA_CO:
                getText = passionTeaCoImg.getAttribute("src");
                break;
            case LEAF:
                getText = leafImg.getAttribute("src");
                break;
            case ORGANIC:
                getText = organicImg.getAttribute("src");
                break;
            case TEA_CUP:
                getText = teaCupImg.getAttribute("src");
                break;
            case HERBAL_TEA:
                getText = herbalTeaImg.getAttribute("src");
                break;
            case LOOSE_TEA:
                getText = looseTeaImg.getAttribute("src");
                break;
            case FLAVORED_TEA:
                getText = flavoredTeaImg.getAttribute("src");
                break;
        }
        assertEquals(getText, src, "Verify Image Source");
    }

    public void navigateMenuLink(MENU_LINKS link, String title) throws Exception {
        String index = null;
        WebDriver driver = CreateDriver.getInstance().getDriver();
        switch (link) {
            case HERBAL_TEA:
                index = "1";
                break;
            case MENU:
                index = "2";
                break;
            case SEE_COLLECTION3:
                index = "3";
                break;
            case MORE_2:
                index = "4";
                break;
            case MORE_1:
                index = "5";
                break;
            case LOOSE_TEA:
                index = "6";
                break;
            case SEE_COLLECTION1:
                index = "7";
                break;
            case SEE_COLLECTION2:
                index = "8";
                break;
            case FLAVORED_TEA:
                index = "9";
                break;
        }
        String query = "(//a[@href='menu.html'])" + "[" + index + "]";
        try {
            driver.findElement(By.xpath(query)).click();
            BrowserUtils.waitFor(MENU_TITLE, Global_VARS.TIMEOUT_ELEMENT);
        } catch (TimeoutException e) {
            BrowserUtils.click(By.xpath(query));
            BrowserUtils.waitFor(MENU_TITLE, Global_VARS.TIMEOUT_ELEMENT);
        }
        assertEquals(MENU_TITLE, title, "Navigate Menu Link");
    }
}

该类继承自 PassionTeaCoBasePO ,是欢迎页面的具体实现,包含了欢迎页面的特定元素和操作方法,如验证图片源和导航菜单链接等。

4. 测试类与数据文件

测试类 PassionTeaCoTest.java 用于执行具体的测试用例,而 PassionTeaCo.json 文件则提供了测试所需的数据。

4.1 PassionTeaCoTest.java
import com.framework.ux.utils.chapter10.PassionTeaCoWelcomePO.WELCOME_PAGE_IMG;
import com.framework.ux.utils.chapter10.PassionTeaCoWelcomePO.MENU_LINKS;
import org.json.simple.JSONObject;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.annotations.*;
import org.testng.annotations.Optional;

/**
 * @author Carl Cocchiaro
 *
 * Passion Tea Co Test Class
 *
 */
public class PassionTeaCoTest {
    private PassionTeaCoWelcomePO<WebElement> welcome = null;
    private static final String DATA_FILE = "src/main/java/com/framework/ux/utils/chapter10/PassionTeaCo.json";

    public PassionTeaCoTest() throws Exception {
    }

    @Parameters({"environment"})
    @BeforeSuite(alwaysRun = true, enabled = true)
    protected void suiteSetup(@Optional(Global_VARS.ENVIRONMENT) String environment, ITestContext context) throws Exception {
        Global_VARS.DEF_ENVIRONMENT = System.getProperty("environment", environment);
        Global_VARS.SUITE_NAME = context.getSuite().getXmlSuite().getName();
    }

    @AfterSuite(alwaysRun = true, enabled = true)
    protected void suiteTeardown() throws Exception {
    }

    @Parameters({"browser", "platform", "includePattern", "excludePattern"})
    @BeforeTest(alwaysRun = true, enabled = true)
    protected void testSetup(@Optional(Global_VARS.BROWSER) String browser,
                             @Optional(Global_VARS.PLATFORM) String platform,
                             @Optional String includePattern,
                             @Optional String excludePattern,
                             ITestContext ctxt) throws Exception {
        if (includePattern != null) {
            System.setProperty("includePattern", includePattern);
        }
        if (excludePattern != null) {
            System.setProperty("excludePattern", excludePattern);
        }
        Global_VARS.DEF_BROWSER = System.getProperty("browser", browser);
        Global_VARS.DEF_PLATFORM = System.getProperty("platform", platform);
        CreateDriver.getInstance().setDriver(Global_VARS.DEF_BROWSER, Global_VARS.DEF_PLATFORM, Global_VARS.DEF_ENVIRONMENT);
    }

    @AfterTest(alwaysRun = true, enabled = true)
    protected void testTeardown() throws Exception {
        CreateDriver.getInstance().closeDriver();
    }

    @BeforeClass(alwaysRun = true, enabled = true)
    protected void testClassSetup(ITestContext context) throws Exception {
        welcome = new PassionTeaCoWelcomePO<WebElement>();
        JSONDataProvider.dataFile = DATA_FILE;
        welcome.loadPage(Global_VARS.TARGET_URL, Global_VARS.TIMEOUT_MINUTE);
    }

    @AfterClass(alwaysRun = true, enabled = true)
    protected void testClassTeardown(ITestContext context) throws Exception {
    }

    @BeforeMethod(alwaysRun = true, enabled = true)
    protected void testMethodSetup(ITestResult result) throws Exception {
    }

    @AfterMethod(alwaysRun = true, enabled = true)
    protected void testMethodTeardown(ITestResult result) throws Exception {
        WebDriver driver = CreateDriver.getInstance().getDriver();
        if (!driver.getCurrentUrl().contains("welcome.html")) {
            welcome.setTitle("Welcome");
            welcome.navigate("Welcome");
        }
    }

    @Test(groups = {"PASSION_TEA"},
            dataProvider = "fetchData_JSON",
            dataProviderClass = JSONDataProvider.class,
            enabled = true)
    public void tc001_passionTeaCo(String rowID,
                                   String description,
                                   JSONObject testData) throws Exception {
        welcome.setTitle(testData.get("title").toString());
        welcome.navigate(testData.get("menu").toString());
        welcome.verifyTitle(testData.get("title").toString());
    }

    @Test(groups = {"PASSION_TEA"},
            dataProvider = "fetchData_JSON",
            dataProviderClass = JSONDataProvider.class,
            enabled = true)
    public void tc002_passionTeaCo(String rowID,
                                   String description,
                                   JSONObject testData) throws Exception {
        welcome.verifyImgSrc(WELCOME_PAGE_IMG.valueOf(testData.get("img").toString()), testData.get("src").toString());
    }

    @Test(groups = {"PASSION_TEA"},
            dataProvider = "fetchData_JSON",
            dataProviderClass = JSONDataProvider.class,
            enabled = true)
    public void tc003_passionTeaCo(String rowID,
                                   String description,
                                   JSONObject testData) throws Exception {
        welcome.verifySpan(testData.get("pattern").toString(), testData.get("text").toString());
    }

    @Test(groups = {"PASSION_TEA"},
            dataProvider = "fetchData_JSON",
            dataProviderClass = JSONDataProvider.class,
            enabled = true)
    public void tc004_passionTeaCo(String rowID,
                                   String description,
                                   JSONObject testData) throws Exception {
        welcome.verifyHeading(testData.get("pattern").toString(), testData.get("text").toString());
    }

    @Test(groups = {"PASSION_TEA"},
            dataProvider = "fetchData_JSON",
            dataProviderClass = JSONDataProvider.class,
            enabled = true)
    public void tc005_passionTeaCo(String rowID,
                                   String description,
                                   JSONObject testData) throws Exception {
        welcome.verifyParagraph(testData.get("pattern").toString(), testData.get("text").toString());
    }

    @Test(groups = {"PASSION_TEA"},
            dataProvider = "fetchData_JSON",
            dataProviderClass = JSONDataProvider.class,
            enabled = true)
    public void tc006_passionTeaCo(String rowID,
                                   String description,
                                   JSONObject testData) throws Exception {
        welcome.navigateMenuLink(MENU_LINKS.valueOf(testData.get("element").toString()), testData.get("title").toString());
    }
}

该测试类包含了多个测试方法,通过数据驱动的方式从 PassionTeaCo.json 文件中获取测试数据,执行不同的测试场景,如页面导航、图片源验证、文本验证等。

4.2 PassionTeaCo.json
{
    "tc001_passionTeaCo": [
        {
            "rowID": "tc001_passionTeaCo.01",
            "description": "Navigate Passion Tea Co 'Welcome' Page",
            "menu": "Welcome",
            "title": "Welcome"
        },
        {
            "rowID": "tc001_passionTeaCo.02",
            "description": "Navigate Passion Tea Co 'Our Passion' Page",
            "menu": "Our Passion",
            "title": "Our Passion"
        },
        {
            "rowID": "tc001_passionTeaCo.03",
            "description": "Navigate Passion Tea Co 'Menu' Page",
            "menu": "Menu",
            "title": "Menu"
        },
        {
            "rowID": "tc001_passionTeaCo.04",
            "description": "Navigate Passion Tea Co 'Let's Talk Tea' Page",
            "menu": "Talk Tea",
            "title": "Let's Talk Tea"
        },
        {
            "rowID": "tc001_passionTeaCo.05",
            "description": "Navigate Passion Tea Co 'Check Out' Page",
            "menu": "Check Out",
            "title": "Check Out"
        }
    ],
    "tc002_passionTeaCo": [
        {
            "rowID": "tc002_passionTeaCo.01",
            "description": "Verify Image Source 'TEA CUP'",
            "img": "TEA_CUP",
            "src": "http://nebula.wsimg.com/7cbbd331e278a100b443a12aa4cce77b? AccessKeyId=7ECBEB9592E2269F1812&disposition=0&alloworigin=1"
        },
        {
            "rowID": "tc002_passionTeaCo.02",
            "description": "Verify Image Source 'HERBAL TEA'",
            "img": "HERBAL_TEA",
            "src": "http://nebula.wsimg.com/d892360c0e73575efa3e5307c619db41? AccessKeyId=7ECBEB9592E2269F1812&disposition=0&alloworigin=1"
        },
        {
            "rowID": "tc002_passionTeaCo.03",
            "description": "Verify Image Source 'LOOSE TEA'",
            "img": "LOOSE_TEA",
            "src": "http://nebula.wsimg.com/18f9b21e513a597e4b8d4c805321bbe3? AccessKeyId=7ECBEB9592E2269F1812&disposition=0&alloworigin=1"
        },
        {
            "rowID": "tc002_passionTeaCo.04",
            "description": "Verify Image Source 'FLAVORED TEA'",
            "img": "FLAVORED_TEA",
            "src": "http://nebula.wsimg.com/d0554952ea0bea9e79bf01ab564bf666? AccessKeyId=7ECBEB9592E2269F1812&disposition=0&alloworigin=1"
        },
        {
            "rowID": "tc002_passionTeaCo.05",
            "description": "Verify Image Source 'PASSION TEA CO'",
            "img": "PASSION_TEA_CO",
            "src": "http://nebula.wsimg.com/01e56eb76d18b60c5fb3dcf451c080a1? AccessKeyId=7ECBEB9592E2269F1812&disposition=0&alloworigin=1"
        },
        {
            "rowID": "tc002_passionTeaCo.06",
            "description": "Verify Image Source 'LEAF'",
            "img": "LEAF",
            "src": "http://nebula.wsimg.com/ab7db4b80e0c0644f5f9226f2970739b? AccessKeyId=7ECBEB9592E2269F1812&disposition=0&alloworigin=1"
        },
        {
            "rowID": "tc002_passionTeaCo.07",
            "description": "Verify Image Source 'ORGANIC'",
            "img": "ORGANIC",
            "src": "http://nebula.wsimg.com/cd390673d46bead889c368ae135a6ec2? AccessKeyId=7ECBEB9592E2269F1812&disposition=0&alloworigin=1"
        }
    ],
    "tc003_passionTeaCo": [
        {
            "rowID": "tc003_passionTeaCo.01",
            "description": "Verify Span Text 'See our line of organic teas.'",
            "pattern": "See our line",
            "text": "See our line of organic teas."
        },
        {
            "rowID": "tc003_passionTeaCo.02",
            "description": "Verify Span Text 'Tea of the month club'",
            "pattern": "month club",
            "text": "Tea of the month club"
        },
        {
            "rowID": "tc003_passionTeaCo.03",
            "description": "Verify Span Text 'It's the gift that keeps on giving all year long.'",
            "pattern": "gift that keeps on giving",
            "text": "It's the gift that keeps on giving all year long."
        },
        {
            "rowID": "tc003_passionTeaCo.04",
            "description": "Verify Span Text 'For more than 25 years...'",
            "pattern": "For more than 25 years, Passion Tea Company has revolutionized the tea industry",
            "text": "For more than 25 years, Passion Tea Company has revolutionized the tea industry by letting our customers create a blend that combines their favorite herbs and spices. We offer thousands of natural flavors from all over the world  and want you to have the opportunity to create a tea, and call it yours! We proudly partner with seleniumframework.com to help them use our website for Continuous Test Automation  practice exercises "
        },
        {
            "rowID": "tc003_passionTeaCo.05",
            "description": "Verify Span Text 'Herbal Tea'",
            "pattern": "Herbal Tea",
            "text": "Herbal Tea"
        },
        {
            "rowID": "tc003_passionTeaCo.06",
            "description": "Verify Span Text 'Loose Tea'",
            "pattern": "Loose Tea",
            "text": "Loose Tea"
        },
        {
            "rowID": "tc003_passionTeaCo.07",
            "description": "Verify Span Text 'Flavored Tea'",
            "pattern": "Flavored Tea",
            "text": "Flavored Tea."
        }
    ],
    "tc004_passionTeaCo": [
        {
            "rowID": "tc004_passionTeaCo.01",
            "description": "Verify Heading Text 'We're passionate about tea.'",
            "pattern": "passionate about tea",
            "text": "We're passionate about tea. "
        }
    ],
    "tc005_passionTeaCo": [
        {
            "rowID": "tc005_passionTeaCo.01",
            "description": "Verify Paragraph Text 'Copyright...'",
            "pattern": "Copyright",
            "text": "Copyright Selenium Practice Website. All rights reserved."
        }
    ],
    "tc006_passionTeaCo": [
        {
            "rowID": "tc006_passionTeaCo.01",
            "description": "Verify Menu Link Text 'MENU'",
            "element": "MENU",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.02",
            "description": "Verify Menu Link Text 'MORE 1'",
            "element": "MORE_1",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.03",
            "description": "Verify Menu Link Text 'MORE 2'",
            "element": "MORE_2",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.04",
            "description": "Verify Menu Link Text 'HERBAL TEA'",
            "element": "HERBAL_TEA",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.05",
            "description": "Verify Menu Link Text 'LOOSE TEA'",
            "element": "LOOSE_TEA",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.06",
            "description": "Verify Menu Link Text 'FLAVORED TEA'",
            "element": "FLAVORED_TEA",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.07",
            "description": "Verify Menu Link Text 'SEE COLLECTION 1'",
            "element": "SEE_COLLECTION1",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.08",
            "description": "Verify Menu Link Text 'SEE COLLECTION 2'",
            "element": "SEE_COLLECTION2",
            "title": "Menu"
        },
        {
            "rowID": "tc006_passionTeaCo.09",
            "description": "Verify Menu Link Text 'SEE COLLECTION 3'",
            "element": "SEE_COLLECTION3",
            "title": "Menu"
        }
    ]
}

这个 JSON 文件为每个测试方法提供了相应的测试数据,包括行 ID、描述、菜单选项、页面标题、图片源、文本模式和验证文本等。

5. 总结

通过以上的配置文件、报告生成类、页面对象模型和测试类,我们构建了一个完整的自动化测试框架。这个框架可以帮助我们更高效地进行 Web 应用的测试,提高测试的覆盖率和准确性。同时,通过数据驱动的方式,我们可以轻松地扩展测试用例,适应不同的测试场景。

以下是一个简单的测试执行流程的 mermaid 流程图:

graph LR
    A[开始] --> B[套件初始化]
    B --> C[测试初始化]
    C --> D[类初始化]
    D --> E[方法初始化]
    E --> F[执行测试用例]
    F --> G[方法清理]
    G --> H{是否还有测试用例}
    H -- 是 --> E
    H -- 否 --> I[类清理]
    I --> J[测试清理]
    J --> K[套件清理]
    K --> L[结束]

这个流程图展示了测试执行的主要步骤,从套件初始化开始,依次进行测试、类和方法的初始化,执行测试用例,再进行相应的清理工作,最后结束测试。

在实际应用中,我们可以根据具体的需求对这个框架进行进一步的扩展和优化,例如添加更多的测试方法、优化报告格式、调整页面元素定位方式等。希望这个框架能为你的自动化测试工作带来便利和帮助。

自动化测试实战:Selenium 与相关工具的综合应用

6. 框架优势分析

这个自动化测试框架具有多方面的优势,以下为详细分析:
- 可维护性高 :页面对象模型将页面元素和操作封装在类中,当页面元素发生变化时,只需修改对应的页面对象类,而不需要修改大量的测试代码。例如,若 PassionTeaCoWelcomePO 页面中的某个图片元素的 src 属性发生变化,仅需在该类中更新相应的元素定位信息。
- 可复用性强 :页面对象类中的方法可以在多个测试用例中复用。如 PassionTeaCoBasePO 中的 navigate 方法,可用于在不同的测试用例中导航到不同的页面。
- 数据驱动 :通过 PassionTeaCo.json 文件提供测试数据,实现了测试代码和测试数据的分离。这样可以方便地添加、修改和删除测试数据,扩展测试用例。例如,若要增加一个新的页面导航测试用例,只需在 tc001_passionTeaCo 数组中添加一条新的数据记录。
- 详细的测试报告 :使用 ExtentReports 生成的测试报告包含了丰富的信息,如测试用例的执行结果、执行时间、异常信息和截图等,方便测试人员快速定位和解决问题。

7. 框架的可扩展性

为了使框架能够适应不同的测试需求,我们可以从以下几个方面进行扩展:
- 添加新的页面对象类 :当需要测试新的页面时,可以创建新的页面对象类。例如,若要测试 PassionTeaCo 的购物车页面,可以创建一个 PassionTeaCoCartPO 类,继承自 PassionTeaCoBasePO ,并实现购物车页面的特定元素和操作方法。
- 增加测试方法 :根据不同的测试场景,在 PassionTeaCoTest 类中添加新的测试方法。例如,添加一个测试购物车结算功能的方法。

@Test(groups = {"PASSION_TEA"},
        dataProvider = "fetchData_JSON",
        dataProviderClass = JSONDataProvider.class,
        enabled = true)
public void tc007_passionTeaCo(String rowID,
                               String description,
                               JSONObject testData) throws Exception {
    // 实现购物车结算功能的测试逻辑
}
  • 优化报告格式 :可以通过修改 extent-config.xml 文件,进一步定制测试报告的外观和功能。例如,添加更多的自定义样式,或者修改报告的布局。
  • 集成其他工具 :可以将该框架与其他测试工具集成,如持续集成工具(Jenkins)、缺陷管理工具(JIRA)等,实现自动化测试的持续执行和缺陷的自动跟踪。
8. 框架的实际应用案例

以下是一个简单的表格,展示了使用该框架进行测试的部分测试用例及其执行结果:
| 测试用例 ID | 测试用例描述 | 执行结果 |
| — | — | — |
| tc001_passionTeaCo.01 | 导航到 Passion Tea Co ‘Welcome’ 页面 | 通过 |
| tc002_passionTeaCo.03 | 验证 ‘LOOSE TEA’ 图片源 | 通过 |
| tc003_passionTeaCo.04 | 验证特定 span 文本 | 通过 |

9. 总结与展望

这个自动化测试框架通过合理的配置和设计,实现了 Web 应用的自动化测试。它利用页面对象模型提高了代码的可维护性和可复用性,通过数据驱动的方式方便了测试用例的扩展,同时使用 ExtentReports 生成详细的测试报告。

在未来的发展中,我们可以进一步优化这个框架。例如,引入更智能的元素定位策略,提高元素定位的准确性和稳定性;集成更多的测试工具,实现更全面的测试覆盖;加强对异步操作的处理,提高测试的可靠性。

以下是一个框架未来发展方向的 mermaid 流程图:

graph LR
    A[现有框架] --> B[优化元素定位策略]
    A --> C[集成更多测试工具]
    A --> D[加强异步操作处理]
    B --> E[提高定位准确性和稳定性]
    C --> F[实现更全面测试覆盖]
    D --> G[提高测试可靠性]
    E --> H[更高效的测试框架]
    F --> H
    G --> H

这个流程图展示了框架未来的主要发展方向,通过对元素定位、工具集成和异步操作处理的优化,最终实现一个更高效、更可靠的自动化测试框架。

总之,这个自动化测试框架为 Web 应用的测试提供了一个坚实的基础,通过不断的优化和扩展,它将能够更好地满足各种复杂的测试需求,为软件质量的保障提供有力支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值