Fixrb

Fix guide, version 1

In a Rails application

What a Rails application needs for Fix to test its models and its controllers, and where the files go.

This page takes a Rails application: fixrb/microblog, a small microblog whose only test framework is Fix. Users pick a handle, post short texts, follow one another and read a timeline. Its domain, a handle and the body of a post, is plain Ruby in lib, tested as In a project showed. This page is about the rest: the models, which need a database, and the controllers, which need the application.

The files shown here are those of the application, as they are in its repository, where they run with it. The repository of this site runs its examples before a page is published; it has no Rails, so these are the ones it shows without running.

The Gemfile

Fix and its command go in the test group, with require: false: gem "fix", require: false and gem "fix-command", require: false. Rails requires the gems of the group of its environment when the application boots, and the files of fix require Fix themselves: the application never loads it.

Layout

The specification of a model or of a controller names nothing of Rails: eq 302, eq ["Body can't be blank"]. So this application keeps its specifications apart from its tests, as Patterns does for a shared one. spec holds the specifications, one Fix :Name do … end document per file, each requiring Fix and nothing else: no Rails, no database, not even the class it specifies. fix holds the tests, the files the fix command runs: each loads what its subject needs, requires its specification and ends with exit(Fix[:Name].test { … }), the block building the subject. A result then points at the line of the specification.

The let of a specification is the interface between the two files: the specification declares a parameter of the subject, with its default and the with that vary it, and the block of the test reads it by name. Writing both halves in one file, as the pages before did, works just as well. An application that also uses RSpec keeps spec for it, and puts the documents in fix/specs, say.

Loading the application

The tests of the models and of the controllers share a helper, fix/support/rails.rb, which fix does not run on its own, since its name does not end with _fix.rb:

# frozen_string_literal: true

# Loads the application in its test environment, for the tests of the
# models and of the controllers. Those of the domain, in fix/microblog,
# need none of this: they run on plain Ruby. The specifications, in
# spec, need nothing but Fix.
#
# The tests empty the database, so they run in the test environment and
# nowhere else, whatever the shell says.
ENV["RAILS_ENV"] = "test"

require_relative "../../config/environment"
require "rack/test"

# Keeps the test database in step with db/schema.rb, as rails/test_help
# does.
ActiveRecord::Migration.maintain_test_schema!

# Fix builds the subject anew for every expectation, in a child process
# of its own, which gets a connection of its own: Active Record forgets
# the pool of the parent after a fork. The database itself is outside
# the processes, so a subject starts by emptying it. Nothing is rolled
# back; nothing needs to be.
def empty_database
  Follow.delete_all
  Post.delete_all
  User.delete_all
end

# A browser for the application, with its cookies, signed in as the user
# of that handle when one is given. Its requests return a
# Rack::MockResponse, with a status, a body and a location.
def browser(as: nil)
  visitor = Rack::Test::Session.new(Rails.application)
  visitor.post("/session", handle: as) if as
  visitor
end

Four things happen here.

The two methods are defined at the top level, where the subject block sees them, as it sees Kernel. A name defined that way can no longer be the name of a let.

A model

A post has a body, which must not be blank and may not exceed 280 characters. The specification, spec/models/post_spec.rb, says so in terms of the methods of a record, valid?, errors and mentions, with the body as a parameter:

# frozen_string_literal: true

require "fix"

# A post, built from a body: it mentions the handles of the body, and
# says why an empty or an overlong body is refused.
Fix :Post do
  let(:body) { "Good morning, @bob and @carol." }

  on :valid? do
    it MUST be_true
  end

  on :mentions do
    it MUST eq %w[bob carol]
  end

  with body: "" do
    on :valid? do
      it MUST be_false
    end

    on :errors do
      on :full_messages do
        it MUST eq ["Body can't be blank"]
      end
    end
  end

  with body: "a" * 281 do
    on :valid? do
      it MUST be_false
    end

    on :errors do
      on :full_messages do
        it MUST eq ["Body is too long (maximum is 280 characters)"]
      end
    end
  end
end

The test, fix/models/post_fix.rb, builds a post of a user with that body, validated so that errors is filled:

#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative "../support/rails"
require_relative "../../spec/models/post_spec"

# The subject is a new post of Alice with the body, a parameter of the
# specification, validated.
exit(Fix[:Post].test do
  empty_database
  User.create!(handle: "alice").posts.new(body:).tap(&:validate)
end)

The user is created inside the block, for each expectation, in a database just emptied: a record a test needs is built where the subject is, never in the file around it, which runs once in the parent. A test that needs a row to exist already, a handle taken or a user followed, creates it there too.

A controller

The subject of a controller test is a response. The specification, spec/controllers/posts_spec.rb, judges its status, its location and its body, for a signed-in visitor and for one who is not:

# frozen_string_literal: true

require "fix"

# The response to posting the body, for the visitor signed in with the
# handle: home when the post is saved, the form again with the reason
# when it is not, the sign-in page for a visitor who is not signed in.
Fix :CreatePost do
  let(:handle) { "alice" }
  let(:body) { "Hello, world!" }

  on :status do
    it MUST eq 302
  end

  on :location do
    it MUST eq "http://example.org/"
  end

  with body: "a" * 281 do
    on :status do
      it MUST eq 422
    end

    on :body do
      it MUST match(/Body is too long/)
    end
  end

  with handle: nil do
    on :status do
      it MUST eq 302
    end

    on :location do
      it MUST eq "http://example.org/session/new"
    end
  end
end

The test, fix/controllers/posts_fix.rb, creates the user, signs the browser in with the handle, posts the body and gives the response:

#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative "../support/rails"
require_relative "../../spec/controllers/posts_spec"

# The subject is the response to posting the body, for the visitor
# signed in with the handle; both are parameters of the specification.
exit(Fix[:CreatePost].test do
  empty_database
  User.create!(handle: "alice")
  browser(as: handle).post("/posts", post: { body: })
end)

The host of a Rack::Test request is example.org, hence the locations. Signing in is a request, as a visitor would make it; in this application a handle is enough, with no password, and an application with passwords signs its browser in the same way, with the request its sign-in form sends. Forgery protection is off in the test environment, as the config/environments/test.rb that Rails generates sets it, and show_exceptions is :rescuable, so that a record not found gives a 404 to judge, not an exception.

Running

bin/rails db:prepare
bundle exec fix

db:prepare creates the development and the test databases. fix then runs the files of fix, each in a process of its own, which boots the application once: that is where the time of a run goes. Each line points at an expectation of spec, and the status is 0 when every file is met. A task of the Rakefile, task(:fix) { sh "fix" }, runs it with rake; the continuous integration of the application runs bin/rails db:prepare, then bundle exec fix, and since the environment sets CI, the application is eager loaded there, as Rails recommends.

The files of fix are executable, with #!/usr/bin/env ruby on their first line: one of them runs on its own, fix/models/post_fix.rb, and finds its gems through config/boot.rb.

What a process does not isolate

In a project listed what a child process leaves behind. In a Rails application, that is the database, hence empty_database, and the log, log/test.log, which the children write to as the parent would. What stays in the process ends with it: a mail that the test environment of Rails keeps in ActionMailer::Base.deliveries instead of sending, say, is read in the subject block, in the process that sent it, and given to the specification.

To stop in a subject with a debugger, run its contexts in the process itself, test(fork: false), as In a project says; each subject still empties the database first.