Rails 事务

Rails 中的事务

Rails 的事务使用方法有两种:

第一种使用方法:直接使用 ActiveRecord::Base 中的事务,如:

ActiveRecord::Base.transaction do
  或许会产生异常的处理
end
  不产生异常时的处理
rescue => e
  产生异常时的处理

第二种,使用模型的事务。由于模型继承了 ActiveRecord::Base,所以模型也包含了事务:

模型.transaction do
  或许会产生异常的处理
end
  不产生异常时的处理
rescue => e
  产生异常时的处理

Rails 中多表事务

Rails 在使用事务时经常会涉及多张表。比如一个 Release 的实例会关联多个 WorkOrder 实例,那么在事务中先存储 Release,然后存储 WorkOrder 时触发异常(比如 redmine_id 的唯一性约束):

Release.transaction do
  release = Release.create
  release.work_orders.create!(redmine_id:1)
  release.work_orders.create!(redmine_id:1)
end

经实验,ReleaseWorkOrder 都会回滚。

Ruby Mechanize 使用小结

1. Mechanize 简介

The Mechanize library is used for automating interaction with websites. Mechanize automatically stores and sends cookies, follows redirects, and can follow links and submit forms. Form fields can be populated and submitted. Mechanize also keeps track of the sites that you have visited as a history.

简单来说:

Mechanize 是一个用来自动化和网站交互的类库。Mechanize 有着自动存储和发送 cookies、跟踪重定向和链接以及提交表单的功能。Mechanize 可以产生和提交表单的域。Mechanize 还能够记录你访问的历史记录。

2. Mechanize 简单使用

使用 Mechanize 需要 require 一下:

require 'mechanize'

2.1 Mechanize 获取页面

我们首先获取一个浏览器的代理:

@agent = Mechanize.new

我们可以对这个代理进行设置,伪装成各种主流的浏览器(此处不表)。然后通过 URL 获取指定页面:

@page = @agent.get('www.github.com')

2.2 表单操作

2.2.1 搜索框

@agent = Mechanize.new
@page = @agent.get('xxx.xxx.com')
form = @page.forms.first
form["q"] = "xxxxx"
result = form.submit

上述代码中使用 @page.forms.first 获取了第一个表单,在页面中包含多个表单时,可以使用 @page.form_with(action: "search") 这样的方法获取特定的表单。

2.2.2 登录表单

@agent = Mechanize.new
@page = @agent.get('xxx.xxx.com')
form = @page.form_with(action: "login")
form["username"] = "username"
form["password"] = "password"
result = form.submit

form 使用 hash 的形式进行参数的赋值,表单中的每个元素用 name 属性来获取。

2.3 获取页面中的元素

从页面中获取元素常见的方法是 @page.links 或者 @page.forms 这样的内建方法。但是要从页面中获取某个特定标签时,可以使用 XPath 来实现:

@page.parser.xpath("//div[name='content']/p[id='id']")

上述语句获取的是页面中一个 name 属性值为 contentdiv 标签下的 id 属性为 idp 标签。XPath 的具体使用方法请看参考文献。

但是偶尔也会碰上你想获取的元素既没有 id 也没有 name 属性的情况,这个时候只能靠类似下文的方法暴力匹配了:

link.children[0].children[2].children[1].text

2.4 POST 数据到指定 URI

Mechanize 不支持 Javascript,对于某些需要 Javascript 支持的表单,Mechanize 无法帮你获取到数据。但是我们可以直接发出 POST 请求。
一般来说,我们可以先使用 Firefox 或者 Chrome 浏览器查看执行 POST 请求时发送的数据中参数的名称、格式以及地址。然后将其作为模板,用我们实际使用的参数替换模板中的参数。以下就是一个例子:

post_url = "http://address/to/your/target/action"

date = Time.now.strftime('%Y-%m-%d')
release_params ={
  "issue[tracker_id]" => "9",
  "issue[subject]" => "[xx上线任务] #{date}",
  "issue[description]" => "#{description}",
  "issue[project_id]" => "46",
  "issue[assigned_to_id]" => "642",
  "issue[status_id]" => "1",
  "issue[priority_id]" => "13",
  "issue[custom_field_values][30]" => "642",
  "issue[custom_field_values][37]" => "xxxx"
}
@agent.post(post_url, release_params)

3. 参考文献

RSpec 测试——2. RSpec Expectations

RSpec::Expectations 允许你表达一个样例中对象的预期结果。

1. 内建匹配器

1.1 相等

expect(actual).to eq(expected)  # passes if actual == expected
expect(actual).to eql(expected) # passes if actual.eql?(expected)
expect(actual).not_to eql(not_expected) # passes if not(actual.eql?(expected))

注意:新的 expect 语法不再支持 == 匹配器。

1.2 身份标识

