构建一个 Ruby Gem 第三章 配置 测试/调试 环境

一本没有测试相关内容的 Ruby 的书不是完整的。如果你对发布和贡献开源项目感兴趣的话,社区会更严肃的对待你的代码如果它们是被测试覆盖的并且测试通过的话。 测试驱动开发(TDD) 是一种实战,在你写代码之前先写测试。实践 TDD 帮助我们只写必要的能让测试通过的代码。这也能减少过度工程的可能性(注意到这里有一个模式了吗?)

在 Ruby 测试社区有两种观点。一种喜欢 Minitest (Ruby标准库内置的), 另一个更喜欢 Rspec。我喜欢后者并且每天都使用 Rspec。 我发现它很适合我而且我喜欢用 DSL 来组织我的测试。

依赖

为了加入 Rspec,让我们打开 mega_lotto.gemspec 文件然后加入下面的依赖:

spec.add_development_dependency "rspec"

注意: 由于我们不想强制让 rspec 被我们的宿主应用加载,我们使用 add_development_dependency 方法,而不是 add_dependency

现在, 让我们切换到终端,在我们的 gem 的根目录下运行:

$ bundle install

这会安装在 gemspec 里列出的所有的依赖(包括 Rspec)。输出结果就像下面那样:

Fetching gem metadata from https://rubygems.org/.........
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Resolvingdependencies...
Using rake (10.1.0)
Using bundler (1.3.5)
Using diff-lcs (1.2.5)
Using mega_lotto (0.0.1) from source at . Using rspec-core (2.14.7)
Using rspec-expectations (2.14.4) Using rspec-mocks (2.14.4)
Using rspec (2.14.1)
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

不要过于在意版本号,它们会经常更变。正如你所看到的, bundler 安装了 rakerspec (由多个 gem 组成),和我们的 gem, mega_lotto 。 为了完成 Rspec 的安装,在 gem 的根目录下运行下面的命令:

$ rspec --init

输出的结果如下:

create   spec/spec_helper.rb
create   .rspec

一个 spec 目录在我们的项目中被创建,并且里面有一个 spec_helper.rb 文件。

我们需要在 spec_helper 中加点东西。因为我们想要测试我们的 gem 的代码,我们需要从 spec_helper.rb 中加载它: 在 spec/spec_helper.rb 文件的头部加上 =require “mega_lotto”= 这行。

require "mega_lotto"
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# Require this file using `require "spec_helper"` to ensure that it is only
# loaded once.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
  config.treat_symbols_as_metadata_keys_with_true_values = true
  config.run_all_when_everything_filtered = true
  config.filter_run :focus
  # Run specs in random order to surface order dependencies. If you find an
  # order dependency and want to debug it, you can fix the order by providing
  # the seed, which is printed after each run.
  # --seed 1234
  config.order = 'random'
end

注意:不同版本的 rspecspec_helper.rb 中生产的内容也可能不同。如果你使用了不同版本的 rspec ,你的 spec_helper.rb 文件可能看上去不一样。

现在,我们可以在命令行运行 rspec spec 然后得到下面的信息:

No examples found.
Finished in 0.00007 seconds
0 examples, 0 failures

注意: rspec 是用来运行 rspec 测试的命令,不内涵。它接受文件路径作为参数(文件或目录)来确定要执行哪个测试。这确保了我们的测试基础设施被正确的设置了。

通常来说,=spec/= 目录(当使用 rspec 时我们的测试被安置的地方),是我们的 gem 的 lib/ 目录的镜像。 我们之后会看到这是如何工作的, 假如模块 lib/meag_lotto/drawing.rb 被加入到我们的 gem 中, spec/mega_lotto/drawing_spec.rb 将是相符的 spec 文件.

正如我们上面看到的,gem 可以有依赖。有时候当你安装一个 gem 时,几个其他的 gem 也会被安装。这是因为被定义在 gemspec 里的依赖。 如果你以前在一个 rails 应用中使用 unicorn gem,你可能注意到安装 unicorn 导致 gemfile.lock 中多了几行。 这几行就是在 unicorngemspec 中定义的依赖。

Rake 任务

为了运行通过 rspec spec/ 命令来执行我们的 specs,我们可以更新我们的 Rakefile 来包含一个 spec 任务并且设置为默认:

require "bundler/gem_tasks"
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)

task :default => :spec

现在我们可以使用我们的终端来运行 rake 来看看和上面一样的输出:

$ rake
No examples found.
Finished in 0.00007 seconds
0 examples, 0 failures

我们要加入的另一个任务是一个快捷键来进入一个终端会话。如果你熟悉 Rails,你应该知道 rails c 是一个很牛逼的工具。 我们可以给我们的 gem 类似的功能。如果我们的系统中有 Ruby,我们可以打开一个命令行使用 irb 命令来进入一个 Ruby 交互环境:

$ irb
irb(main):001:0> 2 + 2 => 4
irb(main):002:0> exit

非常好,只不过解释器没有加载任何 Ruby 标准库以外的东西。这对我们没什么帮助,但是幸运的是 irb 命令接受的一些参数可以帮我们一些忙:

