Filling In the First Spec

You don’t have any classes or methods yet for indicating whether recording an expense succeeded or failed. Let’s take a moment to sketch out what that code would look like.

Connecting to Storage

First, you’ll need some kind of storage engine that keeps the expense history; call it a Ledger. The simplest approach would be for the API class to create a Ledger instance directly:

 class​ API < Sinatra::Base
 def​ initialize
  @ledger = Ledger.new
 super​() ​# rest of initialization from Sinatra
 end
 end
 
 # Later, callers do this:
 app = API.new

But this style limits the code’s flexibility and testability, as it doesn’t allow you to use a substitute ledger for custom behavior. Instead, consider structuring the code so that callers pass an object filling the Ledger role into the API initializer:

 class​ API < Sinatra::Base
 def​ initialize(ledger:)
  @ledger = ledger
 super​()
 end
 end
 
 # Later, callers do this:
 app = API.new(​ledger: ​Ledger.new)

This technique—passing in collaborating objects instead of hard-coding them—is known as dependency injection (DI for short). This phrase conjures up nightmares of verbose Java frameworks and incomprehensible XML files for some folks. But as the previous snippet shows, DI in Ruby is as simple as passing an argument to a method. And with it, you get several advantages:

  • Explicit dependencies: they’re documented right there in the signature of initialize

  • Code that’s easier to reason about (no global state)

  • Libraries that are easier to drop into another project

  • More testable code

One disadvantage of the way we’ve sketched the code here is that callers always have to pass in an object to record expenses. We’d like callers to be able just to say API.new in the common case. Fortunately, we can have our cake and eat it too. All we have to do is give the parameter a default value. Add the following code to app/api.rb, just inside your API class:

 def​ initialize(​ledger: ​Ledger.new)
  @ledger = ledger
 super​()
 end

When the HTTP POST request arrives, the API class will tell the Ledger to record() the expense. The return value of record() should indicate status and error information:

 # Pseudocode for what happens inside the API class:
 #
 result = @ledger.record({ ​'some'​ => ​'data'​ })
 result.success? ​# => a Boolean
 result.expense_id ​# => a number
 result.error_message ​# => a string or nil

It’s not time to write the Ledger class yet. You’re not testing its behavior here; you’re testing the API class. Instead, you’ll need something to stand in for a Ledger instance. Specifically, you’ll need a test double.

Test Doubles: Mocks, Stubs, and Others

A test double is an object that stands in for another one during a test. Testers tend to refer to them as mocks, stubs, fakes, or spies, depending on how they are used. RSpec supports all of these uses under the umbrella term of doubles. We’ll explain the differences in Chapter 13, Understanding Test Doubles, or you can read Martin Fowler’s article “Test Doubles” for a quick summary.[38]

To create a stand-in for an instance of a particular class, you’ll use RSpec’s instance_double method, and pass it the name of the class you’re imitating (this class need not actually exist yet). Since you’ll need to access this phony Ledger instance from all of your specs, you’ll define it using a let construct, just like you did with the sandwich object in the first chapter.

There are a couple of other additions to make to your spec, which we’ve highlighted for you. Alter api_spec.rb to the following structure:

 require_relative ​'../../../app/api'
»require ​'rack/test'
 
 module​ ExpenseTracker
» RecordResult = Struct.new(​:success?​, ​:expense_id​, ​:error_message​)
 
  RSpec.describe API ​do
»include​ Rack::Test::Methods
»
»def​ app
» API.new(​ledger: ​ledger)
»end
»
»let​(​:ledger​) { instance_double(​'ExpenseTracker::Ledger'​) }
 
 describe​ ​'POST /expenses'​ ​do
 context​ ​'when the expense is successfully recorded'​ ​do
 # ... specs go here ...
 end
 
 context​ ​'when the expense fails validation'​ ​do
 # ... specs go here ...
 end
 end
 end
 end

As with the acceptance specs, you’ll be using Rack::Test to route HTTP requests to the API class. The other big change is packaging up the status information in a simple RecordResult class. Eventually, we’ll move this definition into the application code. But that can wait until we’ve defined Ledger.

Use Value Objects at Layer Boundaries

images/aside-icons/info.png

The seam between layers is where integration bugs hide. Using a simple value object like a RecordResult or Struct between layers makes it easier to isolate code and trust your tests. See Gary Bernhardt’s excellent “Boundaries” talk for more details.[39]

Now, you’re ready to fill in the body of the first example. Inside the first context, find the empty ’returns the expense id’ spec that you sketched out earlier. Change it to the following code:

 it​ ​'returns the expense id'​ ​do
  expense = { ​'some'​ => ​'data'​ }
 
»allow​(ledger).to receive(​:record​)
» .with(expense)
» .and_return(RecordResult.new(​true​, 417, ​nil​))
 
  post ​'/expenses'​, JSON.generate(expense)
 
  parsed = JSON.parse(last_response.body)
 expect​(parsed).to ​include​(​'expense_id'​ => 417)
 end

On the highlighted lines, we’re calling the allow method from rspec-mocks. This method configures the test double’s behavior: when the caller (the API class) invokes record, the double will return a new RecordResult instance indicating a successful posting.

Another thing to note: The expense hash we’re passing doesn’t look anything like valid data. In the live app, the incoming data will look more like { ’payee’ => ..., ’amount’ => ..., ’date’ => ... }. This is okay; the whole point of the Ledger test double is that it will return a canned success or failure response, no matter the input.

Having data that looks obviously fake can actually be a big help. You’ll never mistake it for the real thing in your test output and waste time wondering, “How did this expense result in that report?”

Run your specs; they should fail, because the API behavior is not implemented yet:

 $ ​​bundle exec rspec spec/unit/app/api_spec.rb
  truncated
 
 Failures:
 
  1) ExpenseTracker::API POST /expenses when the expense is successfully
  recorded returns the expense id
  Failure/Error: expect(parsed).to include(expense_id’ => 417)
 
  expected {"expense_id" => 42} to include {"expense_id" => 417}
  Diff:
  @@ -1,2 +1,2 @@
  -"expense_id" => 417,
  +"expense_id" => 42,
 
  # ./spec/unit/app/api_spec.rb:30:in ‘block (4 levels) in
  <module:ExpenseTracker>’
 
 Finished in 0.03784 seconds (files took 0.16365 seconds to load)
 4 examples, 1 failure, 3 pending
 
 Failed examples:
 
 rspec ./spec/unit/app/api_spec.rb:20 # ExpenseTracker::API POST /expenses
 when the expense is successfully recorded returns the expense id
 
 Randomized with seed 56373

Once you have a failing spec, it’s time to fill in the implementation.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset