Given/When/Then

Given 用来表示在一个scenario中我们认可为true的事物.
通过这个声明来给出在scenario中要发生的事件的上下文语境

given经常被误认为是先决条件, 但两者有概念上的不同:
先决条件是指某种强制约束, 如果达不到某一条件就无法继续进行下去;
但是given并被没有这种强制性, 为了使特定行为能满足其所需条件, given给出的条件可以被打破

换言之, 可以有Given the world is round, 但绝不会有Given the world is flat

When 用来表示scenario中的事件, 倾向于每个scenario只有一个独立事件

Then 表示期待的结果

Tags

通过类似实例变量@xxx的形式指定tag,可以对feature和scenario指定任意数量的tag

1
2
3
4
5
@approved @iteration_12
Feature: patient requests appointment

  @wip
  Scenario: patient selects available time

scenario会继承feature的tag, 运行时通过--tags指定执行

1
2
3
4
cucumber --tags @wip             #执行全部有@wip的scenarios
cucumber --tags @foo,@bar        #执行有@foo或者@bar的scenarios
cucumber --tags @foo --tags @bar #执行同时有@foo和@bar的
cucumber --tags ~@dev            #没有@dev的

参数

在step definations中的正则表达式如果包含caputre group, 则会把他们作为参数传给代码块, 例如