$ irb --help
Usage:  irb.rb [options] [programfile] [arguments]
  -f            Suppress read of ~/.irbrc
  -m            Bc mode (load mathn, fraction or matrix are available)
  -d                Set $DEBUG to true (same as `ruby -d')
  -r load-module    Same as `ruby -r'
  -I path           Specify $LOAD_PATH directory
  -U                Same as `ruby -U`
  -E enc            Same as `ruby -E`
  -w                Same as `ruby -w`
  -W[level=2]       Same as `ruby -W`

-I 参数允许我们加入一个特定的目录到 Ruby 的 load path。

记得吗当我们讨论 lib/mega_lotto 目录时看到的 mega_lotto.gemspec 文件的头部

...
lib = File.expand_path('../lib', ___FILE___)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
...

当 bundler 加载我们的 gem 时,它也加载了 lib/ 目录, 所以我们可以使用我们的库。

所以通过使用 -I 我们可以指定 lib/ 目录来保证 irb 可以使用我们的代码。

另外,在通常状况下,我们看到 irb 不会加载 Ruby 标准库以外的库。所以即使我们加上了我们的 lib/ 目录到 load path, 我们不得不指定调用 require "mega_lotto"= 来加载我们的代码当会话开始时。 所以,选项 =-r 被我们使用了。它允许我们当会话开始时去加载一个指定库,所以我们不用手动去做这件事了。

把这些参数组合起来我们就得到了一个牛逼的工具来检验我们的 gem 的代码:

$ irb -r mega_lotto -I ./lib
irb(main):001> MegaLotto
=> MegaLotto

我们可以看到 MegaLotto 模块在使用合适的参数的irb会话中被使用了。

更进一步,我们可以把这行命令加入到我们的 =Rakefile=,这样我们就能更容易的调用了。

require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new("spec")

task :default  => :spec

task :console do
  exec "irb -r mega_lotto -I ./lib"
end

这让我们可以快捷的从命令行运行 rake console

$ rake console

irb(main):001:0> MegaLotto
=> MegaLotto

调试

无论我们怎么不愿意承认,我们没有写出完美的代码。Ruby 有很多调试工具,但是 pry 是我的选择。 既然 mega_lotto.gemspec 负责根据环境加载依赖,我们可以加入 pry 到开发列表中:

...
spec.add_development_dependency "bundler", "~> 1.3"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec"
spec.add_development_dependency "pry"

运行 bundle install 我们可以看到 pry gem 被列出来了:

Resolving dependencies...
Using rake (10.1.0)
Using bundler (1.3.5)
Using coderay (1.1.0)
Using diff-lcs (1.2.5)
Using mega_lotto (0.0.2) from source at . Using method_source (0.8.2)
Using slop (3.4.7)
Using pry (0.9.12.3)
Using rspec-core (2.14.7)
Using rspec-expectations (2.14.4)
Using rspec-mocks (2.14.4)
Using rspec (2.14.1)
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

注意: pry gem 已经安装在我的系统中了,所以在输出中有 “Using pry (…)”. 如果 pry 之前没有被安装,输出应该是 “Installing pry(…)”

现在 pry 安装好了,我们需要去加载它。正如前面所提到的, lib/mega_lotto.rb 文件是加载其他代码到 gem 里的主要入口。

通常情况下,我们可以在头部引入 pry 。然而,记住我们只在开发环境下使用 pry 这意味着当我们的 gem 被宿主应用加载时,它会尝试去加载 pry ,有可能会因为没有 pry 而抛出 Ruby LoadError 异常。

知道了这个可能发生的异常,我们可以使用 rescue 然后不做任何处理:

# lib/mega_lotto.rb

require "mega_lotto/version"

begin
  require "pry"
rescue LoadError
end

module MegaLotto
end

注意我们是如何使用 rescue LoadError 代码块来捕获异常的,但是现在我们不做任何处理。 如果不这样做, LoadError 异常就会被抛出然后我们的代码就不能被执行下去了。

一旦 pry 被加载,我们就可以使用 binding.pry 方法来停止代码在那个点上并且开启一个 REPL 会话来调试。让我们在模块里加入:

require "mega_lotto/version"

begin
  require "pry"
rescue LoadError
end

module MegaLotto
  binding.pry
end
$ rake console
Frame number: 0/7
From: /Users/bhilkert/Dropbox/code/mega_lotto/lib/mega_lotto.rb @ line 12 :
     7:   require "pry"
     8: rescue LoadError
     9: end
     10:
     11: module MegaLotto
     => 12: binding.pry
     13: end
[1] pry(MegaLotto)>

完美!

注意: 使用 exit 命令来退出 pry 会话。

我们就不继续下去了,我们会从入口文件移除 binding.pry ,但是保留加载的代码这样我们以后还可以使用:

require "mega_lotto/version"

begin
  require "pry"
rescue LoadError
end

module MegaLotto
end

使用 pry 的详细内容不是本书的范围。这是一个牛逼的 gem 并且值得去探索如果你经常使用 Ruby。

我们就到此为止了,因为我们的测试框架和调试工具都已经被正确的安装和配置了。

总结

我们在本章花费了时间来配置很多工具。它们并不都是必要的,但是在我的开发过程中我发现了它们的价值。 一旦你多试几次这个过程,就只需要花上几分钟就能完成。有了这些工具能帮助你解决一些很麻烦的 bug。

如果你更喜欢 minitest 而不是 rspec ,配置起来会容易些因为 minitest 已经内置在 Ruby 标准库中了,所以不需要额外的 gem。

最后,gem 的依赖会很快的变得复杂。如果一个依赖的定义没有被维护,就可能给你留下很多 bug 和坑。 当然,依赖是有价值的,没有理由去复制功能如果已经有了可靠的解决方案。只要知道,随着你增加依赖,你的 gem 的复杂度就会上升。

在下一章,我们会使用测试驱动的方式来研究和探索 Ruby 的命名空间是如何管理 gem 的文件结构的。