As a developer working extensively with HTML generation in Rust, I found myself repeatedly writing similar test assertions to compare HTML output. This led me to create html-compare-rs
, a library for comparing HTML content while ignoring differences that don't affect the rendered result. The journey taught me a lot about Rust macros and test ergonomics.
Testing HTML output is surprisingly tricky. Consider these equivalent pieces of HTML:
Hello
Hello
They render identically, but a simple string comparison would mark them as different. Add in variations in attribute order, comments, and whitespace handling, and you quickly end up with brittle tests that break on formatting changes.
I wanted a library that would:
The last two points led me to explore Rust macros for the first time.
The core of the library is fairly straightforward - parse the HTML, walk the DOM trees, compare nodes. But I wanted the test experience to be seamless. Instead of:
let comparer = new;
assert!;
I wanted:
assert_html_eq!;
This meant diving into Rust's declarative macros. Here's what I learned:
My first attempt was simple:
This worked, but the error messages were terrible. When a test failed, you'd just see "assertion failed" with no context about what was different.
The next iteration added proper error handling:
Key learnings here:
{{...}}
create a block expressionmatch
with references prevents moving valuesThe final challenge was supporting optional comparison options:
This pattern of having one macro rule delegate to another is common in Rust. The $crate
prefix ensures the macro works correctly when used from other crates.
Start Simple: My first macro was basic but working. Adding features incrementally made the process manageable.
Error Messages Matter: Good error messages are crucial for testing libraries. Taking time to format them well pays off.
Consider Ergonomics: The macro interface feels natural because it mirrors Rust's built-in assert_eq!
. When designing APIs, familiarity helps adoption.
Macro Hygiene: Using $crate
and being careful with identifier scope is important for macros that will be used in other crates.
Testing is Crucial: The library has extensive tests for both the comparison logic and the macros themselves. This caught several edge cases in macro expansion.
While the library works well for my needs, there's room for improvement:
Creating html-compare-rs
taught me a lot about both Rust macros and library design. What started as a simple testing utility led to diving deep into Rust's macro system. The result is a library that makes HTML testing more reliable and maintainable.
Most importantly, I learned that good library design isn't just about the core functionality - it's about creating an interface that feels natural and helps users understand what went wrong when things fail.
The code is open source and available on GitHub. Contributions and feedback are welcome!