Ruby Hanami 的 Action 测试可以不依赖数据库

chenge · 2017年07月02日 · 最后由 lithium4010 回复于 2017年07月03日 · 1679 次阅读

这个是文档中的代码。

不依赖数据库可以快捷一些。不知道 Rails 能否做到?


# spec/web/controllers/users/show_spec.rb
require 'spec_helper'
require_relative '../../../../apps/web/controllers/users/show'

RSpec.describe Web::Controllers::Users::Show do
  let(:action)     { Web::Controllers::Users::Show.new(repository: repository) }
  let(:user)       { User.new(id: 23, name: 'Luca') }
  let(:repository) { double('repository', find: user) }

  it "is successful" do
    response = action.call(id: user.id)

    expect(response[0]).to      eq 200
    expect(action.user).to      eq user
    expect(action.exposures).to eq({user: user})
  end
end

# apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action
    expose :user

    def initialize(repository: UserRepository.new)
      @repository = repository
    end

    def call(params)
      @user = @repository.find(params[:id])
    end
  end
end
  1. http://lmgtfy.com/?q=activerecord+mock

  2. Dependency injection is not a virtue http://david.heinemeierhansson.com/2012/dependency-injection-is-not-a-virtue.html

  3. 用 mock 测试不执行数据库操作,真的能确认应用没有问题么?

@Rei你是全盘否定 mock 啊?

顺着 DHH 的思路,我还是想到一个办法,把查询写在 service 里,用 stub 来代替。

我觉得还是有必要分开测试 action 和数据操作,效率高一些吧。

chenge 回复

我没有全盘否定 mock,我会用在不方便测的地方,比如第三方服务调用。

我觉得 mock 好

我一般用 mock 的情况:

  1. 某个内部实现复杂和关联特别多的依赖,为了加快测试速度
  2. 不是自己写的部分(第三方服务),为了提供稳定的前置条件
  3. 跟别人约定好接口但还没实现的模块,为了开发解耦

其实第一点也是为了提供稳定的前置条件。相比构造 8 个 model 数据才能写点测试,一个 mock 显然更低成本(偷懒)。大量的 mock 看似解耦了,但代价是没法测到多个模块协作的情况。对 Ruby 这种 duck typing 的语言来说更是灾难。有时候模块接口都改了但测试不会报错(因为依赖全是 mock)。最后往往测试全绿但项目都跑不起来。

我觉得好的测试的标准是“改动了软件行为测试就该挂掉” ,速度方面能够接受就行。

darkbaby123 回复

那是你的测试没写全吧。模块接口改了,这个模块本身就应该有测试。

测试分两个部分:被测 + 依赖。此时依赖可以 mock。依赖部分还需要单独作为被测来测试。这样就全面覆盖了。

chenge 回复

依赖改了依赖的测试也改了,mock 没改怎么办?测试代码也是代码,也会有 bug。

Rei 回复

bug 是会有的。但是接口改了,相应的 mock 也要改,这个不是 bug 的问题吧,是工作没到位。

chenge 回复

这就不 DIY 了。

@chenge 这跟测试写没写全不是一个问题。假设 A depends on B。你对 A 做测试,把依赖 B 用 mock 替换掉了。当然 B 也有自己的测试。一个月后你改了 B 的接口。这个时候 A 的测试显示一切正常。但程序已经无法运行了。

Java, C# 等静态语言里有严格的接口依赖,碰到这种情况还会报个编译错误。Ruby 吭都不会吭一声。当然 RSpec 有一些手段可以部分预防这一点,比如 instance_doubleclass_double 只允许你 stub 已经实例/类中已经定义了的方法,不过仅此而已了,检测不到方法参数和返回值,这两个方法提供的保障就是聊胜于无。

接口改了测试当然也要改。问题是到时候还记不记得这件事?比如你是否能够记得每次改 B 的时候都去搜一下它到底被哪些模块依赖了?最后你会发现要保障模块协同工作,还是得靠集成测试。

darkbaby123 回复

是有这个问题。参数个数应该可以检查吧。类型是没办法,估计要等正在搞的类型系统。 我记得有看到过有人做的检查类型的 gem,名字忘了。 Elixir 中可以写 spec,写清楚类型,更容易理解代码。

弥补的办法:

面向对象设计指南一书中,9.6 有给出一个弥补的方案,自己写个接口测试。只是测试名字,好像没有说参数个数。

@chenge 忘了参数个数能不能检查了。RSpec 这个特性我就用了一段时间,后续代码都没用它。除了觉得用处有限之外,也觉得这种方式不太符合 duck typing 的本质 -- 本身定义 mock object 的行为就够了,为什么还要耦合一个类型?

Elixir 也是动态类型语言,typespec 需要依靠 Dialyzer 去做检查。

单元测试用 mock 好

需要 登录 后方可回复, 如果你还没有账号请 注册新账号