Ruby testunit
QA Skills Directory QA Skills is a curated directory of testing-specific skills for AI coding agents (Claude Code, Cursor, Copilot, etc.).
npx -y skills add PramodDutta/qaskills --skill ruby-testunitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Classic xUnit-style Ruby testing with Test::Unit covering assertions, fixtures, test case organization, mocking patterns, and lifecycle hooks for reliable Ruby application testing.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
12.6 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
Ruby Test::Unit Skill
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.
Core Principles
- Test behavior through public interfaces -- Verify what the code does from a caller's perspective rather than inspecting internal state.
- One assertion focus per test -- Each test method should verify a single logical behavior for precise failure diagnostics.
- Arrange-Act-Assert -- Structure every test into setup, execution, and verification phases for readability and consistency.
- Isolate tests completely -- Each test must run independently and produce the same result regardless of execution order.
- Descriptive test names -- Name tests as
test_<method>_<scenario>_<expected>so output reads as a living specification. - Use fixtures for shared state -- Leverage
setupandteardownfor per-test initialization and cleanup. - Cover edge cases -- Test boundary values, empty inputs, nil handling, and error conditions explicitly.
Project Structure
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
Configuration
Gemfile
group :test do
gem 'test-unit', '~> 3.6'
gem 'mocha', '~> 2.1'
gem 'simplecov', require: false
end
test_helper.rb
require 'simplecov'
SimpleCov.start
require 'test-unit'
require 'mocha/test_unit'
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
Rakefile
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
Running Tests
# 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/
Basic Test Structure
require 'test_helper'
require 'services/user_service'
class TestUserService < Test::Unit::TestCase
def setup
@service = UserService.new
@valid_user_data = {
name: 'Alice',
email: '[email protected]',
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 '[email protected]', 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
Assertion Methods Reference
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
Mocking with Mocha
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: '[email protected]' }
@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: '[email protected]', subject: 'Welcome')
)
@service.create_user(name: 'Bob', email: '[email protected]')
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: '[email protected]')
end
end
end
Stubbing Methods
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
Lifecycle Hooks
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
Data-Driven Tests
class TestEmailValidator < Test::Unit::TestCase
VALID_EMAILS = %w[
[email protected]
[email protected]
[email protected]
[email protected]
].freeze
INVALID_EMAILS = [
'',
'not-an-email',
'@domain.com',
'user@',
'user @domain.com'
].freeze
data(
'simple email' => ['[email protected]', true],
'dotted name' => ['[email protected]', true],
'plus tag' => ['[email protected]', 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
Custom Assertions
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 '[email protected]'
end
end
Best Practices
- Use
setupandteardownfor consistent state -- Initialize shared objects insetupand release resources inteardownso each test starts fresh. - Prefer specific assertions -- Use
assert_equaloverassert(a == b)for better error messages and clarity on what failed. - Use
datamethod for parameterized tests -- Test::Unit's data-driven testing keeps multiple test cases organized and produces clear output per data set. - Mock external dependencies -- Use Mocha to stub HTTP clients, databases, and third-party APIs while testing business logic in isolation.
- Test exceptions with
assert_raise-- Verify both the exception class and message content to ensure errors are meaningful and correct. - Use
startup/shutdownfor expensive resources -- Share database connections or file handles across all tests in a class to avoid redundant initialization. - Keep test files parallel to source -- Mirror the
lib/directory structure intest/so developers can quickly locate related tests. - Test edge cases explicitly -- Include nil inputs, empty collections, boundary values, and Unicode strings in your test data.
- Run tests in random order -- Configure random seed execution to catch order-dependent test coupling.
- Use SimpleCov for coverage tracking -- Measure and enforce coverage thresholds to identify untested code paths.
Anti-Patterns
- Testing private methods directly -- Calling private methods via
send(:private_method)couples tests to implementation; test through the public API. - Using
assertwith boolean expressions --assert(result == expected)gives no useful message on failure; useassert_equal expected, resultinstead. - Not cleaning up in teardown -- Failing to close file handles, database connections, or temporary files causes resource leaks across test runs.
- Over-mocking -- Mocking every dependency makes tests prove nothing about real interactions; mock only I/O and non-deterministic behavior.
- Shared mutable class variables -- Modifying
@@variablesin tests causes order-dependent failures that are notoriously difficult to debug. - Hardcoding absolute paths -- Using platform-specific paths breaks tests on different machines; use
File.expand_pathandTempfile. - Large test methods -- Tests exceeding 20 lines usually verify too many things; split into focused test methods with clear names.
- Ignoring test output -- Not running with verbose flags means you miss valuable context about which behaviors are covered.
- Skipping error path testing -- Only testing the happy path leaves exception handling and edge cases unverified.
- Not using
assert_nothing_raised-- When testing that code completes without error, useassert_nothing_raisedto make the intent explicit.