测试 Some tips for writing tests

hegwin · 2016年09月19日 · 最后由 hegwin 回复于 2016年09月19日 · 7498 次阅读

I met something tricky or interesting in writing test scripts and I want to share them.

There are no Chinese input methods in this PC. And I'm too lazy to install one Let me write in English.

Hope they are helpful.

Contents

Test with Floats

Operations on float numbers sometimes return things unexpectedly. For example:

it "sums two float numbers properly" do
  expect(0.7+0.1).to eq(0.8)
end

Looks OK? But actually this is what you'll get:

expected: 0.8
got: 0.7999999999999999

(compared using ==)

The reason is that Ruby follows IEEE, which may cause some imprecisions in float values. And I guess it varies with different systems or even Ruby versions. It's a very common issue within many programming languages.

// Javascript
console.log(0.1+0.7)
// => 0.7999999999999999
# Python 2.7.12
0.1+0.7
=> 0.7999999999999999

But not all the languages.

-- Lua 5.2.4
print(0.1+0.7)
=> 0.8

And fortunately, SQL works correctly.

-- SQL in (PostgreSQL) 9.5.4
select (0.1+0.7) as sum;
 sum 
-----
 0.8
(1 row)

The solution is simple, just use the be_within matcher.

it "sums two float numbers properly" do
  expect(0.7+0.1).to be_within(0.1).of(0.8)
end

Expect to Change

This is my favorite syntax in RSpec. It shortens our codes when we need to assert value changes.

For example in a controller spec:

it 'schedules a new attack task' do
  expect{post :create, attack_params}.to change{AttackTask.count}.by(1)
end

I pick a piece of codes from my game. This is not a very good example, because the conception of ControllerTest in Rails 5 has been changed.

Actually, you can use a single from, to or by, or a combination of them if it is meaningful, or even none of them. I really love this.

expect { i.increase }.to change {i}.by(1)
expect { radix += 2 }.to change {radix}.from(0).to(2)
expect { someone.execute! }.to change { someone.status }.to('dead')
expect { flag.reverse }.to change {flag}

And lastly, a tricky example with what I noticed in the first section:

expect { i += 1.05 }.to change { i }.by( a_value_within(0.01).of(1.05) )

Freeze Time

In some situations, you have to work with time or durations. And it is complicated for us to test them as time will never stop.

You don't have magic in the real world to freeze time, but in the programming world, yes, you do, with the power of timecop.

A very simple example:

it 'should assign ended_at' do
  stubbed_time = Time.new(2016, 12, 25)
  Timecop.freeze(stubbed_time)

  expect{task.finish!}.to change{task.ended_at}.to(stubbed_time)
end

Test after_commit

Ask yourself whether you really have to use after_commit, then whether you need to test it.

If both of the answers are 'yes', then you start to write you specs and you may face this issue that after_commit callbacks are NOT getting called in transactional tests (at least in Rails < 5).

What you need is to try this gem test_after_commit.

Well, the good news is that it will be fixed in Rails 5 https://github.com/rails/rails/pull/18458.

Test ActiveJob

ActiveJob has become a component of Rails since 4.2. And you really don't need to test ActiveJob functionality. But I just want to cover everything as if I am a Vigro programmer. I can give an simple example of ActiveJob specs.

One of my jobs:

class NoticeMailForNewUserJob < ActiveJob::Base
  queue_as :default

  def perform(user)
    AdminMailer.notify_new_player_registration(user).deliver_now
  end
end

And configs:

module MyProject
  class Application < Rails::Application

    #....something

    # ActiveJob
    config.active_job.queue_adapter = :sidekiq
    config.active_job.queue_name_prefix = Rails.env

  end
end

And here are the specs:

require 'rails_helper'

RSpec.describe NoticeMailForNewUserJob, type: :job do
  include ActiveJob::TestHelper

  let(:player) { create :user }
  subject(:job) { described_class.perform_later(player) }

  after do
    clear_enqueued_jobs
    clear_performed_jobs
  end

  it 'queues the job' do
    expect{ job }.to change { ActiveJob::Base.queue_adapter.enqueued_jobs.size }.by(1)
  end

  it 'is in default queue' do
    expect(described_class.new.queue_name).to eq('test_default')
  end

  it 'performs' do
    expect(AdminMailer).to receive_message_chain(:notify_new_player_registration, :deliver_now)
    perform_enqueued_jobs { job }
  end
end

Others

  1. Don't use respond_to to test methods generated by #method_missing. It will not work.

  2. Run $ rake stats, you can see the Code to Test Ratio.

#1 楼 @winnie 多谢支持…换了手机上,终于可以打中文了 😂

😁 其实你还可以打拼音

#3 楼 @kai209209 感觉会引起公愤……

hegwin 我自己的一个帖子不知为何打不开了 提及了此话题。 01月31日 16:22
需要 登录 后方可回复, 如果你还没有账号请 注册新账号