1
2
3
4
Given /^a hotel with "([^"]*)" rooms and "([^"] *)" bookings$/ do
  |room_count, booking_count|
  # blablabla
end

而这些step使用时候, 参数的部分也最好用引号括起来(非强制)

1
2
Scenario: Successful booking
  Given a hotel with "5" rooms and "0" bookings

World

所有的cucumber scenario都运行在一个被称作World的对象新的实例上下文里
默认情况下World只是Object的实例, 在每个scenario之前被实例化
对于同一scenario的所有step definitions, 其代码块都在相同的上下文执行

可以使用World()方法自定义World, 方法接受一个或多个module

1
2
3
4
5
6
7
module MyHelper
  def some_helper
    ...
  end
end

World(MyHelper)

可以在features或其子目录下的任意ruby文件里配置自定义的World,
但是推荐的做法是将其放在features/support/world.rb

除了可以在World里混入代码块之外, 还可以更改用于实例化World的class的类型,
只需要在World()方法传入代码块

1
2
3
4
5
6
7
8
9
class MyWorld
  def some_helper
    ...
  end
end

World do
  MyWorld.new
end

Calling Steps Within Step Definitions

1
2
3
4
5
6
When /I transfer (.*) from (.*) to (.*)/ do |amount, source, target|
  When "I select #{source} as the source account"
  When "I select #{target} as the target account"
  When "I set #{amount} as the amount"
  When "I click transfer"
end

以上代码等效于:

1
2
3
4
5
6
7
8
When /I transfer (.*) from (.*) to (.*)/ do |amount, source, target|
  steps %Q{
    When I select #{source} as the source account
    And I select #{target} as the target account
    And I set #{amount} as the amount
    And I click transfer
  }
end

Tagged Hooks

Before(“@foo,~@bar”, “@zap”) do puts “This will run before each scenario tagged with @foo or not @bar AND @zap” end

Background

有时hooks非技术人员难以理解, 可以用background

1
2
3
4
5
6
7
8
9
10
11
Feature: invite friends
  Background: Logged in
    Given I am logged in as "Aslak"
    And the following people exist:
      | name   | friend? |
      | David  | yes     |
      | Vidkun | no      |

  Scenario: Invite someone who is already a friend
  Scenario: Invite someone who is not a friend
  Scenario: Invite someone who doesn't have an account

background在给定的feature里每个scenario之前执行,
如果有Before hooks, 则先执行Before, 再执行background

Configuration

可以在为cucumber添加配置文件, 放在cucumber.ymlconfig/cucumber.yml下

cucumber.yml
1
wip: --tags @wip features

执行cucumber时

1
cucumber -p wip

The rspec Command

1
2
rspec simple_math_spec.rb  #执行单个文件
rspec spec                 #执行spec文件夹下全部文件 

–format,设置输出格式

1
2
3
4
5
rspec path/to/my/specs --format documentation
rspec path/to/my/specs --format html:path/to/my/report.html
rspec path/to/my/specs  --format progress \
                        --format nested:path/to/my/report.txt \
                        --format html:path/to/my/report.html

其他

1
2
rspec spec --backtrace
rspec spec --color

Rake

1
2
rake spec               #执行spec下的全部specs文件
rake spec:controllers   #执行spec下的全部specs/controllers文件

完整命令列表可以通过执行rake -T spec获取, 这些命令被定义在RSpec::Core::RakeTask

可以在Rakefile里添加以下内容作来配置以rake执行的rspec

1
2
3
4
5
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new do |t|
  t.rspec_opts = ["--color", "--format", "specdoc"]
end

Filtering

Inclusion

1
2
3
4
5
6
7
8
9
10
11
RSpec.configure do |c|
  c.filter = { :focus => true }
end

describe "group" do
  it "example 1", :focus => true do
  end

  it "example 2" do
  end
end

上面这段代码执行后会得到类似下面的输出

1
2
3
4
5
group
  example 2

Finished in 0.00067 seconds
1 example, 0 failures

其中it()里的:focus => true部分被称为metadata
值可以通过example.metadata[:focus]获取到

Exclusion

1
2
3
4
5
6
7
8
9
10
11
RSpec.configure do |c|
c.exclusion_filter = { :slow => true }
end

describe "group" do
  it "example 1", :slow => true do
  end

  it "example 2" do
  end
end

执行时”example 2”被排除在外

Lambdas

Inclusion与Exclusion的filter都可以接受一个lambda处理更复杂的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require 'ping'

RSpec.configure do |c|
  c.exclusion_filter = {
    :if => lambda {|what|
      case what
      when :network_available
        !Ping.pingecho "example.com", 10, 80
      end
    }
  }
end

describe "network group" do
  it "example 1", :if => :network_available do
  end

  it "example 2" do
  end
end

faye的client如果想要接收到某个channel的聊天信息, 需要先subscribe这个channel

1
2
3
4
5
var faye = new Faye.Client("localhost:9292/faye");

faye.subscribe('/chat/channelXX', function (data) {
  XXXXXXXXXXXXXx
})

在进行subscribe时候, 会使用特定的channel /meta/subscribe,
并且faye server会对faye client分配一个唯一的client_id

类似的, 当faye client执行unsubscribe和disconnect时,
也会使用/meta/unsubscribe/meta/disconnect

于是, 可以在faye的browser client端添加扩展
使其在使用channel /meta/subscribe时, 传入ActiveRecord的相关id

1
2
3
4
5
6
7
8
faye.addExtension({
  outgoing: function(message, callback) {
    if (message.channel == '/meta/subscribe') {
      message.data = {user_id: <%= current_user.id %>, chat_team_id: <%= @chat_team.id %>};
    }
    callback(message);
  }
}); 

之后, 与faye server端添加扩展, 通过传入的数据, 找到相应ActiveRecord记录, 并更改其状态为在线
同时还要记录下其client_id, 作为此user离线时再次找到此人的依据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class  MarkOnline
  def incoming(message, callback)

    if message['channel'] == '/meta/subscribe'
      UserChatTeam.mark_online( message['data']['user_id'],
                                message['data']['chat_team_id'],
                                message['clientId'])
    end

    callback.call(message)
  end

end

faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45)
faye_server.add_extension(MarkOnline.new)

为了能在rack程序里使用ActiveRecord, 还需要在faye.ru文件里添加以下内容

1
2
3
4
5
6
7
8
require 'active_record'
require 'mysql'
require 'yaml'
require File.expand_path('../app/models/user_chat_team.rb', __FILE__)

environment = ENV['RACK_ENV'] || 'production'
dbconfig    = YAML.load(File.read('config/database.yml'))
ActiveRecord::Base.establish_connection(dbconfig[environment])

接下来在faye server里添加monitor, 监视unsubscribe和disconnect事件
并根据之前记录的client_id将相关记录标记为离线, 同时清除client_id

1
2
3
4
5
6
7
faye_server.on('unsubscribe') do |client_id, channel|
  UserChatTeam.mark_offline(client_id)
end

faye_server.on('disconnect') do |client_id|
  UserChatTeam.mark_offline(client_id)
end

可能会出现的问题
同一user对相同channel如果打开多个窗口
当其关闭其中一个窗口后, 即会判断为此user已离线

参考
faye monitoring
https://github.com/eoecn/faye-online
Faye Exensions: Tracking Users in a Chat Room

faye.ru
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
require 'faye'
require File.expand_path('../config/initializers/faye_token.rb', __FILE__)
require 'active_record'
require 'mysql'
require 'yaml'

# using 'acts_as_paranoid' as a plugin in 'vendor/plugins'
RAILS_ENV = ENV['RACK_ENV']
load 'active_record/associations.rb'
require File.expand_path('../vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid.rb', __FILE__)
require File.expand_path('../vendor/plugins/acts_as_paranoid/lib/caboose/acts/belongs_to_with_deleted_association.rb', __FILE__)
require File.expand_path('../vendor/plugins/acts_as_paranoid/lib/caboose/acts/has_many_through_without_deleted_association.rb', __FILE__)
require File.expand_path('../vendor/plugins/acts_as_paranoid/init.rb', __FILE__)
require File.expand_path('../app/models/user_chat_team.rb', __FILE__)

environment = ENV['RACK_ENV'] || 'production'
dbconfig    = YAML.load(File.read('config/database.yml'))
ActiveRecord::Base.establish_connection(dbconfig[environment])

class ServerAuth
  def incoming(message, callback)

    if message['channel'] !~ %r{^/meta/}
      if message['ext']['auth_token'] != FAYE_TOKEN
        message['error'] = 'Invalid authentication token.'
      else
        message['ext'].delete('auth_token')
      end
    end

    callback.call(message)
  end
end

class  MarkOnline
  def incoming(message, callback)

    if message['channel'] == '/meta/subscribe'
      UserChatTeam.mark_online( message['data']['user_id'],
                                message['data']['chat_team_id'],
                                message['clientId'])
    end

    callback.call(message)
  end

end


faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45)
faye_server.add_extension(ServerAuth.new)
faye_server.add_extension(MarkOnline.new)

faye_server.on('unsubscribe') do |client_id, channel|
  UserChatTeam.mark_offline(client_id)
end

faye_server.on('disconnect') do |client_id|
  UserChatTeam.mark_offline(client_id)
end

run faye_server
faye browser client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var faye = new Faye.Client("<%= @faye_server %>/faye");
faye.disable('websocket');

faye.addExtension({
  outgoing: function(message, callback) {
    if (message.channel == '/meta/subscribe') {
      message.data = {user_id: <%= current_user.id %>, chat_team_id: <%= @chat_team.id %>};
    }
    callback(message);
  }
}); 

faye.subscribe('/chat/<%= @chat_team.channel %>', function (data) {
  $("chat_list").insert({bottom: data["chat_log"].toString()});

  var post = $("chat_list").childElements().last();

  if(data["user_id"].toString() != "<%= current_user.id %>"){
    post.removeClassName("my-post");
    post.addClassName("member-post")
  }
  var objDiv = $("chat_list");
  objDiv.scrollTop = objDiv.scrollHeight;    
  new Effect.Highlight(post.id);
  $("errors").hide();
});