In this chapter, you implemented the final piece of the app: the storage layer that writes to a real database. You wrote integration specs to test this layer. Because these specs made changes to the same global state (the database), they could interfere with one another. You used RSpec’s random test ordering and --bisect ability to unearth these dependencies. You fixed them with a clean around hook, and kept noisy database transaction code out of your integration specs.
Once your integration specs were passing, you found that your end-to-end specs were also green. You’ve completed the first major piece of a real app.
During this project, you’ve come to know RSpec quite well. You’ve learned how to test individual methods using expectations and test doubles. You’ve created example groups and shared test data to keep your specs laser-focused on what the code is supposed to be doing. You’ve used RSpec’s spec runner to unearth and fix problems in your test code.
Over the next few parts of the book, we’re going to take a deep dive into each of these aspects of RSpec. But first, try your hand at an exercise or two.
In these exercises, you’re going to flesh out your specs so that they check your code’s behavior more closely. Then, you’ll use outside-in development to add a new feature (specifically, support for a new data format) to your expense tracker.
So far, the Ledger class only validates one property of incoming expenses: that they have a payee. What are some other things the record method should check before saving an expense? Write integration specs for these checks, and implement the behavior.
The current version of the app assumes all input is JSON. You could post XML, and the code would still try to parse it as JSON:
| $ curl --data 'some xml here' \ |
| --header "Content-Type: text/xml" \ |
| http://localhost:9292/expenses |
For this exercise, you’ll add the ability for the expense tracker to read and write XML in addition to JSON. First, you’d need to decide on an XML format. If you use something like the Ox library, you’ll get a format, reader, and writer for free.[46]
The next thing to consider is how callers will select the data format. Luckily, HTTP provides a means to do this with headers, which many HTTP clients and servers already understand:[47]
The expenses POST endpoint would look at the HTTP Content-Type header for the standard MIME types for these formats, application/json or text/xml, to know how to parse the incoming data.[48]
The expenses/:date GET endpoint would read the Accept header, similar to Content-Type, and decide how to format the outbound data.
Along the way, you’ll end up deciding what to do when the caller asks for an unsupported format, or when the incoming data doesn’t match the format advertised. The unit specs for your routing layer are a good place to check for all these edge cases.
Rack::Test and Sinatra provide helper methods for you to read and write the various HTTP headers:
In Rack::Test, call header to set up your request headers before calling get or post.[49]
In Sinatra, call request.accept or request.media_type to read the particular headers needed for this exercise.[50]
Also in Sinatra, write your response headers into the headers hash before returning from your routing code.[51]
Jump down into your routing layer, then spec out and implement the logic to read and write XML—including any edge cases for invalid input you thought of while planning this feature.
You’re now armed with the tools to build a major app feature from acceptance spec all the way down to implementation. We could walk you through one more major feature (or you could build one on your own). Doing so would be good practice, but wouldn’t show off all the ways RSpec can help you test more effectively.
Instead, we’ll take a close look at each aspect of how you’ll use RSpec in your daily life. We’ll start with core RSpec components like the command-line interface.
http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html
http://sequel.jeremyevans.net/rdoc/files/doc/testing_rdoc.html
http://www.rubydoc.info/gems/rack-test/0.6.3/Rack/Test/Session#get-instance_method
http://www.sinatrarb.com/intro.html#Accessing%20the%20Request%20Object
http://www.sinatrarb.com/intro.html#Setting%20Body,%20Status%20Code%20and%20Headers