Skip to main content

Testing

Testing Ruby extensions written in Rust requires consideration of both the Ruby and Rust sides of your code. This chapter covers best practices and tools for comprehensive testing.

Rust

Test Types

A well-tested Ruby extension should include:

  1. Unit Tests: Testing individual Rust functions
  2. Integration Tests: Testing the Ruby API
  3. Memory Tests: Ensuring proper memory management
  4. Cross-Platform Tests: Verifying behavior across platforms

Ruby Tests

Ruby tests should verify the public API of your extension:

require "test_helper"

class MyExtensionTest < Test::Unit::TestCase
def setup
# Any setup code
end

def test_basic_functionality
result = MyExtension.process_data("input")
assert_equal "expected output", result
end

def test_error_handling
assert_raises(MyExtension::Error) do
MyExtension.process_data(nil)
end
end

def test_memory_management
GC.stress = true
100.times do
obj = MyExtension::Object.new
obj.perform_operation
end
GC.stress = false
end
end

Rust Tests

Rust tests in your extension should focus on the internal logic:

// Mock function for testing
fn process_input(input: &str) -> Result<String, String> {
if input.is_empty() {
Err("Input cannot be empty".to_string())
} else {
Ok(format!("processed {}", input))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_internal_processing() {
let input = "test data";
let result = process_input(input);
assert_eq!(result, Ok("processed test data".to_string()));
}

#[test]
fn test_error_conditions() {
let result = process_input("");
assert!(result.is_err());
}
}

Memory Testing

Test memory management using rb-sys's test helpers:

use rb_sys_test_helpers::ruby_test;
use magnus::{Value, value::ReprValue};

#[ruby_test]
fn test_no_memory_leaks() {
let ruby = unsafe { magnus::Ruby::get_unchecked() };

// Create and drop Ruby objects
let obj: Value = ruby.eval("Object.new")
.unwrap_or_else(|_| ruby.qnil().as_value());
let _ = obj; // Ensure the object is used

// Run GC and check memory usage
magnus::gc::start();
}

Cross-Platform Testing

Ensure your tests run on all target platforms:

# test/platform_specific_test.rb
class PlatformSpecificTest < Test::Unit::TestCase
def test_platform_specific_behavior
if RUBY_PLATFORM =~ /darwin/
# macOS specific tests
test_macos_behavior
elsif RUBY_PLATFORM =~ /linux/
# Linux specific tests
test_linux_behavior
elsif RUBY_PLATFORM =~ /mingw|mswin/
# Windows specific tests
test_windows_behavior
end
end

private

def test_macos_behavior
# macOS specific assertions
end

def test_linux_behavior
# Linux specific assertions
end

def test_windows_behavior
# Windows specific assertions
end
end

Integration Testing

Test the complete flow from Ruby to Rust and back:

class IntegrationTest < Test::Unit::TestCase
def test_complete_workflow
# Setup test data
input = prepare_test_data

# Process through Rust
result = MyExtension.process(input)

# Verify results
assert_valid_result(result)

# Cleanup
cleanup_test_data
end

private

def prepare_test_data
# Create necessary test fixtures
end

def assert_valid_result(result)
# Comprehensive result validation
end

def cleanup_test_data
# Clean up any resources
end
end

Performance Testing

Implement benchmarks to track performance:

#[cfg(test)]
mod benchmarks {
use super::*;
use criterion::{criterion_group, criterion_main, Criterion};

fn benchmark_process(c: &mut Criterion) {
c.bench_function("process 1000 items", |b| {
b.iter(|| {
// Your benchmarked code here
})
});
}

criterion_group!(benches, benchmark_process);
criterion_main!(benches);
}

Best Practices

  1. Test Both Languages: Write tests in both Ruby and Rust
  2. Memory Verification: Use GC.stress in Ruby tests
  3. Error Cases: Test error handling thoroughly
  4. Platform Coverage: Test on all target platforms
  5. Integration Tests: Test complete workflows
  6. Performance Metrics: Include benchmarks

Common Test Patterns

1. Exception Testing

def test_exception_handling
assert_raises(MyExtension::Error, "Should raise on invalid input") do
MyExtension.process(nil)
end

assert_raises(ArgumentError, "Should raise on wrong type") do
MyExtension.process(123) # Expecting string
end
end

2. Memory Stress Testing

def test_under_memory_pressure
GC.stress = true
begin
100.times do
obj = MyExtension::Object.new
obj.process_data("test")
obj = nil
end
ensure
GC.stress = false
end
end

3. Thread Safety Testing

def test_thread_safety
threads = 10.times.map do
Thread.new do
100.times do
result = MyExtension.process("test")
assert_equal "expected", result
end
end
end

threads.each(&:join)
end

CI/CD Integration

Configure CI to test across platforms:

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
ruby: [2.7, 3.0, 3.1, 3.2]

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}

- name: Install dependencies
run: bundle install

- name: Run tests
run: bundle exec rake test

Debugging Tests

Useful techniques for debugging test failures:

  1. Enable Verbose Output:
ENV["RUST_BACKTRACE"] = "1"
ENV["RUST_LOG"] = "debug"
  1. Use Debug Assertions:
fn validate_input(value: i32) {
debug_assert!(value > 0, "Value must be positive, got {}", value);
debug_assert!(value < 100, "Value must be less than 100, got {}", value);
}
  1. Add Logging:
use log::{debug, info};
use magnus::Error;

fn process_data(input: &str) -> Result<String, Error> {
debug!("Processing input: {}", input);
// Processing logic
let result = input.to_uppercase();
info!("Processing completed");
Ok(result)
}