Outside-In Development
Outside-in development is a methodology that focuses on customer needs by solving for core business flows in the fashion expected for use. In the case of a web application, for example, development would first begin with solving for the set of interactions with a webpage and work backwards into domain models and other abstractions.
Within the realm of testing, we follow this pattern by:
- Writing an end-to-end test asserting behavior through a web browser
- Writing implementation to get that test to pass
- Along the way, writing integration or unit tests to replicate the failure at the level above
- Once the integration or unit test passes, re-run the end-to-end test
- Continue this process until the end-to-end test passes
Example
Imagine a scenario where we display a birthday message for a member of the site.
We might first write an end-to-end test as:
require "rails_helper"
RSpec.describe "Wishing the user a happy birthday" do
it "with their age" do
travel_to Date.new(2023, 1, 1) do
member = create(:member, born_on: Date.new(1983, 1, 1))
sign_in_as(member)
expect(page).to have_notification("Happy 40th birthday!")
end
end
end
With an empty ActiveRecord model:
class Member < ApplicationRecord
end
And the view:
<% if current_member.birthday? %>
<p class="notice">Happy <%= current_member.age.ordinalize %> birthday!</p>
<% end %>
We'd see NoMethodErrors for both Member#birthday? and Member#age. Until
this point, we've been working in the "outside" portion of outside-in
development. Because we can now write tests at the unit level, we can implement
a test (and implementation) for Member#birthday?:
require "rails_helper"
RSpec.describe Member do
describe "#birthday?" do
it "returns true if the month and day are the same as today" do
travel_to Date.new(2023, 1, 1) do
member = Member.new(born_on: Date.new(2020, 1, 1))
expect(member.birthday?).to be_true
end
end
it "returns false if the month and day are different than today" do
travel_to Date.new(2023, 1, 1) do
member = Member.new(born_on: Date.new(2020, 1, 2))
expect(member.birthday?).to be_false
end
end
end
end
The implementation:
class Member < ApplicationRecord
def birthday?
Date.current.month == born_on.month && Date.current.day == born_on.day
end
end
Next, we'd re-run the end-to-end test and see a NoMethodError for
Member#age, write the unit test and implementation (the "in" of outside-in
development), see the unit test pass, and finally see the end-to-end test pass.
While this process might sound tedious, it helps drive implementation and correct behavior for only what's necessary for the code to work as expected.