Rails 使用 Capybara 进行自动化测试

heroyct · 2019年04月01日 · 最后由 SpiderEvgn 回复于 2020年02月28日 · 1818 次阅读

总结了一下如何使用 Capybara 进行 UI 自动化测试 (Feature Test),欢迎指正,补充。

什么是 Capybara

一个进行 UI 测试的框架,可以让你将浏览器进行的手动测试自动化

  • open source
  • 对应多个测试框架
    • MiniTest RSpec Cucumber 等
  • 可以使用多个 driver
    • Selenium Webkit Poltergeist 等

开发环境

Rails + Rspec + Capybara + SeleniumDriver

  • Rails 5.1.6.2
  • rspec-rails 3.7.2
  • capybara 3.9.0
  • SeleniumDriver 3.141.59

安装和设置

1. 添加 Gem

# Gemfile

group :test do
  gem 'capybara'
  gem 'selenium-webdriver'
end

2. 添加 docker chrome

# 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

3. 设置 Capybara

# 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'

UI 测试

登陆页面测试

一个简单的登陆页面测试

# 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 测试基本都是以下几步

  1. 访问一个页面
  2. 查找元素
  3. 操作元素
  4. 判断元素是否存在,元素的属性是否符合预期

访问一个页面

visit root_path
visit new_user_path

然后就可以使用page来对当前页面进行各种操作

page 的具体内容可以参考这里 Capybara::Session

查找元素

find

最常用的查找方式,和 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"]')

all first

使用方法和 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

操作元素

click

<a href="path/sample" id="link" class="link_class">链接</a>
find('#link').click # 查找元素并click
# 简便写法
click_link 'link'
click_on '链接'
click_on class: 'link_class'

textarea,text

<input type="text" name="name" id="name"></input>
fill_in 'name', with: 'sample'

select

<select name="sex" id="sex">
  <option value="0">男性</option>
  <option value="1">女性</option>
</select>
page.select '男性', from: 'sex'

checkbox

<label >
<input type="checkbox" value="1" name="sex" id="sex">
  <span></span>
</label>
check('sex')
uncheck('sex')

radio

<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: 'test@sian.com', 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

测试中的一些技巧

within 限定操作在某一块画面

<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

execute_script

在页面直接执行 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

个人感觉这是最后手段,尽量不使用,维护起来比较麻烦

少用 sleep

比如在用 ajax 进行异步操作的时候,必须等一段时间才可以继续测试,可以sleep 秒数来等待

缺点是 sleep 的秒数对于不同环境是不一样的,也许自己的机子可以跑,放到其他测试环境下就不行了

解决方法

  1. 默认的等待时间设置长一些,比如 30 秒

    Capybara.default_max_wait_time = 30 # default 2s
    
  2. 使用 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 测试

不用 feature 测试就可以测试的就不用 UI 测试

比如分离功能出来进行单体测试、使用 controller 测试等

调试

写 feature 测试的时候,最麻烦的可能是不好查找问题

可以使用以下的方法让调试稍微轻松些

pry-byebug

gem 'pry-byebug'

最方便的 debug 工具,大家应该都熟悉,就不多说了

log

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 的 debug 信息

想知道 JS 是否被执行了,可以在 JS 里面console.log('message')

gem 'capybara-chromedriver-logger'

这样 JS 的 error 和 log 都会显示出来

显示浏览器画面

chrome headless 运行的时候,不好 debug,可以按照以下的方法显示浏览器

1. 添加 debug 用的 docker

# docker-compose-selenium-debug.yml

version: '3'

services:
  snode:
    image: selenium/standalone-chrome-debug:3.141.59
    ports:
      - 5900:5900

2. 连接 vnc

open vnc://localhost:5900

3. 执行

# 启动
$ 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 服务器上看到浏览器的画面

参考资料

最近也在启用 Capybara,看了你的帖子受益匪浅,非常实用!

有几个问题请教一下:

  1. 我有个延迟消失的元素,必须等他消失之后才能 click 被他覆盖的元素。这里的等待如果不用 sleep 还有更好的方式吗?
  2. 还是元素覆盖的问题,我有个全屏覆盖的元素,意图是按任何一个点都能优先触发这个效果,比如侧边栏的弹回,这时候如果测试 click 一个按钮,效果应该是触发侧边栏的弹回,但结果是报错元素覆盖,按不到按钮。但如果直接去 find 那个全屏元素 click 就不符合这个测试意图了。这个该如何解决呢?
  3. 有个文字被包含在一个 display: none 的模块里,效果是希望点击按钮后通过 JS 显示,但是用 have_content 去查的话这个文字是一直存在的。这个该如何验证?

@SpiderEvgn

  1. 我有个延迟消失的元素,必须等他消失之后才能 click 被他覆盖的元素。这里的等待如果不用 sleep 还有更好的方式吗?

用类似下面的应该可以判断,会等到元素消失 (等待时间为 Capybara.default_max_wait_time 设置的时间)

expect(page).to have_no_content('元素的text')
  1. 还是元素覆盖的问题,我有个全屏覆盖的元素,意图是按任何一个点都能优先触发这个效果,比如侧边栏的弹回,这时候如果测试 click 一个按钮,效果应该是触发侧边栏的弹回,但结果是报错元素覆盖,按不到按钮。但如果直接去 find 那个全屏元素 click 就不符合这个测试意图了。这个该如何解决呢?

没想出啥好办法,感觉 直接去 find 那个全屏元素 click 是个折中的办法。

  1. 有个文字被包含在一个 display: none 的模块里,效果是希望点击按钮后通过 JS 显示,但是用 have_content 去查的话这个文字是一直存在的。这个该如何验证?

感觉这样可以

expect(page).to have_no_content('元素的text')
# click some button
expect(page).to have_content('元素的text')
heroyct 回复

第一个问题不是判断存在,是要等覆盖的元素消失了才 click。

第二个问题我放弃了 😂 ,找到了这个 issue

第三个问题你说的对的,我之前可能没搞对,用 have_no_content 和 have_content 就可以了。

SpiderEvgn 回复

第一个问题不是判断存在,是要等覆盖的元素消失了才 click。

变相的 等覆盖的元素消失了,读起来是有点奇怪

expect(page).to have_no_content('元素的text') # 等待元素消失
# do some click
heroyct 回复

哦~有道理,学到了!

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册