LIKE US FOR UPDATES + GET A FREE STICKER PACK!

Operation

Trailblazer Test

Last updated 24 July 2017 trailblazer-test v0.1

Note: This gem is not stable, yet. It will be released in August 2017.

The trailblazer-test gem provides a bunch of assertions, matchers and helpers for writing operation test.

Operation Tests

In Trailblazer, you write operation and integration tests. Operations encapsulate all business logic and are single-entry points to operate your application. There’s no need to test controllers, models, service objects, etc. in isolation - unless you want to do so for a better documentation of your internal APIs.

However, the idea of operation tests is: Run the complete unit with a certain input set, and test the side-effects. This differs to the Rails Way™ testing style, where smaller units of code, such as a specific validation or a callback, are tested in complete isolation. While that might look tempting and clean, it will create a test environment that is not identical to what happens in production.

In production, you will never trigger one specific callback or a particular validation, only. Your application will run all code required to create a Song object, for instance. In Trailblazer, this means running the Song::Create operation, and testing that very operation with all its side-effects.

Luckily, trailblazer-test provides a simple abstraction allowing to run operations and test all side-effects without creating verbose, unmaintable test code.

assert_pass

Use assert_pass to run an operation and assert it was successful, while checking if the attributes of the operation’s model are what you’re expecting.

describe "Create with sane data" do
  let(:params_pass) { { band: "Rancid" } }
  let(:attrs_pass)  { { band: "Rancid", title: "Timebomb" } }

  # just works
  it { assert_pass Create, { title: "Ruby Soho" }, { title: "Ruby Soho" } }
  # trimming works
  it { assert_pass Create, { title: "  Ruby Soho " }, { title: "Ruby Soho" } }
end

Both params_pass and attrs_pass have to be made available via let to provide all default data. They will automatically get merged with the data per test-case. params_pass will be merged with the params passed into the operation call, attrs_pass represent your expected outcome.

The second test case would resolve to this manual test code.

it do
  result = Comment::Create( band: "Rancid", title: "  Ruby Soho " )

  assert result.success?
  assert_equal "Rancid",   result["model"].band
  assert_equal "Timebomb", result["model"].title
end

As you can see, assert_pass drastically reduces the amount of test code.

assert_pass: Block

If you need more specific assertions, use a block with assert_pass.

describe "Create with sane data" do
  let(:params_pass) { { band: "Rancid" } }
  let(:attrs_pass)  { { band: "Rancid", title: "Timebomb" } }

  it do
    assert_pass Create, { title: " Ruby Soho" }, {} do |result|
      assert_equal "Ruby Soho", result["model"].title
    end
  end
end

Here, the only assertion made automatically is whether the operation was run successfully. By yielding the result object in case of success, all other assertions can be made manually.

assert_fail

To test an unsuccessful outcome of an operation, use assert_fail. This is used for testing all kinds of validations. By passing insufficient or wrong data to the operation, it will fail and mark errors on the errors object.

describe "Create with invalid data" do
  let(:params_pass) { { band: "Rancid" } }
  let(:attrs_pass)  { { band: "Rancid", title: "Timebomb" } }

  it { assert_fail Create, { band: "Adolescents" }, [:band] }
end

Here, your params are merged into params_pass and the operation is called. The first assertion is whether result.failure? is true.

After that, the operation’s error object is grabbed. With an array as the third argument to assert_fail this will test if the errors object keys and your expected keys of error messages are equal.

In 2.0 and 2.1, the errors object defaults to result["contract.default"].errors. In TRB 2.2, there will be an operation-wide errors object decoupled from the contracts.

This roughly translates to the following manual test case.

it do
  result = Comment::Create( band: "  Adolescents", title: "Timebomb" )
                                        # Timebomb is a Rancid song.
  assert result.failure?
  assert_equal [:band], result["contract.default"].errors.messages.keys
end

Per default, no assumptions are made on the model.

assert_fail: Block

You can use a block with assert_fail.

describe "Create with invalid data" do
  let(:params_pass) { { band: "Rancid" } }
  let(:attrs_pass)  { { band: "Rancid", title: "Timebomb" } }

  it do
    assert_fail Create, { band: " Adolescents" }, {} do |result|
      assert_equal( {:band=>["must be Rancid"]}, result["contract.default"].errors.messages )
    end
  end
end

Only the failure? outcome is asserted here automatically.

Generic Assertions

As always, the model represents any object with readers, such as a Struct, or an ActiveRecord instance.

Song  = Struct.new(:title, :band)

model = Song.new("Timebomb", "Rancid")
model.title #=> "Timebomb"
model.band  #=> "Rancid"

assert_exposes

Test attributes of an arbitrary object.

Pass a hash of key/value tuples to assert_exposes to test that all attributes of the asserted object match the provided values.

it do
  assert_exposes model, title: "Timebomb", band: "Rancid"
end

Per default, this will read the values via model.{key} from the asserted object (model) and compare it to the expected values.

This is a short-cut for tests such as the following.

assert_equal "Timebomb", model.title
assert_equal "Rancid",   model.band

Note that assert_exposes accepts any object with a reader interface.

assert_exposes: reader

If the asserted object exposes a hash reader interface, use the :reader option.

it do
  assert_exposes model, { title: "Timebomb", band:  "Rancid" }, reader: :[]
end

This will read values with via #[], e.g. model[:title].

If the object has a generic reader, you can pass the name via :reader.

it do
  assert_exposes model, { title: "Timebomb", band:  "Rancid" }, reader: :get
end

Now the value is read via model.get(:title).

assert_exposes: Lambda

You can also pass a lambda to assert_expose in order to compute a dynamic value for the test, or for more complex comparisons.

it do
  assert_exposes model,
    title: "Timebomb",
    band:  ->(actual:, **) { actual.size > 3 }
end

The lambda will receive a hash with the :actual value read from the asserted object. It must return a boolean.

Operation

There are several helpers to deal with operation tests and operations used as factories.

call

Instead of manually invoking an operation, you can use the call helper.

it do
  result = call Song::Update, { title: "Shipwreck" }
end

This will call the operation and passes through all other arguments. It returns the operation’s result object, which allows you to test it.

result.success? #=> true
result["model"] #=> #<Song id=1, ...>

factory

You should always use operations as factories in tests. The factory method calls the operation and raises an error should the operation have failed. If successful, it will do the exact same thing call does.

model = factory( Song::Create, { title: "Shipwreck" } )["model"]

If the factory operation fails, for example due to invalid form input, it raises a `` exception.

factory( Song::Create, { title: "" } )["model"]
#=> Trailblazer::Test::OperationFailedError: factory( Song::Create ) failed.

It is absolutely advisable to use factory in combination with let.

let(:song) { factory( Song::Create, { title: "Timebomb", band: "Rancid" } ) }

Also, you can safely use FactoryGirl’s attributes_for to generate input.