Selenium 整合了一揽子工具与依赖库,支持 Web 浏览器自动化,提供一组扩展来模拟人与浏览器交互。我们基于其满足 W3C 标准的 WebDriver 来编写的自动化代码可在各种主流浏览器复用。
所以这里关键的一个组件即是 WebDriver,其负责与浏览器厂商提供的 API 来与浏览器交互。
使用其即可做出模拟终端用户的操作,如:文本框输入,下拉框选择,链接点击等。此外还提供鼠标移动,JavaScript 脚本执行等能力。
Selenium 提供多种执行方式:如在本机安装 WebDriver 二进制可执行文件,或安装单独的服务,或使用远程 WebDriver 服务,甚至支持多种浏览器多种版本的 Grid 集群方式。
下面我们使用 docker 方式启动一个拥有 Chrome 环境的单独服务。
docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:3.141.59-zirconium
查看页面http://localhost:4444/
发现已启动成功。
下面我们用 Golang 写个测试用例试试吧。
测试场景就选我的博客吧:打开博客首页leileiluoluo.com
,点击搜索按钮,搜索框输入istio
关键字后回车,应至少有一条结果,此外将搜索结果截图保存。
本文选择 Golang 的 selenium 包github.com/tebeka/selenium
。
如下为代码说明:
代码已托管至 GitHub:https://github.com/leileiluoluo/go-exercises
package blog_test
import (
"flag"
"io/ioutil"
"log"
"testing"
"time"
"github.com/tebeka/selenium"
)
var (
browserName = flag.String("browser", "chrome", "browser name")
gridUrl = flag.String("grid", "http://localhost:4444/wd/hub", "grid url")
blogURL = "https://leileiluoluo.com/"
searchButtonIdSelector = "searchOpen"
keywordInputIdSelector = "search-query"
searchResultLoadingCssSelector = "#search-results #loadingDiv"
searchResultCssSelector = "#search-results .border-bottom"
keyword = "istio"
)
var driver selenium.WebDriver
func setup() func() {
// new remote driver
caps := selenium.Capabilities{"browserName": *browserName}
webDriver, err := selenium.NewRemote(caps, *gridUrl)
if nil != err {
panic(err)
}
driver = webDriver
// teardown
return func() {
driver.Quit()
}
}
func screenshot(filename string) {
bytes, err := driver.Screenshot()
if nil != err {
log.Printf("take screenshot error, err: %s", err)
return
}
err = ioutil.WriteFile(filename, bytes, 0666)
if nil != err {
log.Printf("save screenshot error, err: %s", err)
}
}
func TestSearch(t *testing.T) {
// open blog
err := driver.Get(blogURL)
if nil != err {
t.Errorf("search page open error, err: %s", err)
}
// click search button
elem, err := driver.FindElement(selenium.ByID, searchButtonIdSelector)
if nil != err {
t.Errorf("search button not found, err: %s", err)
}
elem.Click()
// type keyword and enter
elem, err = driver.FindElement(selenium.ByID, keywordInputIdSelector)
if nil != err {
t.Errorf("keyword input element not found, err: %s", err)
}
elem.SendKeys(keyword + "\n")
// wait until search result displayed
driver.WaitWithTimeout(func(driver selenium.WebDriver) (bool, error) {
elem, err = driver.FindElement(selenium.ByCSSSelector, searchResultLoadingCssSelector)
if nil != err {
return false, nil
}
visible, err := elem.IsDisplayed()
return !visible, err
}, 30*time.Second)
// assert
elems, err := driver.FindElements(selenium.ByCSSSelector, searchResultCssSelector)
if nil != err || len(elems) < 1 {
t.Errorf("no search result, err: %s", err)
}
// save screenshot
screenshot("search.png")
}
func TestMain(m *testing.M) {
// parse flags
flag.Parse()
// setup / teardown
teardown := setup()
defer teardown()
// run tests
m.Run()
}
执行测试:
$ go test -v
测试结果:
=== RUN TestSearch
--- PASS: TestSearch (46.40s)
PASS
ok github.com/leileiluoluo/test 91.427s
至此,我们已可以使用 Selenium 进行自动化测试了。
分析如上代码,代码编排的有一点粗陋,面对实际 Web 应用的复杂性,测试代码如何落地呢?有一点即是测试代码的编排。
如上测试代码的组织方式在测试逻辑复杂的情况下可能会变得庞杂又混乱。面对一个交互场景稍微复杂些的 Web 应用的时候,我们如何编排测试代码的包结构,或者进而设计一个通用的测试框架呢?
Selenium 给出一个指导原则——页面对象模型,简单点说即是摒弃直接从测试者的角度想问题,而应从终端用户的视角出发,一个测试场景应是一组动作结合页面上下文的组合。
所以编写测试用例时重要的是:不要一开始就设想点哪个按钮,选哪个字段,提交哪个表单这么细粒度的问题,而是过一遍真实用户体验。
所以,写测试用例即如编写业务代码一样,需要考虑重用,封装,单一职责,面向对象,设计模式等知识。
基于此,自动化测试领域的编码规范或设计模式即页面对象模型应运而生。其采用面向对象原则,将各个页面的选择器标记及行为封装在各自的页面,通过方法提供该页面的服务,且页面模型内不应有断言。
下面就基于该规范将上边的代码试着改进一下吧。
blog_test.go 为总测试入口,pages 包下为各页面功能,所以搜索页面的定位标记及功能均封装在 search.go,这样,我们在 blog_test.go 写测试函数调用 pages 下的页面的方法即可进行断言。
改进后的代码已托管至 GitHub:https://github.com/leileiluoluo/go-exercises
$ tree
.
├─ blog_test.go
├─ pages
│ ├─ ...
│ └─ search.go
├─ go.mod
└─ go.sum
package blog_test
import (
"flag"
"testing"
"github.com/leileiluoluo/test/pages"
"github.com/tebeka/selenium"
)
var (
browserName = flag.String("browser", "chrome", "browser name")
gridUrl = flag.String("grid", "http://localhost:4444/wd/hub", "grid url")
keyword = "istio"
)
var driver selenium.WebDriver
func setup() func() {
// new remote driver
caps := selenium.Capabilities{"browserName": *browserName}
webDriver, err := selenium.NewRemote(caps, *gridUrl)
if nil != err {
panic(err)
}
driver = webDriver
// teardown
return func() {
driver.Quit()
}
}
func TestSearch(t *testing.T) {
sp := pages.NewSearchPage(driver)
count, err := sp.Search(keyword)
if nil != err || count < 1 {
t.Errorf("search error, count: %d, err: %s", count, err)
}
}
func TestMain(m *testing.M) {
// parse flags
flag.Parse()
// setup / teardown
teardown := setup()
defer teardown()
// run tests
m.Run()
}
package pages
import (
"errors"
"fmt"
"time"
"github.com/tebeka/selenium"
)
const (
blogURL = "https://leileiluoluo.com/"
searchButtonIdSelector = "searchOpen"
keywordInputIdSelector = "search-query"
searchResultLoadingCssSelector = "#search-results #loadingDiv"
searchResultCssSelector = "#search-results .border-bottom"
)
var drv selenium.WebDriver
type SearchPage struct {
}
// initializer
func NewSearchPage(driver selenium.WebDriver) *SearchPage {
drv = driver
return &SearchPage{}
}
// open blog and click search button
func (sp *SearchPage) openBlogAndClickSearchButton() error {
// open blog
err := drv.Get(blogURL)
if nil != err {
return errors.New(fmt.Sprintf("search page open error, err: %s", err))
}
// click search button
elem, err := drv.FindElement(selenium.ByID, searchButtonIdSelector)
if nil != err {
return errors.New(fmt.Sprintf("search button not found, err: %s", err))
}
return elem.Click()
}
// type keyword and enter
func (sp *SearchPage) typeKeyword(keyword string) error {
elem, err := drv.FindElement(selenium.ByID, keywordInputIdSelector)
if nil != err {
return errors.New(fmt.Sprintf("keyword input element not found, err: %s", err))
}
return elem.SendKeys(keyword + "\n")
}
// wait until search result displayed
func (sp *SearchPage) waitUntilResultDisplayed() error {
return drv.WaitWithTimeout(func(driver selenium.WebDriver) (bool, error) {
elem, err := driver.FindElement(selenium.ByCSSSelector, searchResultLoadingCssSelector)
if nil != err {
return false, nil
}
visible, err := elem.IsDisplayed()
return !visible, err
}, 30*time.Second)
}
// Search by keyword
// return count of search result
func (sp *SearchPage) Search(keyword string) (int, error) {
// open blog and click search button
err := sp.openBlogAndClickSearchButton()
if nil != err {
return 0, err
}
// type keyword and enter
err = sp.typeKeyword(keyword)
if nil != err {
return 0, err
}
// wait until search result displayed
err = sp.waitUntilResultDisplayed()
if nil != err {
return 0, err
}
// return
elems, err := drv.FindElements(selenium.ByCSSSelector, searchResultCssSelector)
if nil != err {
return 0, errors.New(fmt.Sprintf("search element error, err: %s", err))
}
return len(elems), nil
}
参考资料