// 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.
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', subclassMinitest::Test, and everytest_*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
- Add a test that
ofbiz_hashis deterministic — the same word and salt always produce the same digest. - Write
parse_user(body)that pullsuserLoginId, then a test asserting it returns'admin'for the sample line. - Add a
setupmethod that builds the sample line once into@line, and rewrite the parse tests to use@lineinstead 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.