前面我用了一篇内容去分析第二篇脚本中的各种缺点,引导出了我们写自动化测试应该需要框架支撑,然后提出了TestNG+POM的方式去解决大部分的缺点。接下来,我们还是没有介绍到如何用Testng来写我们的自动化测试用例,在这之前,我们要先统一下框架的内容和结构,这个话题有点大,我准备分两篇文章介绍完。这篇,我们介绍如何实现Action和TestCases分层,以及日志类的输出。

声明:本文由凯哥Java(www.kaigejava.com)发布于凯哥公众号(kaigejava)
1. 日志打印
打印日志,记录软件或者代码行在干神马事情,是一个很好的习惯。有了日志内容写入到文件,我们方便运行失败后的问题定位。在Java中,一般来说,主要有Log4j.jar这个日志生成框架。这个功能很强大,但是我们这里是一个小项目,没必要引入这个日志框架。有点大材小用的感觉,所以,我们这里自己动手写一个日志类。方便我们需要日志输出的地方进行调用。
在src下新建一个myframework包,下面新建两个Java类文件。

LogType.java
package myframework;
public class LogType {
public LogType(){
}
public enum LogTypeName{
//
INFO,
//
ERROR,
//
WARNING,
//
DEBUG;
}
}
上面类主要是定位日志输出级别。
package myframework;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Logger {
public static String OutputFileName = getDateTimeByFormat(new Date(), "yyyyMMdd_HHmmss");
private static OutputStreamWriter outputStreamWriter;
private static String logFileName;
public static boolean LogFlag = true;
public Logger() {
}
private static void WriteLog(String logEntry) {
try {
// 定义日志文件路径和日志保存名称
logFileName = ".\\Logs" + "\\" + OutputFileName + ".log";
if (outputStreamWriter == null) {
File logFile = new File(logFileName);
if (!logFile.exists())
logFile.createNewFile();
outputStreamWriter = new OutputStreamWriter(new FileOutputStream(logFileName), "utf-8");
}
outputStreamWriter.write(logEntry, 0, logEntry.length());
outputStreamWriter.flush();
} catch (Exception e) {
System.out.println(LogType.LogTypeName.ERROR.toString() + ": Failed to write the file " + logFileName);
e.printStackTrace();
}
}
private static String getDateTimeByFormat(Date date, String format) {
SimpleDateFormat df = new SimpleDateFormat(format);
return df.format(date);
}
public static void Output(LogType.LogTypeName logTypeName, String logMessage) {
Date date = new Date();
String logTime = getDateTimeByFormat(date, "yyyy-MM-dd HH:mm:ss.SSS");
String logEntry = logTime + " " + () + ": " + logMessage + "\r\n";
System.out.print(logEntry);
if (LogFlag)
WriteLog(logEntry);
}
}
2. POM分层实现
分层主要是Actions文件类在一个大包下,测试脚本文件在另外一个大包下。在Actons中,我们利用POM的思想,把一个模块新建一个包,该包下新建这个模块下的页面类。然后测试包和页面对象里的包大致一样做出区分,把相同模块的页面测试脚本写到一个包,把相同测试场景的用例写到同一个测试类.
在Eclipse的项目中,src包下新建一个pageobjects的包,所有模块下页面类都放这个包。然后新建一个testsuites包,用来放不同测试用例。大致效果如下图。

