有一个Rails应用程序。说这是一个Rails应用程序,允许你喂养一些动物,我们有一个行动,给一些食物,许多动物在同一时间。要做到这一点,我们有一个类迭代每个动物并调用#eat
方法。eat
法是由starved
状态向sated
状态的过渡。如果动物已经是sated
,则此转换失败。
的例子:
class Animal < ActiveRecord::Base
state_machine :state do
state :starved
state :sated
event :eat do
transition starved: :sated
end
end
end
class EatingService
attr_reader :error_models, :models
def new(models)
@error_models = []
@models = models
end
def process
ActiveRecord::Base.transaction do
models.each { |model| @error_models << model unless model.eat }
raise ActiveRecord::Rollback unless successfully_completed?
end
successfully_completed?
end
def successfully_completed?
error_models.empty?
end
end
在添加事务之前,我可以用模拟对象轻松地测试它。
现在,我知道我不应该使用我的Dog
或Cat
类,因为EatingService
类没有绑定到任何类,但是我如何测试回滚在虚拟对象上工作良好?
PS:在这个例子中,我只谈论Animal
,但在实际应用中,我使用"EatingService
"有完全不同类型的类,不仅仅是动物或这些继承类。
在我看来,这样的设计打破了OOP原则"告诉,不要问",所以很难隔离测试。
餐饮服务承担了太多本不属于他们的责任。不管饿不饿都不是这个班级应该关心的。所有的服务需要做的就是让动物吃东西。至于吃不吃,那是动物的事。
我建议按照下面的逻辑,把判断移到动物身上。
# Eating service
def process
food = prepare_food
@animal.eat(food)
end
# Animal
def eat(food)
return false unless is_hungry? || like?(food)
chew(foo)
end
所有动物需要做的就是响应eat
方法,这很容易被嘲笑。服务的工作结束于送动物吃。
注释掉所有代码。然后编写一个测试文件,因为其中一行不在那里。重复,直到你有了代码。不要作弊,先写代码。
并且您的测试应该使用他们需要的任何对象。"隔离"并不意味着在目标类a上的测试不能发现类b中的错误。测试隔离只是意味着测试的通过或失败取决于尽可能少的因素,包括其他测试和其他目标类。
尽量不要使用mock。我曾见过尽管有超过1000个测试用例,但项目还是变慢了,因为测试滥用模拟,而且经常忽略测试实际的代码。
您可以检查ActiveRecord::Rollback
是否被抛出:
it "fails if someone is sated" do
allow(ActiveRecord::Base).to receive(:transaction).and_yield
allow(subject.models[1]).to receive(:eat).and_return(false)
expect { subject.process }.to raise_error ActiveRecord::Rollback
end