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 environment is forced to
test, rather than defaulted to it as atest_helper.rbof Rails does: the tests empty the database, so a shell that exportsRAILS_ENV=developmentmust not send them to the development one. - The application boots, and
maintain_test_schema!keeps the test database in step withdb/schema.rb, asrails/test_helpdoes for minitest. - Fix builds the subject anew for every expectation, in a child process of its own. A child starts as a copy of its parent, open connections included; Active Record discards the connection pools of the parent in a forked child, which connects on its own when it first needs the database. The database is outside the processes, though, and keeps what a subject writes, so a subject starts by emptying it. Nothing is wrapped in a transaction, nothing is rolled back, and nothing is shared between two expectations.
- A browser for the controllers: a
Rack::Test::Sessionon the application, with its cookies, signed in when a handle is given. Rack::Test is a dependency of Action Pack; its requests return aRack::MockResponse, with astatus, abodyand alocation.
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.