前言 我目前的工作是测试工程师,主要是对网站进行测试。对于新功能的测试基本由手动完成,而对于老功能的测试则因为功能繁多、成本和效率等的考量,不适宜于手动执行测试内容。通过使用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
配置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.exeEdgeBin= 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将可配置项统一管理、方便后续修改和维护,并在框架自身基础之上再次封装了网站元素查找使用类和网站截图实用类以提升查找元素时的效率和可靠性。