-
-
Save darrencauthon/5ac518d571c2dc7a4c8e to your computer and use it in GitHub Desktop.
| describe ElephantController do | |
| before { Elephant.delete_all } | |
| let(:controller) { ElephantController.new } | |
| describe "getting the hungry elephants" do | |
| it "should work just fine if I hit the database here, no big deal" do | |
| elephants = [Elephant.create(name: "Dumbo", status: 'hungry'), | |
| Elephant.create(name: "Dumbo's Mom", status: 'hungry'), | |
| Elephant.create(name: "Stinky", status: 'not hungry')] | |
| # let's call the action | |
| controller.hungry_elephants | |
| # I'll be testing that the elephants passed to the view are correct first | |
| # don't worry about HOW i'm doing it, it's not the point | |
| results = controller.instance_eval { @elephants } | |
| results.count.must_equal 2 | |
| results[0].id.must_equal elephants[0].id | |
| results[1].id.must_equal elephants[1].id | |
| end | |
| it "but what if i test through the view?" do | |
| elephants = [Elephant.create(name: "Dumbo", status: 'hungry'), | |
| Elephant.create(name: "Dumbo's Mom", status: 'hungry'), | |
| Elephant.create(name: "Stinky", status: 'not hungry')] | |
| # i don't know the exact code to do this, but you get the point.... | |
| get '/hungry_elephants' | |
| response.body.contains!("Dumbo") # how do I assert that the hungry elephants | |
| # were passed? I don't really, I make a swiping | |
| # pass at the page. | |
| response.body.does_not_contains!("Stinky") # I guess? | |
| end | |
| it "but what if I use the RSpec fancy stuff to simplify the view test" do | |
| elephants = [Elephant.create(name: "Dumbo", status: 'hungry'), | |
| Elephant.create(name: "Dumbo's Mom", status: 'hungry'), | |
| Elephant.create(name: "Stinky", status: 'not hungry')] | |
| # i don't know the exact code to do this, but you get the point.... | |
| get '/hungry_elephants' | |
| # now I'm testing through the view and using ruby magic to get the elephants | |
| # passed to the view. BUUUUUT is this a test for the view, or for what a | |
| # hungry elephant is? | |
| the_controller_elephants.count.must_equal 2 | |
| end | |
| it "now what if the way to determine a hungry elephant changes??" do | |
| # am I supposed to alter all the logic up here, redefining how to create a hungry/not-hungry elephant? | |
| # What I've seen Rails devs do here is use factories, use default databases packed with certain use | |
| # cases, etc etc etc... but see how the details have bled down? | |
| end | |
| it "now what if the elephants size becomes a required field???" do | |
| # code like this breaks, despite how unreleated this test is to that new requirement | |
| elephants = [Elephant.create(name: "Dumbo", status: 'hungry'), | |
| Elephant.create(name: "Dumbo's Mom", status: 'hungry'), | |
| Elephant.create(name: "Stinky", status: 'not hungry')] | |
| # so what devs often do is use factories, packing more abstractions and | |
| # complexity into the test (and usually out-of-sight). | |
| # their tests become so loaded with previous assumptions and changes, it's | |
| # hard to know what is actually what | |
| # this is also where devs throw up their hands and say, "Gosh, this testing stinks, | |
| # and hard, I don't think I need to test controllers anymore." | |
| # this position is backed up by the ease at which a dev can say: | |
| # scope(:hungry) ...... | |
| # which is easy to write in production code, but the tests are hard. So...... we give up | |
| # ... | |
| end | |
| it "but what if I just mocked this complexity away?" do | |
| # look, I can lean on Ruby's flexibility to even forgo the details of | |
| # actual Elephant objects | |
| elephants = Object.new | |
| Elephant.stubs(:hungry).returns elephants | |
| controller.hungry_elephants | |
| # here I've verified those elephants are returned to the view, and my | |
| # test is that much simpler. | |
| # and of course, now I better make sure my next step is to create "hungry" | |
| # on the model! | |
| controller.instance_eval { @elephants }.must_be_same_as elephants | |
| # it's not about the actual database hit, it's about all of the complexity around | |
| # ActiveRecord and what you do about it. | |
| # for simple things, sure, hit the database, who cares. | |
| # but when things get complex, obey SRP and keep pushing the details away. | |
| end | |
| end | |
| end |
Khalid, sorry for the late reply... I don't think notifications are fired for gists. :S
You are right, when unit testing a controller by calling the action directly (i.e. controller.hungry_elephants) and mocking the "params" and associated things, I am not testing the full pipeline. This means that when run as a full website, I might have filters (in MVC, usually filters or attributes) that could change the expected behavior.
In Rails, all of the changes you make to the "pipeline" can be verified. For example, I can see what filters are applied on a controller with something like "ElephantController.before_filters" or something to that effect.
So for an example, let's say that I wanted to verify that the user was logged in before viewing the elephants. Then I'd write a test for "ElephantController.before_filters.include?(:require_login).must_equal true". Then I can write unit tests against "require_login"
The way that many Rails programmers test for the pipeline is to run the tests through the pipeline. But this means that instead of testing an method by calling it, they're testing a method by fiddling with a web request and checking for clues in the web response. This makes testing very difficult, as you can't ship the state of every object through the web request. Plus, you have to do all of the setup before you make the web request. So imagine this....
Given the user is signed in
When the user visits the elephants page
The user should see the elephantsThat's a simple test setup, right? Well, here's what I have to do:
- Create a user
- Go to the login page (with the appropriate route)
- Enter the credentials
- Hit the submit button, verify the login
- Hit the elephants page (with the appropriate route)
- Verify that the page was returned
- Verify that some sort of elephant data is in the page.
See how complex that is?
Compare that to breaking it up into unit tests. One test that the action returns the data. One test that the login is required. One test for each route. Etc. These unit tests are like legos that we stack one-by-one to build anything we want. The integration tests are like pre-packaged toys that, yeah, can be modified... with a lot of glue, paint, etc.
One more thing... RavenDB is fine (though I prefer to pay for support instead of the technology itself), but I don't think this sort of decision should be made based on the technology used.
When I used SQL Server & MVC regularly, I built the same tests that I'm building in Rails today. It's just as trivial to say "elephants.delete_all" and "Elephants.deleteAll()" (with SimpleData) or "Elephants.Connection.executeSql("Delete * FROM ELEPHANTS;") (sort of like that with EF). It's trivial to create a test database in SQL Server, and SQL Server is pretty darn fast. Testing like this, I was still running thousands of tests in a minute or two.
The Slow-Test/Fast-Test debate going around in Rails right now is not really an artifact of running the tests through the database... it's all setup and testing through web pages. No matter what database I'm using, my tests are going to be slow if they require me to run 20 steps, half of which are full page requests and responses.
So no matter how great RavenDB might be (I'd say "eh"), your tests with it are going to be just as slow as The Rails Way if you build a full suite that way.
This is a nice example, but if I had to make a counter argument it would be what about specific pipelines in your application that happen outside of the scope of a stub and still need to run. I'm not sure about Ruby on Rails, but mocking in ASP.NET MVC and using Controllers in tests does not go through the pipeline. That means attributes are not executed and any behavior exhibited in those attributes does not affect your test. This can be desired or not.
All that said, I think there is a place for both kinds of tests. I use RavenDB and it is much nicer to just use the technology directly in memory and if SQL Server provided an in-memory version of itself I would totally use it rather than mocking situations. That's just me though :)