前言

我目前的工作是测试工程师,主要是对网站进行测试。对于新功能的测试基本由手动完成,而对于老功能的测试则因为功能繁多、成本和效率等的考量,不适宜于手动执行测试内容。通过使用Junit 5测试框架和Selenium网站自动化测试框架搭建自己的自动化网站测试平台并进行实际测试案例的自动化改造,实现对于网站的自动化操作和测试,并将每一个测试步骤的结果进行截图保存。

Junit 5

Junit 5是Junit框架的最新版本,它包含了用于在JVM中运行和自定义测试框架的Junit Platform,用于编写和运行测试案例的Junit Jupiter,用于运行Junit 3/4的测试引擎Junit Vintage。对于使用基础的Junit 5测试框架来编写和运行测试案例,只需引用自带测试引擎的Junit Jupiter就足够了。

implementation(group = "org.junit.jupiter", name = "junit-jupiter", version = junit_jupiter_version)

基础注解

Junit 5是一个使用注解来对测试案例进行配置的框架,通过使用注解来配置测试内容和进行扩展。

注解 用途
@Test 声明一个测试方法
@ParameterizedTest 声明一个带参数的测试方法
@RepeatedTest 声明一个测试方法重复执行的次数
@TestClassOrder 声明测试类的执行顺序规则
@TestMethodOrder 声明测试方法的执行顺序规则
@Order 声明测试类或测试方法的顺序
@DisplayName 声明测试类或测试方法在测试结果中的显示名称
@DisplayNameGeneration 声明测试类或测试方法在测试结果中动态创建的显示名称
@BeforeEach 声明在测试类中每个测试方法执行前要执行的内容
@AfterEach 声明在测试类中每个测试方法执行后要执行的内容
@BeforeAll 声明在测试类中的全部测试方法执行前要执行的内容
@AfterAll 声明在测试类中的全部测试方法执行后要执行的内容
@Nested 声明测试的分组
@Disabled 声明不执行的测试类或测试方法
@Timeout 声明测试类或测试方法的执行时限
@ExtendWith 声明测试类或测试方法扩展外部的@BeforeAll @AfterAll @BeforeEach @AfterEach方法

Assertions

Junit 5使用Assertions(断言)来对变量和表达式是否正确、超时等进行判断,以在测试执行中发现可能存在的问题。

  • Assertions.assertTrue{condition}
  • Assertions.assertEquals(expected, actual)
  • Assertions.assertTimeoutPreemptively(timeout){executable}

Selenium

Selenium是一个WebDriver测试框架,它通过使用浏览器厂商提供的Driver来对浏览器和网站中的元素进行获取和控制,为我们在代码层面提供了能够简单调用Driver来获取和控制浏览器的封装接口和方法。

implementation(group = "org.seleniumhq.selenium", name = "selenium-java", version = selenium_version)

常用浏览器Driver

浏览器 Driver下载地址
Chrome https://chromedriver.storage.googleapis.com/index.html
Edge https://developer.microsoft.com/zh-cn/microsoft-edge/tools/webdriver

配置WebDriver

设置Driver和路径

if(chrome){
System.setProperty("webdriver.chrome.driver", driver_path)
}else if(edge){
System.setProperty("webdriver.edge.driver", driver_path)
}

设置浏览器路径

if(chrome){
System.setProperty("webdriver.chrome.bin", browser_path)
}else if(edge){
System.setProperty("webdriver.edge.bin", browser_path)
}

设置隐私模式

val options = if(chrome){
ChromeOptions().apply {
setBinary(browser_path)
if (private) {
addArguments("-incognito")
}
}
}else if(edge){
EdgeOptions().apply {
setBinary(browser_path)
if (private) {
addArguments("-inprivate")
}
}
}

设置最大化浏览器和超时时间

if(chrome){
webDriver = ChromeDriver(options).apply {
manage().apply {
window().maximize()
timeouts().implicitlyWait(Repository.duration)
}
}
}else if(edge){
webDriver = EdgeDriver(options).apply {
manage().apply {
window().maximize()
timeouts().implicitlyWait(Repository.duration)
}
}
}

搭建自己的测试框架

基于Properties和Repository的配置机制

由于我对Kotlin和Android相关的架构和开发更加熟悉,所以将测试框架设计为使用Kotlin语言开发并类似安卓的Repository模式架构,可以根据自己的喜好来调整开发语言和设计架构。

