← Back to Upcase

Testing Fundamentals - Write a controller spec


(Upcase ) #1

This topic is for the [Write a controller spec][] exercise in the [Testing Fundamentals][] trail. Post any questions, corrections, or pointers you have to share with other Upcase subscribers.
[Write a controller spec]: https://exercises.upcase.com/exercises/testing-fundamentals-write-a-controller-spec
[Testing Fundamentals]: https://thoughtbot.com/upcase/testing-fundamentals


(Joshua Steele) #2

I’m very new to doubles/mocks/stubs, but my understanding is that a mock object is to be a simple, stand-in object that prevents us from have to hit the database, pass validations, and all the other complexities associated with creating a real object. We can then stub the mock with only the behavior we need in the test.

In this exercise, we are encouraged first to write passing tests using real Person objects, and then to improve the tests by using doubles. “Once you get this test working, try using test doubles so that you don’t depend on the actual validations in Person.”

This confused me at first because in this particular case, the functionality we are testing is the creation of an object. When we create a mock, haven’t we already done that?

Finding no suitable answer on my own, I submitted my solution with passing tests, albeit using real objects like so:

post :create, person: { first_name: "Tom" }
expect(response).to redirect_to person_path(assigns(:person))

Once I gained access to viewing other’s solutions, I immediately checked the Upcase featured solution, which was submitted by @croaky. To my surprise, his solution uses the actual objects just like mine does. So far, three people have commented on this, asking why there is not an illustration of mock-style solution, but there has been no answer.

As I clicked through other solutions submitted by various users, I came across only a couple that had a solution using mocks, but those seemed quite verbose and I was not at all confident that this was “the right way” to do it.

In summary, could someone from thoughtbot please address this? Thank you!

@benorenstein @Joshua_Clayton


(Andy Waite) #3

Another point about this exercise - the spec says that when the person is invalid, the controller “redirects to #new”. I think it’s supposed to say that it “renders the ‘new’ template”.


(Joshua Clayton) #4

@joshukraine thanks for pointing this out. Here’s what I came up with: https://exercises.upcase.com/exercises/testing-fundamentals-write-a-controller-spec/solutions/joshuaclayton

This still leverages the Person object, but stubs out Person.new to return the correct instance, then stubs save (avoiding DB calls) and returns a predetermined value, either true or false to test the branch. This guarantees that Person can grow in complexity (validations or otherwise) largely without impacting the controller, which is exactly the isolation we’re looking for in the controller.

Imagine if, every time we modified Person, we had to subsequently ensure that the correct params were being passed in these specs; tests would become a nightmare to maintain! This allows us to sleep easily knowing they’re decoupled at the unit level.


(Ben Orenstein) #5

Thanks Andy. I’ve pushed a fix for this.


(Joshua Steele) #6

Wonderful, thanks for the explanation! Would you mind also commenting on the benefit of using Person.new.tap as opposed to just Person.new? The tap method is new to me, and although I did some reading up on it this morning, I’d love to hear your take as well.


(Joshua Steele) #7

Second question: when I pulled down the git repo for the final step of this trail (testing-fundamentals-write-a-feature-with-tests), I noticed that it also includes a mock solution:

    person = Person.create(first_name: "Bob")
    allow(person).to receive(:save).and_return(true)
    allow(Person).to receive(:new).
      with(first_name: "Bob").
      and_return(person)

As you can see, this version actually calls create on Person. My initial understanding was that this would hit the database immediately. (It does in the console.) But given that we’re also stubbing out save and new, I’m assuming that it doesn’t. But I honestly don’t understand why it doesn’t. Is it because this is all occurring inside an it block?

In summary, could you please comment on Person.new.tap vs Person.new vs Person.create in the context of object doubles? Thanks! :smiley:


(Brian Davis) #8

I came up with the same solution and have the same questions. Rspec mocking/stubbing is still mysterious to me, and the documentation is a bit opaque to me. The syntax changes across various Rspec versions don’t help, either. Is there a tutorial article somewhere (Thoughtbot maybe?) that actually explains the underlying concepts?


