总结了一下如何使用 Capybara 进行 UI 自动化测试 (Feature Test),欢迎指正,补充。
一个进行 UI 测试的框架,可以让你将浏览器进行的手动测试自动化
Rails + Rspec + Capybara + SeleniumDriver
# Gemfile
group :test do
gem 'capybara'
gem 'selenium-webdriver'
end
# docker-compose.yml
services:
snode:
image: selenium/standalone-chrome:3.141.59
environment:
NODE_MAX_INSTANCES: 10
NODE_MAX_SESSION: 10
GRID_MAX_SESSION: 10
volumes:
- /dev/shm:/dev/shm
不使用 docker 的可以使用chromedriver-helper
这个 Gem
# spec/support/capybara.rb
require 'capybara/rails'
require 'capybara/rspec'
Capybara.register_driver :selenium do |app|
Capybara::Selenium::Driver.load_selenium
client = Selenium::WebDriver::Remote::Http::Default.new
# 设置成headless
chrome_capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
"chromeOptions": { args: %w(headless window-size=1400,1200) }
)
# 设置docker chrome的url
options = { browser: :remote, url: 'http://snode:4444/wd/hub', http_client: client, desired_capabilities: chrome_capabilities }
Capybara::Selenium::Driver.new(app, options)
end
Capybara.javascript_driver = :selenium
Capybara.app_host = 'http://app.com'
Capybara.server_host = 'app.com'
一个简单的登陆页面测试
# spec/features/sample_login.rb
feature 'SampleLogin', type: :feature, js: true do
describe '登陆' do
it '正确的ID和密码可以登陆' do
visit 'login'
fill_in 'id', with: 'your id'
fill_in 'password', with: '123456789'
click_on '登陆'
expect(page).to have_content '登陆成功'
end
end
end
从这个列子可以看出 UI 测试基本都是以下几步
visit root_path
visit new_user_path
然后就可以使用page
来对当前页面进行各种操作
page 的具体内容可以参考这里 Capybara::Session
最常用的查找方式,和 jquery 的查找比较类似
<div id="sample_id1" class="sample1"></div>
<div id="sample_id2" class="sample" style="display:none"></div>
<a href="/sample">sample</a>
<button name="button" type="submit" data-sample="test">保存/button>
# find by class
find('.sample1')
# find by id
find('#sample_id1')
# find invisible element
find('#sample_id2', visible: false)
# find by text
find('a', text: 'sample')
# find by attr
find('button[data-sample="test"]')
使用方法和 find 基本一样,只是对于多个类似的元素用 find 无法查找,比如多个相同的 class,这个时候可以使用 all 和 first
<div id="sample_id1" class="sample">第一</div>
<div id="sample_id2" class="sample">第二</div>
first('.sample') # 获取第一个div
all('.sample')[1] # 获取第二个div
具体可以参考这里 Capybara::Node::Finders
<a href="path/sample" id="link" class="link_class">链接</a>
find('#link').click # 查找元素并click
# 简便写法
click_link 'link'
click_on '链接'
click_on class: 'link_class'
<input type="text" name="name" id="name"></input>
fill_in 'name', with: 'sample'
<select name="sex" id="sex">
<option value="0">男性</option>
<option value="1">女性</option>
</select>
page.select '男性', from: 'sex'
<label >
<input type="checkbox" value="1" name="sex" id="sex">
<span>男</span>
</label>
check('sex')
uncheck('sex')
<label>
<input type="radio" value="1" name="sex" id="sex_1">
<span>男性</span>
</label>
<label>
<input type="radio" value="2" name="sex" id="sex_2">
<span>女性</span>
</label>
choose('sex_1') # 选择男性
find('input[type="file"]').attach_file('spec/data/upload/sample.jpg')
方法 | 说明 |
---|---|
double_click | 双击 |
drag_to(elem) | 拖动 |
hover | hover |
right_click | 右击 |
send_keys('text') | 输入 text |
具体参考这里 Capybara::Node::Element
判断页面的 URL
expect(current_path).to eq new_user_path
判断页面的元素
expect(page).to have_content '登陆成功'
expect(page).to have_no_content('登陆失败')
expect(page).to have_field('email', with: '[email protected]', type: 'hidden')
expect(page).to have_selector 'h1', text: '测试页面'
expect(page).to have_css '.invalid-feedback'
不好判断的,可以查找元素,然后得到属性来判断
<input id='delete' type="submit" value="删除" data-confirm="确认删除吗?">
expect(find('#delete')['data-confirm']).to eq '确认删除吗?'
具体可以参考这里 Capybara::RSpecMatchers
<div class="section1">
喜欢运动吗?
<label>
<input id="sport1" name="sport" type="radio" value="1" checked="checked">是
</label>
<label>
<input id="sport2" name="sport" type="radio" value="2">否
</label>
</div>
<div class="section2">
按时睡觉吗?
<label>
<input id="sleep1" name="sleep" type="radio" value="1" checked="checked">是
</label>
<label>
<input id="sleep2" name="sleep" type="radio" value="2">否
</label>
</div>
within '.section1' do
choose '是'
end
within '.section2' do
choose '否'
end
当然,如果查找太复杂的话,可以考虑给它一个 ID 或者 Class,让查找元素尽量简单
<a href="/rule" target="_blank">打开新窗口</a>
visit sample_path
click_on '打开新窗口'
within_window(windows.last) do
expect(page).to have_content '新窗口'
end
在页面直接执行 JS 代码,对于一些使用 capybara 难以模拟的操作可以使用
比如用 dropzone.js 来上传图片,用 capybara 没找到如何测试
这个时候可以执行 JS 来模拟图片的上传
def drop_in_dropzone(file_path)
# Generate a fake input selector
page.execute_script <<-JS
fakeFileInput = $('<input/>').attr(
{id: 'fakeFileInput', type:'file'}
).appendTo('body');
JS
# Attach the file to the fake input selector
attach_file("fakeFileInput", file_path)
# Add the file to a fileList array
page.execute_script("var fileList = [fakeFileInput.get(0).files[0]]")
# Trigger the fake drop event
page.execute_script <<-JS
var e = $.Event('drop', { dataTransfer : { files : [fakeFileInput.get(0).files[0]] } });
$('.dropzone')[0].dropzone.listeners[0].events.drop(e);
JS
end
个人感觉这是最后手段,尽量不使用,维护起来比较麻烦
比如在用 ajax 进行异步操作的时候,必须等一段时间才可以继续测试,可以sleep 秒数
来等待
缺点是 sleep 的秒数对于不同环境是不一样的,也许自己的机子可以跑,放到其他测试环境下就不行了
解决方法
默认的等待时间设置长一些,比如 30 秒
Capybara.default_max_wait_time = 30 # default 2s
使用 find,have_selector(默认等待时间是 default_max_wait_time)
# 还可以指定最大等待时间
find('#name', wait: 60)
比如你需要访问一个外部 API,可以用webmock gem
来模拟
Net::HTTP.get('http://www.example.com', '/')
# 会出现这样的显示
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled.
You can stub this request with the following snippet:
stub_request(:get, "http://www.example.com/").
with(
headers: {
'Accept'=>'*/*',
'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
'User-Agent'=>'Ruby'
}).
to_return(status: 200, body: "", headers: {})
方便的是提示的 stub_request 的代码很多时候可以直接使用
不用 feature 测试就可以测试的就不用 UI 测试
比如分离功能出来进行单体测试、使用 controller 测试等
写 feature 测试的时候,最麻烦的可能是不好查找问题
可以使用以下的方法让调试稍微轻松些
gem 'pry-byebug'
最方便的 debug 工具,大家应该都熟悉,就不多说了
tail -f log/test.log
gem 'capybara-screenshot'
失败的时候,会自动将 html 和 png 的文件保存下来
HTML screenshot: /www/sample/tmp/capybara/screenshot_2019-03-30-17-55-11.103.html
Image screenshot: /www/sample/tmp/capybara/screenshot_2019-03-30-17-55-11.103.png
想知道 JS 是否被执行了,可以在 JS 里面console.log('message')
gem 'capybara-chromedriver-logger'
这样 JS 的 error 和 log 都会显示出来
chrome headless 运行的时候,不好 debug,可以按照以下的方法显示浏览器
# docker-compose-selenium-debug.yml
version: '3'
services:
snode:
image: selenium/standalone-chrome-debug:3.141.59
ports:
- 5900:5900
open vnc://localhost:5900
# 启动
$ docker-compose -f docker-compose.yml:docker-compose-selenium-debug.yml up -d
# 执行rspec
$ NO_HEADLESS=1 bundle exec rspec spec/features/xxxx.rb
这时候可以直接在 vnc 服务器上看到浏览器的画面