Define class in Rspec
Asked Answered
R

4

6

I want to test an inclusion of a module into a class. I am trying define a new class in RSpec:

describe Statusable do
  let(:test_class) do
    class ModelIncludingStatusable < ActiveRecord::Base
      include Statusable
      statuses published: "опубликовано", draft: "черновик"
    end
  end

  describe '#statuses' do
    it 'sets STATUSES for a model' do
      test_class::STATUSES.should == ["опубликовано", "черновик"]
    end
  end
end

And I get an error:

TypeError:
       [ActiveModel::Validations::InclusionValidator] is not a class/module

This is probably because in Statusable I have:

validates_inclusion_of  :status, :in => statuses,
            :message => "{{value}} должен быть одним из: #{statuses.join ','}"

But if I comment it out, I get:

TypeError:
       ["опубликовано", "черновик"] is not a class/module

Maybe new class definition isn't the best option, what do I do then? And even if it's not, how can I define a class in RSpec? And how do I fix this error?

Roundshouldered answered 22/1, 2014 at 9:39 Comment(0)
W
25

Do not define new constant in tests otherwise it will pollute other tests. Instead, use stub_const.

Also, for this is an unit test of Statusable module. If ActiveRecord model is not a necessity, better not to use it.

You can also use class_eval to avoid not opening this class(no matter fake or not) actually

describe Statusable do
  before do
    stub_const 'Foo', Class.new
    Foo.class_eval{ include Statusable }
    Foo.class_eval{ statuses published: "foo", draft: "bar"}
  end

  context '#statuses' do
    it 'sets STATUSES for a model' do
      FOO::STATUSES.should == ["foo", "bar"]
    end
  end
end

Though I copied your assertion, I would suggest not to insert a constant say STATUS into the class/module(Foo) who includes this module. Instead, a class method would be better

expect(Foo.status).to eq(["foo", "bar"])
Weir answered 22/1, 2014 at 9:53 Comment(4)
@leemour, my pleasure. Two reasons for class method: 1. the constant name should be static. In question, "STATUS" is actually under namespace "Foo" to read as "Statusable::STATUS", but now it is "FOO::STATUS" which intends to mean same thing. 2. The constant value should be static, you can't expect the value of constant to change as per the code.Weir
Good point, but I was defining STATUS dynamically in #statuses, when it's called in class that included Statusable like in my code above. Bad decision?Roundshouldered
@leemour, constant should not be dynamic. You can use class method for dynamic things.Weir
I like the fact that constants are unchangeable. I am defining it dynamically but doing it only once. Is defining constant once and protecting it from overwriting (I am checking for it) a bad practice?Roundshouldered
R
2

It fails because class definition does not return itself.

$ irb
> class Foo; 1 end
 => 1

you need to do like this:

  let(:test_class) do
    class ModelIncludingStatusable < ActiveRecord::Base
      include Statusable
      statuses published: "опубликовано", draft: "черновик"
    end
    ModelIncludingStatusable # return the class
  end

It works but unfortunately, ModelIncludingStatusable will be defined on top-level because of ruby rule.

To capsulize your class, you should do like this:

  class self::ModelIncludingStatusable < ActiveRecord::Base
    include Statusable
    statuses published: "опубликовано", draft: "черновик"
  end
  let(:test_class) do
    self.class::ModelIncludingStatusable # return the class
  end

It works perfectly :)

Removal answered 21/9, 2016 at 14:3 Comment(0)
C
0

When you call let this define a memoized helper method. You can't class definition in method body.

Cuticula answered 22/1, 2014 at 9:53 Comment(0)
G
0

Another option which I frequently use is to put the entire test in it's own module, e.g.

module Mapping::ModelSpec
  module Human
    Person = Struct.new(:name, :age, :posessions)
    Possession = Struct.new(:name, :value)
  end

  RSpec.describe Mapping::Model do
    it 'can map with base class' do
      person = Human::Person.new('Bob Jones', 200, [])

      ...
    end
  end
end

While this is a bit cumbersome, it avoids polluting the global namespace, is only slightly more syntax, and is generally easy to understand. Personally, I'd like a better option.. but I'm not sure what that would be.

Grillparzer answered 1/3, 2016 at 5:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.