Qaskills Ruby Test::Unit
Classic xUnit-style Ruby testing with Test::Unit covering assertions, fixtures, test case organization, mocking patterns, and lifecycle hooks for reliable Ruby application testing.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/ruby-testunit" ~/.claude/skills/pramoddutta-qaskills-ruby-test-unit && rm -rf "$T"
manifest:
seed-skills/ruby-testunit/SKILL.mdsource content
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
so output reads as a living specification.test_<method>_<scenario>_<expected> - Use fixtures for shared state -- Leverage
andsetup
for per-test initialization and cleanup.teardown - 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: '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
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: '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
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[ 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
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 'alice@example.com' end end
Best Practices
- Use
andsetup
for consistent state -- Initialize shared objects inteardown
and release resources insetup
so each test starts fresh.teardown - Prefer specific assertions -- Use
overassert_equal
for better error messages and clarity on what failed.assert(a == b) - Use
method for parameterized tests -- Test::Unit's data-driven testing keeps multiple test cases organized and produces clear output per data set.data - Mock external dependencies -- Use Mocha to stub HTTP clients, databases, and third-party APIs while testing business logic in isolation.
- Test exceptions with
-- Verify both the exception class and message content to ensure errors are meaningful and correct.assert_raise - Use
/startup
for expensive resources -- Share database connections or file handles across all tests in a class to avoid redundant initialization.shutdown - Keep test files parallel to source -- Mirror the
directory structure inlib/
so developers can quickly locate related tests.test/ - 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
couples tests to implementation; test through the public API.send(:private_method) - Using
with boolean expressions --assert
gives no useful message on failure; useassert(result == expected)
instead.assert_equal expected, result - 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
in tests causes order-dependent failures that are notoriously difficult to debug.@@variables - Hardcoding absolute paths -- Using platform-specific paths breaks tests on different machines; use
andFile.expand_path
.Tempfile - 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
-- When testing that code completes without error, useassert_nothing_raised
to make the intent explicit.assert_nothing_raised