At the Forge - RSpec for Controllers
RSpec is a popular testing framework for Ruby programmers that works on the principle of behavior-driven development (BDD). BDD distinguishes itself from test-driven development (TDD) in that it looks at programs from the outside, rather than from the inside, considering code as a user or observer, as opposed to an implementer. In the BDD world, you don't implement tests, but rather specifications; if the specification passes, the code is doing what it is supposed to do.
As with many things in the Ruby arena, RSpec has become particularly popular among users of the Rails framework for Web development. Last month, I discussed RSpec in the context of testing Rails models (that is, classes that connect to the relational database). This month, I look at the slightly more complicated case of controller testing. Controller testing is more complicated because it requires that you consider a few more cases, or at least different cases. Now you have to consider inputs from the outside world, in the form of HTTP requests. It also introduces the need for mocks and stubs, objects you can use to test your controllers without having to create real objects (and the database that sits behind them).
This month, I examine some of the ways the RSpec testing framework allows you to test controllers in your Ruby on Rails applications. Along the way, I consider what it means to test controllers and how much you might want to test them. Finally, I take a quick look at the world of mocks and stubs, and show how they can help improve your testing.
Last month, I started building a simple appointment calendar as an example. As it happens, I implemented only a small part of that appointment calendar, creating a single person model, which you can use to represent the people with whom you will meet. Now, let's create appointments as well:
./script/generate rspec_model appointment starting_at:timestamp \ ending_at:timestamp person_id:integer location:text notes:text
As you might expect, you will enhance your model files by linking them together, indicating that each person has_many appointments, but that each appointment belongs_to one person. That'll allow you to use Ruby's object-oriented syntax to retrieve person.appointments, or appointment.person.
Now that you have two models in place, you should do something with them. One obvious thing to do is list today's appointments. In BDD fashion, let's write a spec that describes what the system should do; you actually will implement the code afterward.
The spec will describe how you want to be able to see a list of appointments. Let's assume that the specs for the models (people and appointments) are in place, and that you now can concentrate on your controllers. Basically, you want an appointment controller whose index action shows all current appointments. You can do that by generating such a controller:
./script/generate rspec_controller appointment index new create show
Create a controller named appointment, along with a few actions named similarly to a purely RESTful controller (which this is not). Now, open up spec/controllers/appointment_controller_spec.rb, which is the location of the spec file for this controller, and you will see a number of simple specs, one for each of the methods you've defined. As I explained last month, RSpec's power is its readability, with “describe” blocks that indicate an overall context, “it” blocks that describe specifications, and then individual assertions, which are written as “something SHOULD be-something”. The initial, automatically generated spec for the index action, thus, looks like this:
describe "GET 'index'" do it "should be successful" do get 'index' response.should be_success end end
The response object is given automatically in controller specs, and it allows you to do such things as check for success. The thing is, you also want this index action to retrieve (and display) all the current appointments in your database. How can you test for that?
One way is to load your database with a bunch of fake data, or “fixtures”, and actually retrieve the data from the database. But hey, you're trying to test the controller here, not the database—so going to the database is going to be massive overkill.
What you can do instead is tell Ruby you expect the controller to request a bunch of appointment objects. Indeed, it should request all the appointments in the database, as per your specification. So long as it does that, you can rest assured that the action's specification has been met.
You can do this by switching your normal Appointment object with a mock, sometimes called a test double object. This mock object allows you to check that the right things are happening, while staying within your program. For example, if you want to make sure that Appointment.find(:all) is being invoked, modify your spec to read as follows:
describe "GET 'index'" do it "should be successful" do appointments = [mock(:appointment), mock(:appointment)] Appointment.should_receive(:find).and_return(appointments) get 'index' response.should be_success end end
Here, two lines are added before the invocation of “get 'index'”. In the first line, you create an array of two mock objects, each of which will claim (if asked) that it is an instance of Appointment. It isn't a real appointment object, of course, but rather a thin layer meant just for testing. You will create two such objects, so you can pretend that there are multiple appointments in your database.
The next line is even more interesting. It says that Appointment (the class) should expect to receive the find method at some point. Notice that the placement here is important; if you were to put this mocking line after the invocation of GET, it would be too late. Instead, set up the mock such that the GET method can do things appropriately. If the mock doesn't receive an invocation of “index”, RSpec exits with a fatal error. Indeed, using BDD methods, that's exactly what I can expect to see after I run RSpec:
Spec::Mocks::MockExpectationError in 'AppointmentController ↪GET 'index' should be successful' <Appointment(id: integer, starting_at: datetime, ending_at: ↪datetime, person_id: integer, location: text, notes: ↪text, created_at: datetime, updated_at: datetime) ↪(class)> expected :find with (any args) once, ↪but received it 0 times
In other words, the example above says you want Appointment to have its “find” method called, but that never happened. Thus, add that invocation of find to the index action:
def index Appointment.find(:all) end
Now the spec passes (thanks to the mock object), and you have functionality. What could be better? Well, perhaps you want to test the output you see in the view that displays that object. I'm not going to go into it here, but RSpec allows you to test views as well, using a similar mechanism that looks at the resulting HTML output.
Indeed, I have begun to scratch only the surface of what is possible with RSpec's mocking mechanism. You can stub out specific object methods, allowing you to use models without their overhead or dependencies. For example, you could replace calls to “find” with a mock object that you return, and ignore any calls to “save”—thus, allowing you to work with real models, but faster and more reliably.
You also can imagine how you could test your ability to retrieve models that are associated with one another using mocks. For example, the “index” method probably would be useless if it displayed only appointments. You probably would want to show the person with whom the appointment was scheduled. That requires traversing a foreign key association, which you easily can take care of with stub objects that you then reference from within your mock.
Now, you might be wondering if all this would be possible with either fixtures or factories. The answer is yes, and different developers have used fixtures and factories successfully over the years. I generally find fixtures to be the most natural of the bunch to understand and to use, but the fact that they go through the database and require that I set up and coordinate each of the individual objects begins to take its toll as a project gets larger. I also enjoy using factories and have been experimenting (as I mentioned a few months back) with different factory classes.
But, the more I'm exposed to mocking, the more I wonder if the entire factory class is necessary, or if I simply can use mocks and stubs to pinpoint and use the functionality that interests me. I'm sure other developers are thinking about these considerations as well, and I hope the plethora of options available to Ruby developers will improve and encourage the culture of testing that is already so strong in the Ruby community.
Pick up any e-commerce web or mobile app today, and you’ll be holding a mashup of interconnected applications and services from a variety of different providers. For instance, when you connect to Amazon’s e-commerce app, cookies, tags and pixels that are monitored by solutions like Exact Target, BazaarVoice, Bing, Shopzilla, Liveramp and Google Tag Manager track every action you take. You’re presented with special offers and coupons based on your viewing and buying patterns. If you find something you want for your birthday, a third party manages your wish list, which you can share through multiple social- media outlets or email to a friend. When you select something to buy, you find yourself presented with similar items as kind suggestions. And when you finally check out, you’re offered the ability to pay with promo codes, gifts cards, PayPal or a variety of credit cards.Get the Guide
|smbclient Security for Windows Printing and File Transfer||Mar 28, 2017|
|How to Calculate Flash Storage TCO||Mar 27, 2017|
|Non-Linux FOSS: Don't Drink the Apple Kool-Aid; Brew Your Own!||Mar 27, 2017|
|Three EU Industries That Need HPC Now||Mar 25, 2017|
|HOSTING Monitoring Insights||Mar 24, 2017|
|FinTech and SAP HANA||Mar 24, 2017|
- Non-Linux FOSS: Don't Drink the Apple Kool-Aid; Brew Your Own!
- How to Calculate Flash Storage TCO
- smbclient Security for Windows Printing and File Transfer
- Returning Values from Bash Functions
- Two Ways GDPR Will Change Your Data Storage Solution
- Preseeding Full Disk Encryption
- Hodge Podge
- GRUB Boot from ISO
- Three EU Industries That Need HPC Now
- Tech Tip: Really Simple HTTP Server with Python