为了突出效果,我新建一个dashboard的页面对象包,这个页面,就是我们登录后的wordpress的仪表盘页面,登录后默认到这个页面。
1. HomePage.java
新建这么一个类,就不要勾选main方法了。这个类,我们主要写元素定位地址和Actions方法。注意这里元素定位采用的是@FindBy方法,没有像前面的driver.findelement(by.xxx)的方式。注意构造函数中WebDriver driver,否则测试类用PageFactory就会出现空指针异常。
package pageobjects.homepage;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class HomePage {
/**
* 页面对象类,主要写页面定位的地址,下面写各种页面actions的方法
*/
public HomePage(WebDriver driver) {
this.driver = driver;
}
// 定义一个driver变量
public WebDriver driver;
// 元素定位
// 登录链接
@FindBy (xpath=".//*[@id='meta-2']/ul/li[1]/a")
WebElement login_link;
// 用户名输入框
@FindBy (id="user_login")
WebElement userName_inputBox;
// 密码输入框
@FindBy (id="user_pass")
WebElement password_inputBox;
// 登录提交按钮
@FindBy (id="wp-submit")
WebElement loginSubmitBtn;
// actions方法
// 点击登录链接
public void clickLoginLnk() {
login_link.click();
}
// 构造登录方法
public void login(String userName, String password) {
userName_inputBox.sendKeys(userName);
password_inputBox.sendKeys(password);
loginSubmitBtn.click();
}
// 首页下拉滚动条,参考元素是登录链接
public void scrollToLogin() {
JavascriptExecutor je = (JavascriptExecutor) driver;
je.executeScript("arguments[0].scrollIntoView(true);", login_link);
}
}
2. TestLogin.java
我们先写到main方法里,浏览器初始化和打开过程都没有进行函数构造。我们下面主要关注是当前测试类声明的driver变量,如何传递给了页面对象,方法就是我们使用了PageFactory进行了页面对象的元素初始化操作。
package testsuites.homepage;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.PageFactory;
import pageobjects.homepage.HomePage;
public class TestLogin {
public static void main(String args[]) {
System.setProperty("webdriver.chrome.driver", ".\\Tools\\chromedriver.exe");
WebDriver driver= new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
driver.get("http://127.0.0.1/wordpress");
HomePage hp = PageFactory.initElements(driver, HomePage.class);
hp.scrollToLogin();
hp.clickLoginLnk();
hp.login("root", "123456");
}
}
运行一下,测试通过完美。上面的变化和前面第二篇脚本比较,实现了Action和测试调用用例分离。我在HomePage这个页面对象类进行了登录事件的代码优化,构建了一个登录函数给测试脚本调用。
3. 把日志打印给添加上,运行看看效果。
注意日志Logger.java中,设置了log文件保存路径,所以,我们在当前项目根目录下新建一个Logs的文件夹,用来保存log文件。我们只需要在想要日志输出的代码行进行调研Logger类中的静态方法就可以。
HomePage.java 添加日志打印后如下(登录事件我进行更细的构建方法)
package pageobjects.homepage;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import myframework.LogType.LogTypeName;
import myframework.Logger;
public class HomePage {
/**
* 页面对象类,主要写页面定位的地址,下面写各种页面actions的方法
*/
public HomePage(WebDriver driver) {
this.driver = driver;
}
// 定义一个driver变量
public WebDriver driver;
// 元素定位
// 登录链接
@FindBy (xpath=".//*[@id='meta-2']/ul/li[1]/a")
WebElement login_link;
// 用户名输入框
@FindBy (id="user_login")
WebElement userName_inputBox;
// 密码输入框
@FindBy (id="user_pass")
WebElement password_inputBox;
// 登录提交按钮
@FindBy (id="wp-submit")
WebElement loginSubmitBtn;
// actions方法
// 点击登录链接
public void clickLoginLnk() {
try{
login_link.click();
Logger.Output(LogTypeName.INFO, "登录链接点击成功。");
} catch (Exception e) {
Logger.Output(LogTypeName.ERROR, "登录链接点击失败。"+ e.toString());
}
}
// 输入用户名文本框
public void typeUserName(String userName) {
try {
userName_inputBox.sendKeys(userName);
Logger.Output(LogTypeName.INFO, "用户名输入框输入:"+ userName);
} catch(Exception e) {
Logger.Output(LogTypeName.ERROR, "无法在用户名输入框输入"+ e.toString());
}
}
// 输入密码框输入
public void typeUserPasswd(String password) {
try {
userName_inputBox.sendKeys(password);
Logger.Output(LogTypeName.INFO, "密码输入框输入:"+ password);
} catch(Exception e) {
Logger.Output(LogTypeName.ERROR, "无法在密码输入框输入"+ e.toString());
}
}
// 点击登录提交行为
public void clickLoginSubmitBtn() {
try {
loginSubmitBtn.click();
Logger.Output(LogTypeName.INFO, "已点击登录提交按钮");
} catch(Exception e) {
Logger.Output(LogTypeName.ERROR, "无法点击登录提交"+ e.toString());
}
}
// 构造登录方法
public void login(String userName, String password) {
typeUserName(userName);
typeUserPasswd(password);
clickLoginSubmitBtn();
}
// 首页下拉滚动条,参考元素是登录链接
public void scrollToLogin() {
try{
JavascriptExecutor je = (JavascriptExecutor) driver;
je.executeScript("arguments[0].scrollIntoView(true);", login_link);
Logger.Output(LogTypeName.INFO, "拖动滚动条到登录链接处");
} catch (Exception e) {
Logger.Output(LogTypeName.ERROR, "拖动滚动条失败"+ e.toString());
}
}
}
上面的登录,我把三个步骤都单独写了调用方法。为什么要这么写呢,主要是为了方便我们测试这种场景:不输入用户名和密码直接点击登录,用户名和密码只输入一个,然后点击登录,输入用户名和密码,不点击登录。类似这样的用例,我们就必须细化调用方法,如果只写一个登录的方法,怎么去覆盖这么多的登录测试的用例呢。
TestLogin.java 添加日志输出后代码如下
package testsuites.homepage;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.PageFactory;
import myframework.LogType.LogTypeName;
import myframework.Logger;
import pageobjects.homepage.HomePage;
public class TestLogin {
public static void main(String args[]) {
System.setProperty("webdriver.chrome.driver", ".\\Tools\\chromedriver.exe");
WebDriver driver= new ChromeDriver();
Logger.Output(LogTypeName.INFO, "初始化浏览器");
driver.manage().window().maximize();
Logger.Output(LogTypeName.INFO, "浏览器最大化操作");
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
driver.get("http://127.0.0.1/wordpress");
Logger.Output(LogTypeName.INFO, "打开wp首页");
Logger.Output(LogTypeName.INFO, "开始测试登录");
HomePage hp = PageFactory.initElements(driver, HomePage.class);
hp.scrollToLogin();
hp.clickLoginLnk();
hp.login("root", "123456");
Logger.Output(LogTypeName.INFO, "测试登录结束");
driver.quit();
Logger.Output(LogTypeName.INFO, "退出和关闭浏览器");
}
}
运行下,然后看看控制台和根目录下的Logs文件

以上是控制台打印的日志
看看我们日志文件是否保存
需要先刷新下Logs文件,才可以看到时间戳的日志文件。
打开内容

本篇优化内容就介绍到这里,注意理解如何把当前测试类的driver变量和页面HomePage的driver进行合并,否则如果出现两个driver,就会报空指针,这个问题太多人遇到了太多次。下面一篇继续介绍如何优化浏览器初始化代码和页面对象中公共部分进行抽象,然后才去继承的思维,让页面对象类减少很多代码的编写。
















