Growing Web Apps, Guided by Tests

Exploring “Growing Object-Oriented Software, Guided By Tests” in context of web applications development with Rails and rich UI. I keen to build sample app but am I on the right track or missing the point?

Given a part of ordering system where, among other features, user can create an order and make a payment. And given basic rails setup with extra gems for enabling rich UI. I’d like to explore how would tests looks like.

Quick, high level, look into architecture reveals the following key layers:

  • Rich UI (REST client?)
  • CRUD (REST server?)
  • Yet another domain (whatever…)

While not as RESTful as defined by Roy T. Fielding, Rails embraces resource as a central model and makes it universal communication entity which crosses all layers. It forces us to define resources sooner and maintaining low coupling to its structure in every layer including tests.

And as long as it makes sense, I imagine the tests would looks like this:

End-To-End

Covers observable application behavior and interaction with 3rd party services.

1
2
3
4
5
6
7
8
9
10
11
12
13
describe "pay full amount" do
  let(:order_page)   { application.open order_record }
  let(:order_record) { an_order.with(a_line.with_price(100)).create }

  before do
    order_page.make_payment amount: 100
  end

  it 'marks order as paid and sends to the manufacturing queue' do
    expect(order_page).to have_status_paid
    expect(manufacturing_queue).to have_received order_record
  end
end
  • order_record - persisted active record model
  • order_page - manipulates and assert UI (capybara page wrapper)
  • manufacturing_queue - external service

Rich UI

This layer enables interaction with the system in a convenient for the humans way. Basically it consists of a set of UI components, translates user activities into requests to the next layer and interprets responses.

The test for this layer would share matchers with end-to-end scenarios:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# creating order
describe "when create button clicked" do
  let(:new_order_record) { an_order.build }

  before do
    order_page.create_order new_order_record
  end

  it 'makes request' do
    expect(application).to have_received_request :create, new_order_record
  end
end

# and for making payments
describe "when pay button clicked" do
  let(:order_record) { an_order.create }
  let(:new_payment_record) { a_payment.build }

  before do
    order_page.open order_record
    order_page.make_payment new_payment_record
  end

  it 'makes request' do
    expect(application).to have_received_request :create, [order_record, new_payment_record]
  end
end
  • new_order_record - non persisted active record model
  • new_payment_record - non persisted active record model
  • order_record - persisted active record model
  • order_page - manipulates and assert UI (capybara page wrapper)
  • application - rails application runner (or wrapper)
  • have_received_request - custom matcher which decodes request details from the type and active record objects (similar to respond_with from Responders gem)

CRUD

Provides resource manipulation interface.

Basically level two in Richardson’s maturity model and Rails fully covers this layer, i.e. form input to output, from handling request/response, to persisting resources in database of choice.

The test for this layer might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe "POST /order/{id}/payment", type: :request do
  it 'creates resource' do
    make_request :create, [order, new_payment]

    expect(response).to have_http_status(:created)
    expect(response_json).to include(
      id: a_kind_of(Integer)
    )
  end

  it 'makes user request' do
    expect(user_request_listener).to receive(:process_payment).with(
      gateway_id: user.gateway_id,
      amount: order.amount
    )

    make_request :create, [order, new_payment]
  end
end
  • new_* - not persisted active record model
  • order - persisted active record model
  • make_request - helper method, makes HTTP request based on the type and resources specified (similar to respond_with from Responders gem)
  • user_request_listeners - mock object

Yet another domain

This is where “interesting” things happens as a side effect of resource manipulation. For example: payment resource creation would trigger payment processing behavior or “return order” creation would trigger behavior responsible for adjusting stock levels, cost prices, etc.

Test for this layer usually dive into details of business domain, covering rules, validations, exceptions, etc.

Comments