Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spec RSpec model attribute setter

I'm using Sinatra (1.2) and RSpec (2.5) and would like to create a new object with an attribute TDD style. This is how the end result should look like:

class User
  def initialize(name)
    @name = name
  end
end

I know I have to write the example before the implementation but I'm trying to explain my question here. :) Here is the not working spec I have so far:

describe User
  it "creates a new user object" do
    name = mock("A name")
    user = mock(User) # shouldn't do this, see the reply's
    user.should_receive(:name=).with(name)
    User.new(name)
  end
end

When I run RSpec I get the "expected: 1 time, received 0 times" error. Any idea how I can explain RSpec I would like to assign the name attribute?

Note: I'm not using Rails, not using ActiveRecord or anything, just Ruby.

like image 757
Cimm Avatar asked Apr 09 '11 07:04

Cimm


3 Answers

First of all, let me explain why the spec you have written doesn't work:

You set an expectation that the mock object returned by mock(User) should receive name=. There are two problems with this. First of all the mock will receive nothing, because it is never called. mock(User) returns a mock object, and it cannot be used to set expectations for what the User class object will receive (to do that simply do User.should_receive(...)). Secondly, even if you had set the expectation on the User class object, that object will never receive name=. There are two reasons for this, too: firstly because name= (had it existed) would be an instance method, and as such not called on the class object, and secondly, you declare no name= instance method. What your code does is that it sets an instance variable.

Now, how should you write a test for this? You shouldn't. Tests are to define and assert the behaviour, not the implementation. Setting an instance variable is pure implementation. There is in your example code no way to get the value of the @name instance variable from outside of the class, therefore there is no reason to write a test for it.

Obviously your code is just an example, anything useful would do something with the @name variable, and that is what you should test. Start by writing a test for what a User object will be used for, then write all the implementation needed to fulfill that test (but no more). Write a test that reflects how the object will be used in actual production code.

like image 140
Theo Avatar answered Nov 15 '22 07:11

Theo


I'd really recommend you don't approach this using mocks. It's not what they're for. In fact, specifying getters/setters like this is not really what TDD is for. The idea is to let a requirement drive the setters/getters into existence. For example, there might be a requirement that the user's name appear in a welcome message when he/she logs in. Then you might do something like this:

describe 'login process' do
  it "displays user's name after successful login" do
    user = User.new("Cimm", "[email protected]", "secret")
    post "/login", :email => "[email protected]", :password => "secret"
    last_response.body.should =~ /Welcome Cimm/m
  end
end

This specifies behavior, and forces you to implement a means of setting the name attribute (via the constructor, in this case) and a means of accessing it. No need to specify the constructor directly.

like image 5
David Chelimsky Avatar answered Nov 15 '22 05:11

David Chelimsky


Do you really want to mock the very object you are developing?

require 'rspec'

class User
  attr_accessor :name

  def initialize(name)
    @name = name
  end 
end

describe User do
  subject {User.new "other name"}

  it "creates a new user object" do
    subject.should respond_to :name=
  end 
end
like image 1
Wes Avatar answered Nov 15 '22 07:11

Wes