将对WebDriver设置及网站测试相关的可变量提取成Properties配置文件

在resources文件夹下创建settings.properties文件,并将部分可配置项通过UTF-8写入并保存,以便在后续更加灵活地进行调整

# Some fields support both of upper and lower case letters.
# "Chrome" or "Edge" or "QQ"
Browser=chrome
ChromeDriver=chromeDriver_105.0.5195.52.exe
ChromeBin=
EdgeDriver=edgeDriver_115.0.1901.183.exe
EdgeBin=
QQDriver=chromeDriver_94.0.4606.113.exe
QQBin=C:/Program Files (x86)/Tencent/QQBrowser/QQBrowser.exe
Private=false
Duration=10
# "Desktop" or file directory
Screenshot=desktop
# "QA" or "PROD"
Environment=qa
Dealer=xxx
CustomerPhoneNumber=xxx
CustomerPassword=xxx

通过Repository存取主要资源并进行测试环境预加载

Repository

object Repository {
lateinit var properties: Properties
lateinit var browser: Browser
lateinit var webDriver: WebDriver
var private by Delegates.notNull<Boolean>()
lateinit var duration: Duration
lateinit var screenshot: File
lateinit var environment: Environment
lateinit var webBin: String
lateinit var dealer: Dealer
var customerPhoneNumber by Delegates.notNull<Long>()
lateinit var customerPassword: String

fun isPropertiesInitialized() = ::properties.isInitialized
}

存取Properties并进行测试环境预加载

enum class Browser {
CHROME, EDGE, QQ
}
enum class Dealer {
dealer1, dealer2, dealer3, dealer4
}
enum class Environment {
QA, PROD
}
object EnvironmentUtils {
fun loadEnvironment() {
var bufferedReader: BufferedReader? = null
if (!Repository.isPropertiesInitialized()) {
bufferedReader = BufferedReader(FileReader(EnvironmentUtils::class.java.classLoader.getResource("settings.properties")!!.path, Charsets.UTF_8))
Repository.properties = Properties().apply {
load(bufferedReader)
}
}
Repository.browser = Browser.valueOf(Repository.properties.getProperty("Browser").uppercase(Locale.getDefault()))
Repository.private = Repository.properties.getProperty("Private").toBoolean()
Repository.duration = Duration.ofSeconds(Repository.properties.getProperty("Duration").toLong())
Repository.screenshot =
if (Repository.properties.getProperty("Screenshot").uppercase(Locale.getDefault()) == "DESKTOP") {
File(FileSystemView.getFileSystemView().homeDirectory.path + "/Screenshot")
} else {
File(Repository.properties.getProperty("Screenshot"))
}.apply {
if (!exists()) {
mkdirs()
}
}
when (Repository.browser) {
Browser.CHROME -> {
Repository.webBin = Repository.properties.getProperty("ChromeBin")
System.setProperty("webdriver.chrome.driver", EnvironmentUtils::class.java.classLoader.getResource(Repository.properties.getProperty("ChromeDriver"))!!.path)
if (Repository.webBin.isNotBlank()) {
System.setProperty("webdriver.chrome.bin", Repository.webBin)
}
val options = ChromeOptions().apply {
setBinary(Repository.webBin)
if (Repository.private) {
addArguments("-incognito")
}
}
Repository.webDriver = ChromeDriver(options).apply {
manage().apply {
window().maximize()
timeouts().implicitlyWait(Repository.duration)
}
}
}

Browser.EDGE -> {
Repository.webBin = Repository.properties.getProperty("EdgeBin")
System.setProperty("webdriver.edge.driver", EnvironmentUtils::class.java.classLoader.getResource(Repository.properties.getProperty("EdgeDriver"))!!.path)
if (Repository.webBin.isNotBlank()) {
System.setProperty("webdriver.edge.bin", Repository.webBin)
}
val options = EdgeOptions().apply {
setBinary(Repository.webBin)
if (Repository.private) {
addArguments("-inprivate")
}
}
Repository.webDriver = EdgeDriver(options).apply {
manage().apply {
window().maximize()
timeouts().implicitlyWait(Repository.duration)
}
}
}

Browser.QQ -> {
Repository.webBin = Repository.properties.getProperty("QQBin")
System.setProperty("webdriver.chrome.driver", EnvironmentUtils::class.java.classLoader.getResource(Repository.properties.getProperty("QQDriver"))!!.path)
if (Repository.webBin.isNotBlank()) {
System.setProperty("webdriver.chrome.bin", Repository.webBin)
}
val options = ChromeOptions().apply {
setBinary(Repository.webBin)
if (Repository.private) {
addArguments("-incognito")
}
}
Repository.webDriver = ChromeDriver(options).apply {
manage().apply {
window().maximize()
timeouts().implicitlyWait(Repository.duration)
}
}
}
}
Repository.environment =Environment.valueOf(Repository.properties.getProperty("Environment").uppercase(Locale.getDefault()))
Repository.dealer = Dealer.valueOf(Repository.properties.getProperty("Dealer").uppercase(Locale.getDefault()))
Repository.customerPhoneNumber = Repository.properties.getProperty("CustomerPhoneNumber").toLong()
Repository.customerPassword = Repository.properties.getProperty("CustomerPassword")
bufferedReader?.close()
}
}

