If you ask me, tests are little executable promises. They describe what you believe your code should do, then give Python a way to tell you when that promise has been broken.
For offensive-security tooling, tests are especially helpful around parsing, formatting, network boundaries, and error handling: the boring edges where a tool often fails before it ever reaches the exciting part.
The point isn’t to make writing cool hacker tools boring, but tests do give you a fast way to ask, “did I just break the thing I thought was working?” Future-you deserves that kindness.
Types of Tests
Different tests answer different questions:
Unit tests check one small piece of code in isolation, such as a parser, formatter, validator, or helper function.
Integration tests check that two or more pieces work together, such as your scanner calling a HTTP client and interpreting the response.
Functional tests check a complete feature from the user’s point of view, without necessarily driving the whole deployed system.
End-to-end tests drive the whole path through the system, usually as close to production as practical.
Performance tests check speed, load, memory use, throughput, or other resource behaviour.
Smoke tests are quick checks that the important paths are alive before you trust a larger run.
Today we’re going to focus on unit tests and integration tests. They give you a lot of confidence early, and they’re small enough to write while the code is still fresh in your head.
A Small Testing Manifesto
When you’re creating tools that will eventually interact with networks, filesystems, APIs, or client environments, tests give you a controlled way to understand how that tool behaves before it gets anywhere near a production system.
Testing matters because a tool that floods a production service, mutates real data, or changes a client environment isn’t harmless. Even when you are running it in an authorised context, you’re still responsible for its blast radius. If you’re representing yourself, your team, or your employer, you need to care about what actions your tool takes, how you verified those actions, and whether your confidence came from evidence rather than the idea that “it should work”.
Some people might argue that “real hackers don’t care about that”, maybe, but it’s also a thought-terminating cliché and not a very useful one. Whether you call yourself a security engineer, penetration tester, ethical hacker, or something else entirely, accountability and responsibility matters when talking about your work. Know what your tool does, keep your tests controlled, and make sure it logs enough detail to diagnose runtime problems or explain any impact it has on a client environment.
A Tiny Function Worth Testing
This function takes raw wordlist lines and returns clean subdomain labels. It strips whitespace, skips empty lines, ignores comments, and keeps the order from the original wordlist.
def normalise_wordlist(lines): words = []for line in lines: word = line.strip()ifnot word or word.startswith("#"):continue words.append(word)return wordssample = [" www\n", "", "# comment", "mail\n"]print(normalise_wordlist(sample))
['www', 'mail']
Unit Tests With unittest
Python ships with a testing framework called unittest. A unittest test usually lives inside a class that inherits from unittest.TestCase. Each test method starts with test_, and each assertion says what must be true.
pytest is still a useful test runner, and this book’s repository uses python -m pytest for validation. One nice thing about pytest is that it can run many unittest test cases too, so learning unittest doesn’t trap you in one runner forever.
test_keeps_duplicate_looking_entries (__main__.WordlistTests.test_keeps_duplicate_looking_entries) ...
ok
test_removes_blank_lines_and_comments (__main__.WordlistTests.test_removes_blank_lines_and_comments) ...
ok
----------------------------------------------------------------------
Ran 2 tests in 0.004s
OK
Testing Exceptions
Sometimes the right behaviour is an error. assertRaises() lets you state that explicitly: this input should fail, and it should fail in this specific way.
test_builds_https_url_by_default (__main__.UrlBuilderTests.test_builds_https_url_by_default) ...
ok
test_rejects_empty_subdomain (__main__.UrlBuilderTests.test_rejects_empty_subdomain) ...
ok
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
Unit Tests for Binary Helpers
Unit tests are especially good for protocol helpers. You don’t need a live server to test whether a TLS record header is parsed correctly; you need a known byte string and a clear expected result.
test_formats_hex_preview (__main__.TlsHelperTests.test_formats_hex_preview) ...
ok
test_parses_tls_record_header (__main__.TlsHelperTests.test_parses_tls_record_header) ...
ok
----------------------------------------------------------------------
Ran 2 tests in 0.003s
OK
Integration Tests Without Touching the Internet
Integration tests check how pieces fit together. For a security tool, the risky part often comes from the test reaching across the network, not from the assertion at the end.
The safer pattern is to test against fixtures, fakes, or local services where you control the inputs and the consequences. In this book, fake_requests_get() acts like a small slice of requests.get() without ever leaving your machine.
test_finds_known_fixture_subdomain (__main__.SubdomainIntegrationTests.test_finds_known_fixture_subdomain) ...
ok
test_unknown_fixture_target_raises_connection_error (__main__.SubdomainIntegrationTests.test_unknown_fixture_target_raises_connection_error) ...
ok
----------------------------------------------------------------------
Ran 2 tests in 0.005s
OK
The same idea works for socket-shaped code. If a fixture behaves like the object your function expects, you can test parsing and control flow without sending packets to a real host.
test_reads_heartbeat_fixture_response (__main__.HeartbleedFixtureIntegrationTests.test_reads_heartbeat_fixture_response) ...
ok
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
Recording External Calls With VCR.py
Sometimes you really do need to test code that talks to a live HTTP API. VCR.py can record an HTTP interaction into a cassette file, then replay that response during later test runs.
VCR.py is useful because it can make HTTP-dependent tests repeatable, but you need to be mindful that the cassette may contain secrets or sensitive information that isn’t suitable for test cases. So always make sure you choose safe record modes, review what is in the cassette, and make sure you’re allowed to call the live service in the first place.
Checkpoint
Write a unittest.TestCase for normalise_wordlist() that covers blank lines, comments, whitespace, and duplicate-looking input such as api, api, and API.
Write unit tests for a URL-building helper, including the normal https://www.example.test case and at least one invalid input case.
Write unit tests for parse_tls_record_header() and hex_preview() using deterministic byte strings.
Write an integration-style test that uses fake_requests_get() against known .example.test fixture hosts.
Write an integration-style test that uses HeartbleedFixtureSocket to exercise socket-shaped code without touching the network.
Optional stretch: sketch where VCR.py would fit if your tool needed to call a real API during one carefully controlled recording run.
Run the tests, then deliberately break one expected value and run them again. If everything went the way we expected, Python should complain in a way that points you back to the broken promise.
Extension Activity
Add one fixture-backed integration test for a failure path, not just a successful response. For example, assert that an unknown subdomain raises or is handled cleanly without touching the internet.