接上篇:Rails模式的实际应用--第一部分:Model(上)
https://www.testwo.com/article/892
选择
框架特征的易用性和源于紧密耦合的可维护性之间的紧张关系,对数据的持久性和数据验证有很大的影响。想要去找一个解决方案,我们可以先问问自己,当两件事情关联过于紧密的时候我们会怎么做:分开他们。
如果我们有一件事情涉及保存一个movie,另一件事情涉及验证一个movie,我们通常会创建更好的类的去解绑关联。我们最终会得到两个新的类,他们更符合单一责任原则。感谢随之而来的一个副作用,我们得到的好处是更好的可测试性。任何不是特定的要关联到某个指定movie的测试,我们都可以创建一个新的movie去适应它。
代码示例
使用之前movie的例子,我们想要做的无非是把所有关于验证一个movie的数据的代码单独封装成一个新的类。
让我们以我们的新类MovieValidator来开始我们的第一个测试。
RSpec.describe MovieValidator do
let(:valid_movie) {Movie.new(name: "the name", release_date: DateTime.parse("08 June 1984")}
it "validates a movie" do
expect(MovieValidator.valid?(valid_movie)).to be_truthy
Endend
MovieValidator有一个叫做valid的方法,根据给定的movie是否是有效的返回true或者false。
为了让测试通过,我们的办法如下:
class MovieValidator
def self.valid?(movie)
has_title?(movie)
end
private
def self.has_title?(movie)
movie.title.present?
endend
但是如果movie是无效的会发生什么呢?我们需要一个可以向MovieValidator的服务器返回验证消息的办法。随后,这些信息会被展示给终端用户,例如。
让我们用可以区分这些信息的测试来捕捉这些信息。
RSpec.describe MovieValidator do
let(:valid_movie) {Movie.new(name: "the name", release_date: DateTime.parse("08 June 1984")}
let(:invalid_movie) {Movie.new}
def validation_of(movie)
MovieValidator.validate(movie)
end
it "validates a movie" do
expect(validation_of(valid_movie)).to be_okay
end
it "does not contain any messages" do
expect(validation_of(valid_movie).messages).to be_empty
end
it "does not validate an invalid movie" do
expect(validation_of(invalid_movie)).not_to be_okay
end
it "has messages for invalid movies" do
expect(validation_of(invalid_movie).messages).to contain("Name cannot be empty.")
endend
为了让测试通过,我们来介绍一下验证结果的概念。
class MovieValidator
def self.validate(movie)
new(movie).validate
end
def initialize(movie)
@movie = movie
end
def validate
result = Result.new
result << validate_title
result
end
private
attr_reader :movie
def validate_title
"Title can't be empty." if title_empty?
end
def title_empty?
movie.title.blank?
end
class Result
attr_reader :messages
def initialize
@messages = []
end
def okay?
messages.empty?
end
def <<(message)
@messages << message if message
end
endend
当一个要求movie的有效性的需求提出来的时候,我们可以用Movievalidator来独立的实现而不影响其他的所有用户。测试的最终版本和产出的代码可以再GitHub上找到。
相对于我们自己进行验证,另一个方案是引入ActiveModel:Validations。取决于手上的用例,这是一个可选择的方法,我们可以充分利用已有的实现几乎不用做任何改动。
class MovieValidator
include ActiveModel::Validations
validates_presence_of :title
validate :release_year_not_before_1895
def initialize(movie)
@title = movie.title
@release_date = movie.release_date
end
private
attr_reader :title, :release_date
def release_year_not_before_1895
if release_date.nil?
errors.add(:release_date, "must be provided.")
elsif release_date.year < 1895
errors.add(:release_date, "is older than the world's first motion picture.")
end
endend
提取验证之后,Movie类变成了什么样子呢?
class Movie < ActiveRecord::Base
end
有一句真理是,最好的代码就是没有代码。我们设法让我们的模型摆脱所有自定义的逻辑。我们的movie只是通向持久性的大门,而不是其他。
我们保留了哪些ActiveRecord的测试呢?
理想情况下没有,但容我解释一下。
完全不用测试是不可能的。最后我们还是需要确认一个Movie在一些时间点被保存了。但是我们不需要去合适ActiveRecord如预期一般执行。那是一个我们期望能够正常运转的依赖。
我们真正需要验证的是,我们代码中与ActiveRecord相关的部分做的事情是正确的。这个当前在测试和MovieController的应用里是指定的。
在即将到来的这个系列的其他博文里面,我们会继续探索关于当前做的是否正确的更细节,以及是否还有更好的选择。
【英文原文:https://8thlight.com/blog/christoph-gockel/2016/10/19/getting-rails-on-track-part-1-models.html】
{测试窝原创译文,译者:婷婷}
译者简介:婷婷,一个萌妹子~