自动化测试框架的Step By Step搭建及测试实战

本文永久更新地址:https://my.oschina.net/bysu/blog/1635592

写在前面:若有侵权,请发邮件[email protected]告知。

本文主要是学习《Selenium WebDriver实战宝典》过程中的摘录。

转载者告知如果本文被转载,但凡涉及到侵权相关事宜,转载者需负责。请知悉!

本章为高级自动化工程师的必修内容。也是本书最具吸引力的一章。本章将从零开始搭建一个完整的自动化测试框架,请立志成为高级自动化测试专家的读者仔细阅读,建议参照本章的内容在本地计算机环境进行搭建实践。笔者认为,只有不断地实践,才能让读者真正具备自动化测试框架的搭建能力。


16.1什么是自动化测试框架


大多数的测试从业者都是从手工测试开始的职业生涯的,经过多年的手工测试后,手工测试人员开始沉思自己的未来发展之路,难道要一辈子靠手工的方式来完成测试吗?一些手工测试工程师开始尝试使用自动化测试工具来替代执行自己每天不断重复的手工测试过程。但是在执行过程中,他们会发现好不容易写好的测试脚本,因为需求变化的原因没过多久就无法执行成功了。这样的情况在软件开发过程中是不可避免的,测试工程师只能去不断修改和维护自动化测试脚本。但是没过多长时间,被测试软件的需求又发生了或多或少的变化或调整,导致原有的自动化测试脚本义无法运行了。这样的情况反复出现后,测试工程师发现投入维护脚本的时间和精力比纯手工测试的方式还要多,而且还造成手工测试的时间被明显减少,导致测试的效果大打折扣,软件的质量还不如以前纯手工测试的时候好。有的时候测试脚本刚刚修改一半,软件的需求又发生了变化,这样的情况导致测试工程师只能放弃自动化测试脚本的维护,被迫重新投入到手工测试中。以上场景大量地出现在各个尝试自动化测试的公司中,大家也在思考和尝试解决这样的问题: 如何能够降低测试脚本的维护成本和工作量,提消自动化测试脚本的编写和维护效率,真正让自动化测试能够提高软件测试工程师的工作效率,为企业真正节省测试成本,能够为开发团队快速地反馈当前软件质量状态? 在这样的历史时期,自动化测试框架应运而生。
1.什么是自动化测试框架
自动化测试框架是应用于自动化测试的程序框架,它提供了可重用的自动化测试模块,提供最基础的自动化测试功能(例如,打开浏览器、单击链接等功能),或提供自动化测试执行和管理功能的架构模块(例如:TestNG),它是由一个或多个自动化测试基础模块、自动化测试管理模块、自动化测试统计模块等组成的工具集合。
2.自动化测试框架常见的4 种模式
1) 数据驱动测试框架
使用数据数组、测试数据文件或者数据库等方式作为测试过程输入的自动化测试框架,此框架可以将所有测试数据在自动化测试执行的过程中进行自动加载,动态判断测试结果是否符合预期,并自动输出测试报告。此框架一般用于在一个测试流程中使用多组不同的测试数据,以此来验证被测试系统是否能够正常工作。

2) 关键字驱动测试框架
关键字驱动测试框架可以理解为高级的数据驱动测试框架,使用被操作的元素对象、操作的方法和操作的数据值作为测试过程输入的自动化测试框架,简单表示为item.operation (value )。被操作的元素对象、操作的方法和操作的数据值可以保存任数据数组、数据文件、数据库中作为关键字驱动测试框架的输入,例如在页面上的用户名输入框中输入用户名,则可以在数据文件中进行如下定义:

用户名输入框,输入,testman

