by thetestingacademy
Classic xUnit-style Ruby testing with Test::Unit covering assertions, fixtures, test case organization, mocking patterns, and lifecycle hooks for reliable Ruby application testing.
npx @qaskills/cli add ruby-testunitAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
You are an expert Ruby developer specializing in testing with Test::Unit. When the user asks you to write, review, or debug Test::Unit tests, follow these detailed instructions to produce well-structured, reliable test suites that exercise Ruby code thoroughly.
test_<method>_<scenario>_<expected> so output reads as a living specification.setup and teardown for per-test initialization and cleanup.project/
lib/
services/
user_service.rb
payment_service.rb
models/
user.rb
order.rb
utils/
validators.rb
formatters.rb
test/
test_helper.rb
services/
test_user_service.rb
test_payment_service.rb
models/
test_user.rb
test_order.rb
utils/
test_validators.rb
test_formatters.rb
integration/
test_user_payment_flow.rb
fixtures/
sample_users.yml
Gemfile
Rakefile
group :test do
gem 'test-unit', '~> 3.6'
gem 'mocha', '~> 2.1'
gem 'simplecov', require: false
end
require 'simplecov'
SimpleCov.start
require 'test-unit'
require 'mocha/test_unit'
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.libs << 'test'
t.libs << 'lib'
t.test_files = FileList['test/**/test_*.rb']
t.verbose = true
end
task default: :test
# Run all tests
ruby -Itest -Ilib test/services/test_user_service.rb
# Run with Rake
rake test
# Run specific test method
ruby -Itest -Ilib test/services/test_user_service.rb --name test_create_user_with_valid_data
# Auto-discovery
testrb test/
require 'test_helper'
require 'services/user_service'
class TestUserService < Test::Unit::TestCase
def setup
@service = UserService.new
@valid_user_data = {
name: 'Alice',
email: 'alice@example.com',
age: 30
}
end
def teardown
@service = nil
end
def test_create_user_with_valid_data
user = @service.create_user(@valid_user_data)
assert_not_nil user
assert_equal 'Alice', user.name
assert_equal 'alice@example.com', user.email
end
def test_create_user_without_email_raises_argument_error
invalid_data = { name: 'Bob' }
assert_raise(ArgumentError) do
@service.create_user(invalid_data)
end
end
def test_create_user_with_invalid_email_raises_error
invalid_data = { name: 'Bob', email: 'not-an-email' }
error = assert_raise(ArgumentError) do
@service.create_user(invalid_data)
end
assert_match(/email/, error.message)
end
end
class TestAssertionExamples < Test::Unit::TestCase
def test_equality_assertions
assert_equal 4, 2 + 2
assert_not_equal 5, 2 + 2
assert_in_delta 0.3, 0.1 + 0.2, 0.001
assert_in_epsilon 100, 99, 0.02
end
def test_truth_assertions
assert true
assert_true true
assert_false false
assert_nil nil
assert_not_nil 'value'
end
def test_identity_assertions
a = 'hello'
b = a
assert_same a, b
assert_not_same 'hello', 'hello'
end
def test_type_assertions
assert_instance_of String, 'hello'
assert_kind_of Numeric, 42
assert_kind_of Enumerable, [1, 2, 3]
end
def test_collection_assertions
assert_include [1, 2, 3], 2
assert_not_include [1, 2, 3], 4
assert_empty []
assert_not_empty [1]
end
def test_string_assertions
assert_match(/\d+/, 'hello123')
assert_not_match(/\d+/, 'hello')
end
def test_exception_assertions
assert_raise(ZeroDivisionError) { 1 / 0 }
assert_nothing_raised { 1 + 1 }
end
def test_comparison_assertions
assert_compare 10, '>', 5
assert_compare 5, '<', 10
assert_compare 10, '>=', 10
assert_compare 5, '<=', 5
end
def test_respond_to_assertion
assert_respond_to 'hello', :upcase
assert_respond_to [1, 2], :length
end
end
require 'test_helper'
require 'services/user_service'
class TestUserServiceWithMocks < Test::Unit::TestCase
def setup
@db = mock('database')
@email_client = mock('email_client')
@service = UserService.new(db: @db, email_client: @email_client)
end
def test_get_user_by_id
expected_user = { id: 1, name: 'Alice', email: 'alice@example.com' }
@db.expects(:find_one).with(id: 1).returns(expected_user)
user = @service.get_user(1)
assert_equal 'Alice', user[:name]
end
def test_get_user_not_found_returns_nil
@db.expects(:find_one).with(id: 999).returns(nil)
user = @service.get_user(999)
assert_nil user
end
def test_create_user_sends_welcome_email
@db.expects(:insert).returns(1)
@email_client.expects(:send).with(
has_entries(to: 'bob@example.com', subject: 'Welcome')
)
@service.create_user(name: 'Bob', email: 'bob@example.com')
end
def test_create_user_handles_email_failure_gracefully
@db.expects(:insert).returns(1)
@email_client.expects(:send).raises(RuntimeError, 'SMTP error')
assert_nothing_raised do
@service.create_user(name: 'Bob', email: 'bob@example.com')
end
end
end
class TestPaymentService < Test::Unit::TestCase
def test_process_payment_calls_gateway
gateway = stub('gateway')
gateway.stubs(:charge).returns({ status: 'success', txn_id: 'abc123' })
service = PaymentService.new(gateway: gateway)
result = service.process_payment(amount: 50.00, card_token: 'tok_123')
assert_equal 'success', result[:status]
end
def test_process_payment_retries_on_timeout
gateway = mock('gateway')
gateway.expects(:charge).times(3).raises(Timeout::Error).then.raises(Timeout::Error).then.returns({ status: 'success' })
service = PaymentService.new(gateway: gateway)
result = service.process_payment(amount: 50.00, card_token: 'tok_123')
assert_equal 'success', result[:status]
end
end
class TestWithLifecycleHooks < Test::Unit::TestCase
class << self
def startup
puts 'Runs once before ALL tests in this class'
@@shared_resource = ExpensiveResource.new
end
def shutdown
puts 'Runs once after ALL tests in this class'
@@shared_resource.close
end
end
def setup
puts 'Runs before EACH test'
@local_state = fresh_state
end
def teardown
puts 'Runs after EACH test'
@local_state = nil
end
def test_example_one
assert_not_nil @@shared_resource
assert_not_nil @local_state
end
def test_example_two
assert_not_nil @@shared_resource
assert_not_nil @local_state
end
private
def fresh_state
{ counter: 0, items: [] }
end
end
class TestEmailValidator < Test::Unit::TestCase
VALID_EMAILS = %w[
user@example.com
user.name@domain.org
user+tag@example.co.uk
user123@test.io
].freeze
INVALID_EMAILS = [
'',
'not-an-email',
'@domain.com',
'user@',
'user @domain.com'
].freeze
data(
'simple email' => ['user@example.com', true],
'dotted name' => ['user.name@domain.org', true],
'plus tag' => ['user+tag@example.co.uk', true],
'empty string' => ['', false],
'no at sign' => ['not-an-email', false],
'no local part' => ['@domain.com', false],
)
def test_email_validation(data)
email, expected = data
assert_equal expected, Validators.valid_email?(email),
"Expected valid_email?('#{email}') to return #{expected}"
end
data(
'zero' => [0, false],
'one' => [1, true],
'seventeen' => [17, false],
'eighteen' => [18, true],
'one twenty' => [120, true],
'one twenty one' => [121, false],
'negative' => [-1, false],
)
def test_age_validation(data)
age, expected = data
assert_equal expected, Validators.valid_age?(age)
end
end
module CustomAssertions
def assert_valid_email(email, message = nil)
email_regex = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
full_message = build_message(message, "Expected ? to be a valid email", email)
assert_block(full_message) { email_regex.match?(email) }
end
def assert_json_response(response, message = nil)
full_message = build_message(message, "Expected response to be valid JSON with status 200")
assert_block(full_message) do
response.code == '200' && JSON.parse(response.body)
end
end
end
class TestWithCustomAssertions < Test::Unit::TestCase
include CustomAssertions
def test_email_format
assert_valid_email 'alice@example.com'
end
end
setup and teardown for consistent state -- Initialize shared objects in setup and release resources in teardown so each test starts fresh.assert_equal over assert(a == b) for better error messages and clarity on what failed.data method for parameterized tests -- Test::Unit's data-driven testing keeps multiple test cases organized and produces clear output per data set.assert_raise -- Verify both the exception class and message content to ensure errors are meaningful and correct.startup/shutdown for expensive resources -- Share database connections or file handles across all tests in a class to avoid redundant initialization.lib/ directory structure in test/ so developers can quickly locate related tests.send(:private_method) couples tests to implementation; test through the public API.assert with boolean expressions -- assert(result == expected) gives no useful message on failure; use assert_equal expected, result instead.@@variables in tests causes order-dependent failures that are notoriously difficult to debug.File.expand_path and Tempfile.assert_nothing_raised -- When testing that code completes without error, use assert_nothing_raised to make the intent explicit.- name: Install QA Skills
run: npx @qaskills/cli add ruby-testunit10 of 29 agents supported