(Joshua Steele) #9

Actually, the second testing trail covers this very topic! https://thoughtbot.com/upcase/test-doubles I haven’t been through it yet, but it’s next on my list. Hope that helps!


(Brian Davis) #10

I’ll check it out, thanks.


(Ben Orenstein) #11

Yeah, this is a little confusing.

Heres what’s going on.

# Make a person to use in the test.
person = Person.create(first_name: "Bob")

# If we call save on this person, just return true
allow(person).to receive(:save).and_return(true)

# If someone calls Person.new(first_name: "Bob"), then return the person
# that we created above. DON'T actually return a new person. We're 
# stubbing the result of the call to Person.new.
allow(Person).to receive(:new).
  with(first_name: "Bob").
  and_return(person)

Does that make sense?


(Andy Waite) #12

I also found this difficult when I started writing controller specs.

If you try use a plain double, you quickly run into problems, because Rails queries the object to discover its type, since it needs that for routing. So you end up stubbing a lot of things that you can’t control, which violates the guideline of “Don’t mock what you don’t own”.

The approach that Ben shows above is often called a ‘partial double’.

In the past, rspec-rails had the methods mock_model and stub_model, which were useful in this situation. These have now been moved to an separate gem, rspec-activemodel-mocks. I’m not sure whether to take that as a sign that their use is discouraged by the RSpec team.

Another option is to use build_stubbed from FactoryGirl: Use Factory Girl’s build_stubbed for a Faster Test Suite.


(Joshua Steele) #13

@benorenstein Thanks for the explanation. I think my primary point of confusion revolves around what happens when we call create on an object. My understanding heretofore has been that calling Object.create was equivalent to calling Object.new followed by Object.save. In other cases (like @joshuaclayton’s solution further up in this thread) this is how it’s done.

So I think what’s throwing me is this: how can we can we first call Object.create and then later stub the save method? Wasn’t save already invoked?

Sorry if I’m missing something obvious here. Just trying to make sure I understand this. Perhaps I should go ahead and dive into the test doubles trail, then see what questions I have there. :wink: In any case, thanks for the help!


(Ben Orenstein) #14

Yes, save was already invoked, but that’s okay.

The goal is to avoid actually using save in the controller code. It’s okay that we use it to create some test data beforehand.

So yes, we’ve already invoked save in the setup phase of the test, but that’s not the bit we care about stubbing out. It’s the save call in the controller that we want to avoid.

Making sense yet? If not, let’s jump on a Skype call next week.


(Joshua Steele) #15

@benorenstein Thanks for the info. I still have some questions, but I think at this point it might be better for me to go ahead and work through the Test Doubles trail first. After that, if I still have questions, I may take you up on the Skype idea. Much appreciated!


(Luke Bacon) #17

This is a helpful explanation that, I think, lead me in the write direction https://exercises.upcase.com/exercises/testing-fundamentals-write-a-controller-spec/solutions/equivalentideas .

The mocking stage of this stage was really confusing and I don’t think I understand it still. I think this exercise would benefit from more explanation.

Thanks :slight_smile:


(Ben Orenstein) #18

Agreed. It’s on my list of things to tweak.


(Sze Chan) #19

So I’m also very new to stubs and doubles. Your answer was a great explanation. Let me see if I get this straight: The purpose of doubles and stubs is to create a quick mock Model object so that in the test env you don’t have to make actual DB queries that may slow the test suite down. You are actually only testing the controller for controller-like behavior so it won’t fail if there is a problem with the db or in a model. Am I correct?


(Ben Orenstein) #20

That’s roughly correct, yes.

But my motivation is less about performance or not hitting the database in case it breaks, and more because I want to test the individual pieces of my system in isolation when unit testing.


(Pierre Lebrun) #21

Wouldn’t something like this work?

person = double('person')
person.stub(:first_name) { "Bob" }
person.stub(:save) { true }
allow(Person).to receive(:new) { person }