关键字驱动测试框架属于更加高级的自动化测试框架,可以兼容更多的自动化测试操作类型,大大提高了自动化测试框架的使用灵活性。
3) 混合型测试框架
在关键字驱动测试框架中加入了数据驱动的功能,则框架被定义为混合型测试。
4) 行为驱动测试框架
支持自然语言作为测试用例描述的自动化测试框架,例如前面章节讲到的Cucumber框架。
3.自动化测试框架的作用
(1)能够有效组织和管理测试脚本。
(2) 进行数据驱动或者关键字驱动的测试。
(3) 将基础的测试代码进行封装,降低测试脚本编写的复杂性和重复性。
(4) 提高测试脚本维护和修改的效率。
(5) 自动执行测试脚本,并自动发布测试报告,为持续集成的开发方式提供脚本支持。
(6) 让不具各编程能力的测试工程师开展自动化测试工作。
4.自动化测试框架的设计核心思想
世上没有最好的自动化测试框架,也没有万能的自动化测试测试框架,各种自动化测试框架都有自身的优点和缺点,所以我们在设计自动化测试框架的时候要考虑到实现一套自动化测试框架到底能够为测试工作本身解决什么样的具体问题,不能为了自动化而自动化,我们要以解决测试中的问题和提高测试工作的效率为主要导向来进行自动化测试框架的设计。
自动化测试框架的核心思想是将常用的脚本代或者测试逻辑进行抽象和总结,然后将这些代码进行面向对象设计,将需要复用的代码封装到可公用的类方法中。通过调用公用的类方法,测试类中的脚本复杂度会被大大降低,让更多脚本能力不强的测试人员来实施自动化测试。
创建和实施Web 自动化测试框架的步骤如下:
(1) 根据测试业务的手工测试用例,选出需要可自动化执行的测试用例。
(2) 根据可自动化执行的测试用例,分析出测试框架需要模拟的手工操作和重复高的测试流程或逻辑。
(3) 将手工操作和重复高的测试逻辑用在代码中实现,并在类中进行封装方法的编写。
(4) 根据测试业务的类型和本身技术能力,选择数据驱动框架、关键字驱动框架、混合型框架还是行为驱动框架。
(5) 确定框架模型后,将框架中常用的浏览器选择、测试数据处理、文件操作、数据库操作,页面元素的原始操作、日志和报告等功能进行类方法的封装实现。
(6) 对框架代码进行集成测试和系统测试,采用PageObject 模式和TestNG 框架(或JUnit) 编写测试脚本,使用框架进行自动化测试,验证框架的功能可以满足自动化测试的需求。
(7) 编写自动化测试框架的常用API 文档,以供他人参阅。
(8) 在测试组内部进行培训和推广。
(9) 不断收集测试过程中的框架使用问题和反馈意见,不断增加和优化自动化框架的功能,不断增强框架中复杂操作的封装效果,尽量降低测试脚本的编写复杂性。
(10) 定期评估测试框架的使用效果,评估自动化测试的投入产出比,再逐步推广自动化框架的应用范围。


16.2数据驱动框架及实战


本节主要讲解数据驱动测试框架的搭建,并且使用此框架来测试163 邮箱登录和地址薄的相关功能。框架用到的基础知识均在前面的章节做了详细介绍,本章节重点讲解框架搭建的详细过程。

被测试功能的相关页面描述:

登录页面如图1所示。
登录后页面如图2 所示。
单击“通讯录”链接后,进入通讯录主页,如图3所示。

单击“新建联系人”按钮后,弹出“新建联系人”对话框,如图4所示。

输入联系人信息后,单击“确定”按钮保存,显示的页面如图5所示。

数据驱动框架搭建步骤:
(1) 新建一个Java工程,命名为DataDrivenFrameWork,并按照前面章节描述配置好工程中的WebDriver 和TestNG 环境,并导入Excel 操作相关和Log4j 相关的JAR 文件到工程中。
(2) 在工程中新建4 个Package,分别命名为:
cn.gloryroad.appMode,主要用于实现复用的业务逻辑封装方法。
cn.gloryroad.pageObject,主要用于实现被测试对象的页面对象。
cn.gloryroad.testScripts, 主要用于实现具体的测试脚本逻辑。
cn.gloryroad.util,主要用于实现测试过程中调用的工具类方法,例如文件操作、mapObject、页面元素的操作方法等。
(3) 在cn .gloryroad.util 的Package下新建ObjectMap 类,用于实现在外部配置文件中配置页面元素的定位表达式。ObjectMap 的代码如下。
 

