zedryas
zedryas

Reputation: 974

How to rspec threaded code?



Starting using rspec I have difficulties trying to test threaded code. Here is a simplicfication of a code founded, and I made it cause i need a Queue with Timeout capabilities

require "thread"

class TimeoutQueue
   def initialize
      @lock = Mutex.new
      @items = []
      @new_item = ConditionVariable.new
   end

   def push(obj)
      @lock.synchronize do
         @items.push(obj)
         @new_item.signal
      end
   end

   def pop(timeout = :never)
      timeout += Time.now unless timeout == :never
      @lock.synchronize do
         loop do
            time_left = timeout == :never ? nil : timeout - Time.now
            if @items.empty? and time_left.to_f >= 0
               @new_item.wait(@lock, time_left)
            end
            return @items.shift unless @items.empty?
            next if timeout == :never or timeout > Time.now
            return nil
         end
      end
   end

   alias_method :<<, :push
end

But I can't find a way to test it using rspec. Is there any effective documentation on testing threaded code? Any gem that can helps me? I'm a bit blocked, thanks in advance

Upvotes: 2

Views: 5045

Answers (1)

Uri Agassi
Uri Agassi

Reputation: 37409

When unit-testing we don't want any non-deterministic behavior to affect our tests, so when testing threading we should not run anything in parallel.

Instead, we should isolate our code, and simulate the cases we want to test, by stubbing @lock, @new_item, and perhaps even Time.now (to be more readable I've taken the liberty to imagine you also have attr_reader :lock, :new_item):

it 'should signal after push' do
  allow(subject.lock).to receive(:synchronize).and_yield

  expect(subject.new_item).to receive(:signal)

  subject.push('object')

  expect(subject.items).to include('object')
end

it 'should time out if taken to long to enter synchronize loop' do
  @now = Time.now
  allow(Time).to receive(:now).and_return(@now, @now + 10.seconds)
  allow(subject.items).to receive(:empty?).and_return true
  allow(subject.lock).to receive(:synchronize).and_yield

  expect(subject.new_item).to_not receive(:wait)

  expect(subject.pop(5.seconds)).to be_nil
end

etc...

Upvotes: 3

Related Questions