Writing Tests
No Fixtures
The app uses database-level encryption (via raft_encrypts). Fixtures don’t work because encrypted columns would need pre-encrypted fixture values tied to specific test keys. Instead of fixtures, tests create records inline using the creator helper and the once {} pattern.
The once {} Pattern
Database records live for the duration of a single test file, not per test method.
This is why parallelization is disabled — each test process would independently
truncate the database, causing race conditions.
In setup, use once {} to initialize records that all tests in the file will share:
def setup
once {
creator.user # creates a user with a userspace
creator.workspace # creates a groupspace with memberships
}
end
The call once {} does three things:
- Truncates the database (via
DatabaseCleaner) - Resets global references (
@@user,@@workspace, etc.) - Yields the block — this only runs once per test class
Because once {} sets self.class.use_transactional_tests = false, there is no
transaction wrapping individual tests. Records created in once {} persist across
all test methods in the class.
The creator Helper
creator returns a TestCreator instance that sets up users, workspaces, and encryption
keys for you.
# creates a user with a userspace, profile, and memberships
creator.user
# creates an additional user with a different username
creator.user("fredrick")
# creates a Groupspace with memberships. by default the global user is the admin
creator.workspace
# creates a workspace with specific roles
creator.workspace("Team Space", admin: user1, edit: user2)
Encryption Context
creator.user and creator.workspace automatically set up vaults and encryption keys. The
first call to creator.user also sets the global user (@@user or global_user), and the first call to
creator.workspace sets the global workspace (@@workspace or global_workspace) and switches the encryption
context to that workspace.
Because of this, you only need to call encryption_context() directly if a test needs to
operate under a different encryption context than the one set up in once {}:
def test_something
encryption_context(global_workspace) do
# ... create or read records in the workspace
end
encryption_context(global_user.userspace) do
# ... create or read records in the userspace
end
end
encryption_context accepts a block and yields within that context, or sets the key globally
if called without a block.
Globals
The creator helper sets these globals on the test class:
| Global | Set by | Accessor |
|---|---|---|
@@user |
creator.user |
global_user |
@@workspace |
creator.workspace |
global_workspace |
@@vault |
(automatic) | global_vault |
These are available in any test method after setup runs.
Tests That Delete Records
Since once {} creates records that are shared across every test method, never delete the
records created in once {}. If a test needs to destroy something, create a new record
specifically for that test and destroy it:
def test_destroy
label = Label.create!(workspace: global_workspace, name: "test", hue: 0)
assert_difference("LabelAssignment.count", -1) do
label.destroy
end
end
The same principle applies to any state mutation: create fresh records for the test rather
than modifying the ones set up in once {}.
Authentication in Integration Tests
Controller and integration tests use the demo auth system to log in:
def setup
once {
creator.user
}
end
test "requires login" do
login_as(global_user)
get some_path
assert_response :success
end
Test Encryption Assertions
The test helper provides assert_encrypted to verify a field is properly encrypted:
assert_encrypted record: doc, field: :title, cleartext: "Secret Title"
This checks that:
- The field uses
Raft::EncryptedAttributeType - The ciphertext differs from the cleartext
- The ciphertext decrypts to the original value
- A fresh database read returns the correct cleartext
Running Tests
All Tests
rake test
This runs the main app’s tests first, then iterates over every directory in engines/ and
gems/.
For directories that contain a test/dummy folder (a full Rails engine with its own dummy app), it runs:
cd engine/ && RAILS_ENV=test rake db:test:prepare test
For directories without a dummy app (most engines), it runs from the parent app:
bin/rails test engine/test
Single Test File
bin/rails test test/models/my_test.rb
bin/rails test engines/raft_files/test/models/files/file_tree_test.rb
Single Test Method
bin/rails test test/models/my_test.rb:15
Engine Tests Only
rake engine:test
Engine Test Helpers
Most engines don’t have their own dummy Rails app. Instead, their test_helper.rb simply
requires the main app’s test helper:
# engines/raft_files/test/test_helper.rb
require_relative "../../../test/test_helper"
module ActiveSupport
class TestCase
# engine-specific helpers go here
end
end
This gives engine tests access to all the same helpers (creator, encryption_context,
once, login_as, etc.) as the main app’s tests.