expect(actual).to be(expected)    # passes if actual.equal?(expected)
expect(actual).to equal(expected) # passes if actual.equal?(expected)

1.3 比较

expect(actual).to be >  expected
expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be <  expected
expect(actual).to be_within(delta).of(expected)

1.4 正则表达式

expect(actual).to match(/expression/)

1.5 类型

expect(actual).to be_an_instance_of(expected) # passes if actual.class == expected
expect(actual).to be_a(expected)              # passes if actual.is_a?(expected)
expect(actual).to be_an(expected)             # an alias for be_a
expect(actual).to be_a_kind_of(expected)      # another alias

1.6 真假

expect(actual).to be_truthy   # passes if actual is truthy (not nil or false)
expect(actual).to be true     # passes if actual == true
expect(actual).to be_falsy    # passes if actual is falsy (nil or false)
expect(actual).to be false    # passes if actual == false
expect(actual).to be_nil      # passes if actual is nil
expect(actual).to_not be_nil  # passes if actual is not nil

1.7 预期错误

expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
expect { ... }.to raise_error("message")
expect { ... }.to raise_error(ErrorClass, "message")

1.8 预期抛出

expect { ... }.to throw_symbol
expect { ... }.to throw_symbol(:symbol)
expect { ... }.to throw_symbol(:symbol, 'value')

1.9 断言匹配

expect(actual).to be_xxx         # passes if actual.xxx?
expect(actual).to have_xxx(:arg) # passes if actual.has_xxx?(:arg)

1.10 范围(Ruby >= 1.9)

expect(1..10).to cover(3)

1.11 集合

expect(actual).to include(expected)
expect(actual).to start_with(expected)
expect(actual).to end_with(expected)

expect(actual).to contain_exactly(individual, items)
# ...which is the same as:
expect(actual).to match_array(expected_array)

2 should 和 should_not 内建匹配器

2.1 相等

actual.should     eq(expected)  # passes if actual == expected
actual.should     == expected   # passes if actual == expected
actual.should_not eql(expected) # passes if actual.eql?(expected)

注意:我们推荐 eq 匹配器而不是 == 以避免 Ruby 的警告。

2.2 身份标识

actual.should     be(expected)    # passes if actual.equal?(expected)
actual.should_not equal(expected) # passes if actual.equal?(expected)

2.3 比较

actual.should be >  expected
actual.should be >= expected
actual.should be <= expected
actual.should be <  expected
actual.should be_within(delta).of(expected)

2.4 正则表达式

actual.should match(/expression/)
actual.should =~ /expression/

2.5 类型

actual.should     be_an_instance_of(expected)
actual.should_not be_a_kind_of(expected)

2.6 真假

actual.should be_true  # passes if actual is truthy (not nil or false)
actual.should be_false # passes if actual is falsy (nil or false)
actual.should be_nil   # passes if actual is nil

2.7 断言

actual.should     be_xxx         # passes if actual.xxx?
actual.should_not have_xxx(:arg) # passes if actual.has_xxx?(:arg)

2.8 范围(Ruby >= 1.9)

(1..10).should cover(3)

2.9 集合

actual.should include(expected)
actual.should start_with(expected)
actual.should end_with(expected)

RSpec 测试——1. RSpec Core

1. 基本结构

RSpec.describe Order do
  it "sums the prices of its line items" do
    #something...
  end
end

2. 紧凑结构

RSpec.describe Order do
  context "with no items" do
    it "behaves one way" do
      # ...
    end
  end

  context "with one item" do
    it "behaves another way" do
      # ...
    end
  end
end

3. 别名

对于顶层的 example group 来说应该用 describe 和 context,组群内可以使用 it、specify、example。
it、specify、example 是测试的基本单元,它们内部不能使用 before 语句块。before 语句块只能在 describe 和 context 内使用。

4. 共享的样例和上下文

RSpec.shared_examples "collections" do |collection_class|
  it "is empty when first created" do
    expect(collection_class.new).to be_empty
  end
end

RSpec.describe Array do
  include_examples "collections", Array
end

RSpec.describe Hash do
  include_examples "collections", Hash
end

在共享样例中几乎可以声明一切,before、after 以及 around 等钩子方法,let 声明以及紧凑结构等。
类似的用法还有 shared_context 以及 include_context。

5. 元数据

rspec-core 存储了一个包含所有样例和组群的元数据 hash。可以通过如下方法从样例中获取:

it "does something" do
  expect(example.metadata[:description]).to eq("does something")
end

或者:

RSpec.shared_examples "collections" do
  it "is empty when first created" do
    expect(described_class.new).to be_empty
  end
end

RSpec.describe Array do
  include_examples "collections"
end

RSpec.describe Hash do
  include_examples "collections"
end

6. RSpec 命令