实用类框架设计

查找元素实用类

object WebElementUtils {
fun findElement(by: By): WebElement {
WebDriverWait(Repository.webDriver, Repository.duration).until {
ExpectedConditions.presenceOfElementLocated(by)
}
return Repository.webDriver.findElement(by)
}

fun findElements(by: By): List<WebElement> {
WebDriverWait(Repository.webDriver, Repository.duration).until {
ExpectedConditions.presenceOfAllElementsLocatedBy(by)
}
return Repository.webDriver.findElements(by)
}

fun findAlert(): Alert {
WebDriverWait(Repository.webDriver, Repository.duration).until {
ExpectedConditions.alertIsPresent()
}
return Repository.webDriver.switchTo().alert()
}

fun findFrame(nameOrId: String): WebDriver {
WebDriverWait(Repository.webDriver, Repository.duration).until {
ExpectedConditions.frameToBeAvailableAndSwitchToIt(nameOrId)
}
return Repository.webDriver.switchTo().frame(nameOrId)
}

fun findWindow(nameOrHandle: String): WebDriver {
WebDriverWait(Repository.webDriver, Repository.duration).until {
ExpectedConditions.numberOfWindowsToBe(2)
}
return Repository.webDriver.switchTo().window(nameOrHandle)
}
}

网站截图实用类

enum class ScreenshotCategory {
Category1, Category2, Category3
}
object ScreenshotUtils {
fun screenshot(category: ScreenshotCategory, filename: String, webElement: WebElement? = null) {
BufferedOutputStream(
FileOutputStream(File(Repository.screenshot, "/${category.name}/${filename}").also { it.createNewFile() }, false)
).also {
if (webElement == null) {
it.write((Repository.webDriver as TakesScreenshot).getScreenshotAs(OutputType.BYTES))
} else {
it.write((webElement as TakesScreenshot).getScreenshotAs(OutputType.BYTES))
}
it.flush()
it.close()
}
}

fun createScreenshotFolder(category: ScreenshotCategory) {
if (File(Repository.screenshot, category.name).exists()) {
File(Repository.screenshot, category.name).deleteRecursively()
}
File(Repository.screenshot, category.name).mkdirs()
}
}

简单测试案例设计

@ExtendWith(TestEnvironment::class)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
@DisplayNameGeneration(ReplaceUnderscores::class)
class TestCase {
@Test
fun test() {
Repository.webDriver.findElement(By.id("xxx")).click()
WebDriverWait(Repository.webDriver,Duration.ofSeconds(Repository.duration)).until { Repository.webDriver.currentUrl == "xxx" }
Assertions.assertTrue{ WebElementUtils.findElement(By.id("xxx")).text == "xxx" }
ScreenshotUtils.createScreenshotFolder(ScreenshotCategory.Category1)
ScreenshotUtils.screenshot(ScreenshotCategory.Category1, "Screenshot 1.png",
WebElementUtils.findElement(By.className("xxx")))
}
}

小结

这个测试工程是由我于2022.05.27-2022.08.19搭建完成的,它使用Junit 5与Selenium框架搭建了一下类似于安卓Repository模式的测试平台,通过使用Properties将可配置项统一管理、方便后续修改和维护,并在框架自身基础之上再次封装了网站元素查找使用类和网站截图实用类以提升查找元素时的效率和可靠性。