Link to wealthfront.com

Fork me on GitHub

Friday, December 17, 2010

Factory > Fixtures

In general, using fixtures in testing is a big no no. Not only does it become a maintenance nightmare, it can also significantly slow down your test suite. Instead, the general practice should be to build out factories.

I personally like using machinist for its terse syntax, but because our front end stack relies on RPC calls for backend data, I thought it would be easier and more fun to build our own custom mocking factory. We use forgery in conjunction with our factory to generate random json data, greatly simplifying the amount of testing setup. This allows the developer to focus soley on satisfying the conditions necessary for a specific test. The factory will randomly generate all other unspecified fields to maintain a valid model object.

For those that are unfamiliar with how a factory can help, here's a quick walkthrough with how BDD testing would've been done. Let's say we need to create a page that shows a customer's valid accounts. We first begin by writing our controller spec:
it "should be able to login and view active accounts" do
  user = User.new("id" => 120)
  login_as(user)
  web_service.expects(:get_accounts_for_user).with(user).returns([Account.new("id" => 4, "userId" => 120, "state" => "PENDING"), 
                                                                 Account.new("id" => 3, "userId" => 120, "state" => "FUNDED"), 
                                                                 Account.new("id" => 5, "userId" => 120, "state" => "OPENED")])
  get :view_active_accounts 
  assigns[:active_accounts].size.should == 2
end
And then we write our controller code
def active_accounts
    @active_accounts = web_service.get_accounts_for_user(@current_user)
  end

Because the total number of QA engineers at our company is a big fat 0, we use RSpec's handy integrate_views option to test all view renders instead of mocking out the render. This helps us sleep a healthy 8 hours on school nights. Lets continue on with writing our view code...

<div>Welcome <%= @current_user.full_name.capitalize %></div>
<div>Your total account value is: <%= sum_market_value(@active_accounts) %></div>
<table>
<tr><br/><th>Account ID</th><br/><th>Account State</th><br/><th>Market Value</th><br/></tr><br/>
<% @active_accounts.each do |account| %>
<tr><br/><td><%= mask_account_number(account.account_id) %></td><br/><td><%= account.state %></td><br/><td><%= number_to_curenty(account.market_value) %></td><br/></tr>
<% end %>
</table><br/>

Now re-running our specs again will probably blow up with failures since we didn't provide some of the key fields. Lets go ahead and add in those fields...

it "should be able to login and view active accounts" do
  user = User.new("id" => 120, "lastName" => "Smith", "firstName" => "John")
  login_as(user)
  web_service.expects(:get_account_for_user).with(user).returns([Account.new("id" => 4, "userId" => 120, "state" => "PENDING", "marketValue" => 100000, "accountId" => "#462462252"), 
                                                                 Account.new("id" => 3, "userId" => 120, "state" => "FUNDED", "marketValue" => 2522, "accountId" => "#3252522"), 
                                                                 Account.new("id" => 5, "userId" => 120, "state" => "OPENED", "marketValue" => 6821, "accountId" => "#9258235")])
  get :view_active_accounts 
  assigns[:active_accounts].size.should == 2
end

The specs will work now, but that doesn't prevent another coworker from exposing another field on this page in the future. He/she will then have to look into this test and make changes in order to make the spec pass again. As a developer, this gets annoying because I'm not specifically testing this particular view--in fact, I may have to test this exact same page again with different conditions. All that I'm really concerned about for this test case is that there are 2 active accounts for my user. With our handy factory, we can now specify the code as follows:

it "should be able to login and view active accounts" do
  user = User.new_mock
  login_as(user)
  web_service.expects(:get_account_for_user).with(user).returns([Account.new_mock("userId" => user.id, "state" => "PENDING"), 
                                                                 Account.new_mock("userId" => user.id, "state" => "FUNDED"), 
                                                                 Account.new_mock("userId" => user.id, "state" => "OPENED")])
  get :view_active_accounts 
  assigns[:active_accounts].size.should == 2
end


Since we don't care about any of the fields for either the User or the Account objects, they technically can be made up of any valid data. All that we are testing for is that the active accounts should be selected. This allows engineers to only have to be explicit for what they are actually testing for.

For any of you that are interested in how we created our own factory, here is the code with some sample blueprints of the User and Account models.

class Object
  def self.blueprint
    hash_result = yield
    self.to_s.constantize.module_eval("def self.blueprint_defined() #{hash_result.inspect} end")
  end

  def self.new_mock(json_hash = {})
    self.new(self.mock_json(json_hash))
  end

  def self.mock_json(json_hash = {})``
    self.blueprint_defined.merge(json_hash)
  end
end

User.blueprint do
{
  "id" => Forgery::Basic.id,
  "lastName" => Forgery::User.last_name,
  "firstName" => Forgery::User.first_name,
  "address" => Address.mock_json
}
end

Account.blueprint do
{
  "id" => Forgery::Basic.id,
  "userId" => Forgery::Basic.id,
  "state" => "FUNDED",
  "marketValue" => Forgery::Portfolio.market_value,
  "accountId" => Forgery::Portfolio.account_id
}


Notice that we can also nest json models within one another. The ultimate goal for creating tools like our blueprint factory is for both maintainability and ease of use. Writing specs may not always be fun, but they are a necessity. And anything that can help us stay efficient is a big win.