package main.cn.gloryroad.util;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

import org.openqa.selenium.By;

public class ObjectMap {

	Properties properties;

	public ObjectMap(String propFile) {
		properties = new Properties();

		try {
			FileInputStream in = new FileInputStream(propFile);
			properties.load(in);
		} catch (IOException e) {
			System.out.println("读取对象文件出错~!");
			e.printStackTrace();
		}
	}

	public By getLocator(String ElementNameInpropFile) throws Exception{
		//根据变量ElementNameInpropFile,从属性配置文件中读取对应的配置对象
		String locator = properties.getProperty(ElementNameInpropFile);
		//将配置对象中的定位类型存到locatorType变量,将定位表达式的值存入locatorValue变量
		String locatorType = locator.split(">")[0];
		String locatorValue = locator.split(">")[1];
		
		/**
		 *在Ecplise中的配置文件均默认为ISO-8859-1编码存储,使用getBytes方法可以将字符串编码转换
		 *为UTF-8编码,以此来解决在配置文件读取中文乱码的问题
		 * 
		 **/
		locatorValue = new String(locatorValue.getBytes("ISO-8859-1"),"UTF-8");
		//输出locatorType变量值和locatorValue变量值,验证是否赋值正确
		System.out.println("获取定位类型:" + locatorType + "\t 获取定位表达式" + locatorValue);
		//根据locatorType的变量值内容判断返回何种定位方式的By对象
		
		locatorType = locatorType.toLowerCase();
		switch(locatorType) {
		case "id":
			return By.id(locatorValue);
		case "name":
			return By.id(locatorValue);
		case "class":
			return By.id(locatorValue);
		case "tag":
			return By.id(locatorValue);
		case "linktext":
			return By.id(locatorValue);
		case "link":
			return By.id(locatorValue);
		case "partiallinktext":
			return By.id(locatorValue);
		case "cssselector":
			return By.id(locatorValue);
		case "css":
			return By.id(locatorValue);
		case "xpath":
			return By.id(locatorValue);
		default:
			throw new Exception("输入的locator type未在程序中被定义:" + locatorType);
		}
/*	if(locatorType.toLowerCase().equals("id")) {
			return By.id(locatorValue);
		}else if(locatorType.toLowerCase().equals("name")) {
			return By.name(locatorValue);
		}else if(locatorType.toLowerCase().equals("class")) {
			return By.className(locatorValue);
		}else if(locatorType.toLowerCase().equals("tag")) {
			return By.tagName(locatorValue);
		}else if((locatorType.toLowerCase().equals("linktext")) || (locatorType.toLowerCase().equals("link"))) {
			return By.linkText(locatorValue);
		}else if(locatorType.toLowerCase().equals("partiallinktext")) {
			return By.partialLinkText(locatorValue);
		}else if(locatorType.toLowerCase().equals("cssselector") || (locatorType.toLowerCase().equals("css"))){
			return By.cssSelector(locatorValue);
		}else if(locatorType.toLowerCase().equals("xpath")) {
			return By.xpath(locatorValue);
		}else {
			throw new Exception("输入的locator type未在程序中被定义:" + locatorType);
		}*/
	}
}

4.在工程中添加一个存储页面定位方式和定位表达式的配置文件objectMap.properties,文件内容如下:

163mail.loginPage.username=id>idInput
163mail.loginPage.password=id>pwdInput
163mail.loginPage.loginbutton=id>loginBtn

测试页面中的所有页面元素的定位方式和定位表达式均可以在此文件中进行定义,实现定位数据和测试程序的分离。在一个配置文件中修改定位数据,可以在测试脚本中全局生效,此方式可以大大提高定位表达式的维护效率。

5.在main.cn.gloryroad.pageObjects的Package下新建LoginPage类,用于实现163邮箱登录页面的PageObject对象。Login类代码如下:

package main.cn.gloryroad.pageObjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import main.cn.gloryroad.util.ObjectMap;

public class LoginPage {

	private WebElement element = null;
	// 指定页面元素定位表达式配置文件的绝对路径
	private ObjectMap objectMap = new ObjectMap(".//objectMap.properties");
	private WebDriver driver;

	public LoginPage(WebDriver driver) {
		this.driver = driver;
	}

	// 返回登录页面中的用户名输入框页面元素对象
	public WebElement userName() throws Exception {
		// 使用objectMap类中的getLocator方法获取配置文件中关于用户名的定位方式和定位表达式
		element = driver.findElement(objectMap.getLocator("163mail.loginPage.username"));
		return element;
	}

	// 返回登录页面中的密码输入框页面元素对象
	public WebElement password() throws Exception {
		// 使用objectMap类中的getLocator方法获取配置文件中关于用户名的定位方式和定位表达式
		element = driver.findElement(objectMap.getLocator("163mail.loginPage.password"));
		return element;
	}

	// 返回登录页面中的登录按钮页面元素对象
	public WebElement loginButton() throws Exception {
		// 使用objectMap类中的getLocator方法获取配置文件中关于用户名的定位方式和定位表达式
		element = driver.findElement(objectMap.getLocator("163mail.loginPage.loginbutton"));
		return element;
	}
}

6.在main.cn.gloryroad.testScript的Package中新建TestMail163Login测试类,具体代码如下:

package main.cn.gloryroad.testScripts;

import java.util.concurrent.TimeUnit;

import org.junit.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import main.cn.gloryroad.pageObjects.LoginPage;

public class TestMail163Login {

	public WebDriver driver;
	String baseUrl = "https://mail.163.com/";
	@Test
	public void testMailLogin() throws Exception {
		driver.get(baseUrl + "/"); 
		LoginPage loginPage = new LoginPage(driver);
		loginPage.userName().sendKeys("testusernm");
		loginPage.password().sendKeys("testpasswd");
		loginPage.loginButton().click();
		Thread.sleep(5000);
		
		Assert.assertTrue(driver.getPageSource().contains("未读邮件"));
	}
	
	@BeforeMethod
	public void beforeMethod() {
		//设定Chrome浏览器启动文件的绝对路径,其他浏览器https://my.oschina.net/bysu/blog/828254
		System.setProperty("webdriver.chrome.driver", "C:/Program Files (x86)/Google/Chrome/Application/chromedriver.exe");  
		driver = new ChromeDriver();
		//webdriver自己实现的模糊等待,而webdriver的针对某个元素的明确等待(explicitly wait)
		driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
	}
	
	@AfterMethod
	public void afterMethod() {
		driver.quit();
	}
}

7.在main.cn.gloryroad.appModules中增加LoginAction类,具体类代码如下:

package main.cn.gloryroad.appModules;

import org.openqa.selenium.WebDriver;

import main.cn.gloryroad.pageObjects.LoginPage;

public class LoginAction {

	public static void execute (WebDriver driver,String userName,String passWord) throws Exception{
		driver.get("https://mail.163.com/");
		LoginPage loginPage = new LoginPage(driver);
		loginPage.userName().sendKeys(userName);
		loginPage.password().sendKeys(passWord);
		loginPage.loginButton().click();
		Thread.sleep(5000);
	}
}

由于登陆过程是其他测试过程的前提条件,所以将登录的操作逻辑封装在LoginAction类的execute方法中,方便其他测试脚本进行调用。

8.修改TestMail163Login类的代码,具体代码如下:

package main.cn.gloryroad.testScripts;

import java.util.concurrent.TimeUnit;

import org.junit.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import main.cn.gloryroad.appModules.LoginAction;
import main.cn.gloryroad.pageObjects.LoginPage;

