测试 Rails 如何写测试

nainc · 2016年04月03日 · 最后由 adamshen 回复于 2016年04月05日 · 5206 次阅读

本帖只是个人总结的一些实践经验,适用于一些不太会写测试的同学,高手请指教

为什么要写单元测试?

个人总结为以下两点:

  • 现在的测试是为了避免以后的麻烦。当一个功能比较复杂,关系到比较重要的业务或者难以进行黑盒测试的时候,或许在你开发的时候你很有信心,相信这里不会出现 bug。但过了几个月甚至一年,你需要扩展功能或者别人需要接手你的代码时,这就会令人头疼了(尤其是公司没有测试工程师!)。

  • 依赖于第三方服务的功能,比如支付回调,支付平台退款。在本地环境无法以正确的数据调用第三方服务,这时候可以用测试来 mock 返回值,以便进行后续开发。

  • 节约回归测试的时间,每次回归测试都要跑的 case 可以写成单元测试

个人认为,如果开发时间充裕,尽可能的写测试覆盖全部功能。如果时间不够,比如在创业公司,测试最好也能够覆盖 service 和 api

miniTest+fixtures+mocha 的测试框架

  • minTest: rails 的测试框架
  • fixtures: 生成测试数据
  • mocha: 各种 mock 方法的补充

常用的测试方法

详细可见 http://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest/Expectations.html

  • 判断真假 must_equal 这是最常用的一个测试方法了。

    it 'model save must be true' do
      model.save.must_equal true
    end
    it 'service must return true' do
      service.execute.must_equal true
    end
    
  • 判断一个对象的类型 must_be_kind_of

    it 'man must be kind of Human' do
      man = Human.new
      man.must_be_kind_of Human
    end
    
  • 判断一定会抛出异常 must_raise

    it 'model must raise error if name is nil' do
      model.name = nil
      model.save.must_raise ValidationError
    end
    

常用的模拟方法

  • 模拟调用 stubs

    在写测试的过程中,我们常常会希望某个方法返回我们希望的值,不管它如何执行的,这时可以用 stubs。在下面这段代码中,我们需要测试可以成功申请支付宝退款,而实际代码中,申请支付宝退款是一个 http 请求,没有真实的订单号我们一定会申请失败,所以我们模拟一下它的返回。

    class AliPayDrawbackService
    
      def do_drawback(order)
        if apply(order)
          return order.update_attribute(:state, 1)
        else
          return false
        end
      end
    
      def apply(order)
        RestClient.post(url, order.id)
      end
    end
    it 'apply alipay drawback success' do
      #any_instance表示该service的任意实例对象
      AliPayDrawbackService.any_instance.stubs(:apply).returns(true)
    
      order = Order.first
      service = AliPayDrawbackService.new()
      service.do_drawback
      order.state.must_equal 1 #pass
    end
    
  • 期待调用 expects

    某个功能在执行过程中会调用一个其他系统服务,或者某个功能会插入一个任务到异步队列。这是我们需要秉承一个原则:自己的功能自己测。即我不关心其他服务的功能是否正确,我认为只要我成功调用了就是正确的。

    #一个消息队列的pusher
    class Mq::Publisher
      def self.publish(key, msg = {})
        # push 消息体到队列
      end
    end
    class Order
      def submit
        # ...业务逻辑
        Mq::Publisher.puhlish('order.submit.success', {xxx})
      end
    end
    
    it 'send a message if order submit success' do
      #注意,期待调用的方法一定要写在实际调用前
      #这段代码表示期待Mq::Publisher的publish方法在本测试中至少调用一次,并且第一个参数是"order.submit.success",any_parameters表示后面的可以是任意参数
      Mq::Publisher.expects(:publish)
      .with("order.submit.success", any_parameters)
      .at_least_once.returns(true)
    
      order = Order.new
      #xxx
      order.submit
      #如果Mq::Publisher没有调用publish,测试结果会是失败
    end
    
  • mock 对象

    有时某个方法可能会需要一个很复杂的参数,或者某个方法返回的一个结果对象会影响剩余方法的执行,这时我们可以使用 mock

    个人总结了两个方法来 mock

  • 使用 Minitest::Mock

    def execute
      service_a = ServiceA.new
      ret = service_a.do_something
      if ret.success?
        #xxxx
        return true
      else
        #xxxx
        return false
      end
    end
    
    it 'execute must return true if do_something' do
      #创建一个Mock对象,设置它的success?方法返回true
      mock = Minitest::Mock.new
      mock.expect :success?, true
      #设置ServiceA查到任意实例对象调用do_smoething方法返回mock
      ServiceA.any_instance.stubs(:do_smoething).returns(mock)
    
      execute.must_equal true
    end
    
  • 使用 Class.new

    def execute
      service_a = ServiceA.new
      ret = service_a.do_something
      if ret.success?
        #xxxx
        return true
      else
        #xxxx
        return false
      end
    end
    it 'execute must return true if do_something' do
      mock = Class.new{
              define_method(:success?) { true }
            }.new
      ServiceA.any_instance.stubs(:do_smoething).returns(mock)
    
      execute.must_equal true
    end
    

## 先总结这么多

用 rspec,由于项目时间较紧,打算只写 model,controller,server 一类的测试可否?

@nainc stubs 和 mock 那段写的不错,但是感觉还没有抓到本质的点~回头我也看看

#1 楼 @pathbox 时间紧,建议可以多写写 model 和 server 的,毕竟 controller 的逻辑可以通过页面比较直观的测出来

#2 楼 @zamia 其实我也感觉没说清楚,主要是想写 stubs,excepts 和 mock,感觉测试不好写的地方就是这里

FactoryGirl + shoulda + capybara + mocha

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