获取帮助:rspec --help
以文档格式输出:rspec <spec_file> --format doc
以网页格式输出:rspec <spec_file> --format html
rspec 命令的参数储存到 .rspec 文件中来自动调用如:echo "--format doc" >> .rspec

Rails使用RSpec的配置

1. Gemfile 引入 RSpec

group :development, :test do
  gem 'sqlite3', '1.3.8'
  gem 'rspec-rails', '2.13.1'
end

group :test do
  gem 'selenium-webdriver', '2.35.1'
  gem 'capybara', '2.1.0'
  gem 'factory_girl_rails', '4.2.1'
end

2. Rails 初始化跳过默认的 Test::Unit

rails new <app> --skip-test-unit

3. Rails 设置使用 RSpec进行测试

rails generate rspec:install

结果是:

创建 .rspec 目录
创建 spec 目录
创建 spec/spec_helper.rb 文件

4. 把 CapybaraDSL 加入 RSpec

spec_helper.rb

RSpec.configure do |config|
  ...
  config.include Capybara::DSL
end

Ruby Rugged 使用简介

1. 初始化仓库

1.1 使用路径打开存在的仓库

repo = Rugged::Repository.new('path/to/my/repository')

1.2 创建一个新的仓库

Rugged::Repository.init_at('.', :bare)

1.3 根据子目录搜寻仓库

Rugged::Repository.discover("/Users/me/projects/repo/lib/subdir/")

2. 分支操作

2.1 创建分支

repo.create_branch('branch-name')

2.2 切换到某一个分支

repo.checkout('branch-name')

2.3 查找分支

根据某个关键字查找分支:

branch_names = []
last_commits = []

repo.branches.each do |branch|
  keywords.each do |keyword|
    if branch.name.include?(keyword)
      branch_names << branch.name
      last_commits << repo.branches[branch.name].target
    end
  end
end

branch_names 用来保存符合条件的分支名字,last_commits 用来保存这些分支的最后一次提交。

2.4 获取分支

branch = repo.branches[branch_name]

3. 增加对文件的追踪

3.1 增加对单个文件的追踪

repo.index.add('./b.txt') # path or object
commit_tree = repo.index.write_tree repo
repo.index.write

相当于 git add b.txt 的操作。注意:index.write 操作会同步暂存区和工作目录。在增加或者删除对文件的追踪后必须要执行,否则执行 git status 看不到预期的效果。疑问:commit_tree = repo.index.write_tree repo 的作用是获取提交时所需要的 tree 参数?

3.2 增加对多个文件的追踪

repo.index.add_all
repo.index.write

相当于 git add -A 操作。

4. 创建 commit

author = { email: "wuzhiyu@note4code.com",
           name: "wuzhiyu",
           time: Time.now }
options = {}
options[:author] = author
options[:commiter] = author
options[:message] = "commit message here"
options[:parents] = repo.empty? ? [] : [ repo.head.target ].compact
options[:tree] = commit_tree
options[:update_ref] = 'HEAD'
Rugged::Commit.create(repo, options)

5. 分支的合并

5.1 commit 合并分析

repo.merge_analysis(last_commits[index])

返回一个包含如下 symbol 组合的数组:

symbol description
:normal A “normal” merge is possible, both HEAD and the given commit have diverged from their common ancestor. The divergent commits must be merged.
:up_to_date The given commit is reachable from HEAD, meaning HEAD is up-to-date and no merge needs to be performed.
:fastforward The given commit is a fast-forward from HEAD and no merge needs to be performed. HEAD can simply be set to the given commit.
:unborn The HEAD of the current repository is “unborn” and does not point to a valid commit. No merge can be performed, but the caller may wish to simply set HEAD to the given commit.

5.2 commit 的合并

#git fetch:
remote = Rugged::Remote.lookup(repo, "origin")
remote.connect(:fetch) do |r|
  r.download
  r.update_tips!
end

#git merge:
merge_index = repo.merge_commits(
  Rugged::Branches.lookup(repo, "master").tip,
  Rugged::Branches.lookup(repo, "origin/master").tip
)
raise "Conflict detected!" if merge_index.conflicts?
merge_commit = Rugged::Commit.create(repo, {
  parents: [
    Rugged::Branches.lookup(repo, "master").tip,
    Rugged::Branches.lookup(repo, "origin/master").tip
  ],
  tree: merge_index.write_tree(repo),
  message: 'Merged `origin/master` into `master`',
  author:    { name: "User", email: "example@test.com" },
  committer: { name: "User", email: "example@test.com" },
  update_ref: 'master'
})

上文中使用了 merge_index.conflicts? 来测试是否有冲突。但是这种冲突检测方法仅限于检测目录的冲突,也就是说只要两个分支修改了同一个文件就会报冲突,但实际上修改的内容可能并不冲突。而使用合并命令则可以针对行来检测是否冲突。

6.参考