public class TestMail163Login {

	public WebDriver driver;
	String baseUrl = "https://mail.163.com/";
	@Test
	public void testMailLogin() throws Exception {
		driver.get(baseUrl + "/"); 
		LoginAction.execute(driver, "testUserNM", "testPassWD");
		Thread.sleep(5000);
		Assert.assertTrue(driver.getPageSource().contains("未读邮件"));
	}
	
	@BeforeMethod
	public void beforeMethod() {
		//设定Chrome浏览器启动文件的绝对路径,其他浏览器https://my.oschina.net/bysu/blog/828254
		System.setProperty("webdriver.chrome.driver", "C:/Program Files (x86)/Google/Chrome/Application/chromedriver.exe");  
		driver = new ChromeDriver();
		//webdriver自己实现的模糊等待,而webdriver的针对某个元素的明确等待(explicitly wait)
		driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
	}
	
	@AfterMethod
	public void afterMethod() {
		driver.quit();
	}
}

比较TestMail163Login类的前后代码,可以发现多个登录操作的步骤被一个函数替代了。函数为LoginAction.execute(driver, "testUserNM", "testPassWD");。此种方式实现了业务逻辑的封装。只要调用一个函数均可以实现登录的操作,大大减少了测试脚本的重复编写。这就是封装的作用。

9.在main.cn.gloryroad.pageObjects的Package中新建Homepage和AddressBookPage类,并在配置文件objectMap.properties中补充新的定位表达式。

objectMap.properties配置文件更新后的内容如下:

163mail.loginPage.username=id>idInput
163mail.loginPage.password=id>pwdInput
163mail.loginPage.loginbutton=id>loginBtn
163mail.homePage.addressbook=xpath>//*[@id="_mail_tabitem_1_45text"][contains(text(),'通讯录')]
163mail.addressBook.createContactPerson=xpath>//*[@id="_mail_button_64_452"]/span[2]
163mail.addressBook.contactPersonName=xpath>//*[@id="input_N"]
163mail.addressBook.contactPersonName=xpath>//*[@id="_mail_input_6_487"]/input
163mail.addressBook.=xpath>//*[@id="_mail_input_7_492"]/input
163mail.addressBook.savaButton=xpath>//*[@id="_mail_button_81_527"]/span

HomePage类代码如下:

package main.cn.gloryroad.pageObjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import main.cn.gloryroad.util.ObjectMap;

public class HomePage {

	private WebElement element = null;
	private ObjectMap objectMap = new ObjectMap("./objectMap.properties");
	private WebDriver driver;
	
	public HomePage(WebDriver driver) {
		this.driver = driver;
	}
	
	//获取登录后主页中的“通讯录”链接
	public WebElement addressLink() throws Exception{
		element = driver.findElement(objectMap.getLocator("163mail.homePage.addressbook"));
		return element;
	}
	
	//如果需要在HomePage页面操作更多的链接或元素,可以根据需要自定义
}

AddressBookPage类代码如下:

package main.cn.gloryroad.pageObjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import main.cn.gloryroad.util.ObjectMap;

public class AddressBookPage {
	private WebElement element = null;
	private ObjectMap objectMap = new ObjectMap("./objectMap.properties");
	private WebDriver driver;

	public AddressBookPage(WebDriver driver) {
		this.driver = driver;
	}

	// 获取新建联系人
	public WebElement createContactPersonButton() throws Exception {
		element = driver.findElement(objectMap.getLocator("163mail.addressBook.createContactPerson"));
		return element;
	}

	// 获取新建联系人界面中的姓名输入框
	public WebElement contactPersonName() throws Exception {
		element = driver.findElement(objectMap.getLocator("163mail.addressBook.contactPersonName"));
		return element;
	}

	// 获取新建联人界面中的电子邮件输入框
	public WebElement contactPersonEmail() throws Exception {
		element = driver.findElement(objectMap.getLocator("163mail.addressBook.contactPersonEmail"));
		return element;
	}

