// Ruby for Pentesters · Part 9

Testing your tool

A tool you'll rerun deserves a safety net. We meet minitest, Ruby's built-in test framework, and learn to write code that's testable — separating the logic you can check from the network you can't.

RubyTestingminitestDesign
4 min read

Part 8 turned our scripts into a real tool. Tools get edited, and edits break things — usually the quiet way, where it still runs but returns garbage. A test is a second piece of code that checks the first still does what you think. Write them once and every future change gets checked for free. Ruby ships a test framework in the standard library: minitest.

Your first test

A test file requires minitest/autorun, subclasses Minitest::Test, and defines methods whose names start with test_:

require 'minitest/autorun'

class SanityTest < Minitest::Test
  def test_math_still_works
    assert_equal 42, 40 + 2
  end
end

Run the file directly and minitest finds and runs every test_ method:

$ ruby sanity_test.rb
Run options: --seed 12345

# Running:

.

Finished in 0.001s, 1000 runs/s, 1000 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Each . is a passing test; assert_equal(expected, actual) is the workhorse — it fails loudly if the two don’t match. That’s the whole loop: assert what should be true, run, watch for dots.

Test the logic, not the network

The parts worth testing are the ones with real logic — the hash reimplementation from Part 6, the extraction regex from Part 5. Crucially, neither needs a live box: they’re pure functions from input to output, so a test can pin them down in milliseconds:

require 'minitest/autorun'
require 'digest'
require 'base64'

def ofbiz_hash(word, salt)
  Base64.urlsafe_encode64(Digest::SHA1.digest(salt + word), padding: false)
end

def parse_hash(body)
  body[/currentPassword="(\$SHA\$[^"]+)"/, 1]
end

class BiznessTest < Minitest::Test
  def test_hash_matches_known_pair
    assert_equal 'uP0_QaVBpDWFeo8-dRzDqRwXQ2I', ofbiz_hash('monkeybizness', 'd')
  end

  def test_wrong_password_differs
    refute_equal 'uP0_QaVBpDWFeo8-dRzDqRwXQ2I', ofbiz_hash('hunter2', 'd')
  end

  def test_parse_pulls_the_hash
    line = 'currentPassword="$SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I" userLoginId="admin"'
    assert_equal '$SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I', parse_hash(line)
  end
end
$ ruby test_bizness.rb
...
3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Three assertions worth knowing: assert_equal (they match), refute_equal (they don’t — proving a wrong password gives a different hash), and assert_includes for “is this substring in there.” There’s one for every shape of expectation.

Designing for testability

Notice why parse_hash was testable: it takes a response body as a string and returns the hash — the parsing is separate from the fetching. That’s the whole trick. A method that both makes the request and digs through the answer can’t be tested without a live target; split it in two — one method fetches, another parses the string it returns — and the parser becomes checkable in isolation.

Writing tests quietly pushes you toward that cleaner shape, which is half the reason to bother. The tool you can test is usually the tool that’s built well.

Takeaways

  • minitest ships with Ruby: require 'minitest/autorun', subclass Minitest::Test, and every test_* method runs when you run the file.
  • assert_equal, refute_equal, assert_includes (and friends) express what should be true; a failed assertion reports exactly what didn’t match.
  • Test pure logic — hashing, parsing — because it needs no network and pins down the parts most likely to break on a change.
  • Separating parsing from fetching makes code testable; reaching for tests nudges you into that better design.

Exercises

  1. Add a test that ofbiz_hash is deterministic — the same word and salt always produce the same digest.
  2. Write parse_user(body) that pulls userLoginId, then a test asserting it returns 'admin' for the sample line.
  3. Add a setup method that builds the sample line once into @line, and rewrite the parse tests to use @line instead of repeating the string.

Next — Part 10: the finale. We take this exact chain and repackage it as a Metasploit module — the same Ruby you already know, dropped into the framework’s structure so use and run just work.

← All parts