	// 获取新建联系人界面中的手机号码输入框
	public WebElement contactPersonMobile() throws Exception {
		element = driver.findElement(objectMap.getLocator("163mail.addressBook.contactPersonMobile"));
		return element;
	}

	// 获取新建联系人界面中保存信息的“确定”按钮
	public WebElement saveButton() throws Exception {
		element = driver.findElement(objectMap.getLocator("163mail.addressBook.saveButton"));
		return element;
	}
}

在main.cn.gloryroad.appModules中增加AddContactPersonAction类,具体类代码如下:

package main.cn.gloryroad.appModules;

import org.openqa.selenium.WebDriver;

import junit.framework.Assert;
import main.cn.gloryroad.pageObjects.AddressBookPage;
import main.cn.gloryroad.pageObjects.HomePage;

public class AddContactPersonAction {

	public static void execute(WebDriver driver,String userName,String password,String contactName,String contactEmail,String contactMobile) throws Exception{
		LoginAction.execute(driver, userName, passWord);
		Thread.sleep(3000);
		Assert.assertTrue(driver.getPageSource().contains("未读邮件"));
		HomePage homePage = new HomePage(driver);
		homePage.addressLink().click();
		AddressBookPage addressBookPage = new AddressBookPage(driver);
		Thread.sleep(3000);
		addressBookPage.createContactPersonButton().click();
		Thread.sleep(1000);
		addressBookPage.contactPersonName().sendKeys(contactName);
		addressBookPage.contactPersonEmail().sendKeys(contactEmail);
		addressBookPage.contactPersonMobile().sendKeys(contactMobile);
		addressBookPage.saveButton().click();
		Thread.sleep(5000);
	}
}

11.在main.cn.gloryroad.testScripts的package中新增测试类TestMail163AddContactPerson,测试类的代码如下:

package main.cn.gloryroad.testScripts;

import java.util.concurrent.TimeUnit;

import org.junit.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import main.cn.gloryroad.appModules.AddContactPersonAction;

public class TestMail163AddContactPerson {
	public WebDriver driver;
	String baseUrl = "http://mail.163.com/";

	@Test
	public void testAddContactPerson() throws Exception {
		AddContactPersonAction.execute(driver, "testName", "testPasswd", "", "", "");
		Thread.sleep(2000);
		Assert.assertTrue(driver.getPageSource().contains(""));
		Assert.assertTrue(driver.getPageSource().contains(""));
		Assert.assertTrue(driver.getPageSource().contains(""));
	}

	@BeforeMethod
	public void beforeMethod() {
		// 设定Chrome浏览器启动文件的绝对路径,其他浏览器https://my.oschina.net/bysu/blog/828254
		System.setProperty("webdriver.chrome.driver",
				"C:/Program Files (x86)/Google/Chrome/Application/chromedriver.exe");
		driver = new ChromeDriver();
		// webdriver自己实现的模糊等待,而webdriver的针对某个元素的明确等待(explicitly wait)
		driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
	}

	@AfterMethod
	public void afterMethod() {
		driver.quit();
	}
}

在main.cn.gloryroad.util的package中新建Constant类,其代码如下:

package main.cn.gloryroad.util;

public class Constant {

	//定义测试网址的常量
	public static final String Url = "http://mail.163.com/";
	//定义邮箱用户名的常量
	public static final String MailUserName = "testName163";
	//定义邮箱密码的常量
	public static final String MailPassWord = "testPasswd";
	//定义新建联系人的常量
	public static final String ContactPersonName = "张三";
	//定义新建联系人邮箱地址的常量
	public static final String ContactPersonEmail = "[email protected]";
	//定义新建联系人手机号码的常量
	public static final String ContactPersonMobile = "13333333333";
}

未完待续...

未完待续...

未完待续...

猜你喜欢

转载自my.oschina.net/bysu/blog/1635592