Create a Test Module in Rust
Mục tiêu: Set up the basic structure for unit testing in a Rust file by creating a mod tests block that is only compiled during test runs using the #[cfg(test)] attribute.
Incredible work! You have successfully built an end-to-end pipeline that transforms a simple Markdown file into a complete, valid HTML5 document. Your tool now reads a file, passes its content through a sophisticated multi-stage parser, wraps the result in a professional HTML shell, and writes the final product to a new file. This is a major milestone.
Before we add even more features, now is the perfect time to build a safety net. As your parser grows in complexity, how can you be sure that a new feature (like parsing for lists) won’t accidentally break the link parsing logic you’ve so carefully crafted? The answer is automated testing.
Building a Safety Net: An Introduction to Unit Testing
Automated tests are small pieces of code that execute parts of your application and verify that they behave as expected. They act as a safety net, catching regressions—bugs that appear in existing functionality after you make a change. By writing tests, you can refactor and enhance your code with confidence, knowing that your safety net will alert you if anything breaks.
Rust has a fantastic, first-class testing framework built right into the language and its tooling. You don’t need to install any external libraries to get started. The cargo test command will automatically discover and run any tests you’ve written.
The Anatomy of a Test Module in Rust
The convention in Rust is to place unit tests—tests that focus on a small, isolated “unit” of code like a single function—in the same file as the code they are testing. This keeps the tests and the implementation close together, making it easier to maintain them in sync. These tests are typically grouped into a dedicated submodule named tests.
To prevent our test code from being included in the final, compiled executable that we would distribute to users, Rust provides a powerful mechanism called conditional compilation.
Conditional Compilation: The Magic of #[cfg(test)]
At the heart of Rust’s testing structure is the #[cfg(test)] attribute. Let’s break this down:
#[...]: This syntax denotes an attribute in Rust. Attributes are metadata that you add to your code to change its behavior.cfg: This stands for “configuration”. Thecfgattribute tells the Rust compiler to only compile the code it’s attached to if a certain configuration option is set.(test): This is the specific configuration option. When you runcargo test, Cargo tells the compiler to set thetestconfiguration flag to true. When you runcargo buildorcargo run, this flag is not set.
Putting it all together, #[cfg(test)] is a directive that says: “Only compile the following block of code when I am running cargo test.”
This is incredibly powerful. It means you can write extensive test code, helper functions for testing, and even include test-specific dependencies without bloating your final release binary. The compiler will simply ignore all of it when building your application for production.
Let’s create this structure now. Open your src/parser.rs file and add the following code snippet to the very bottom.
// src/parser.rs
// ... all of your existing parser functions are above this line ...
// The `#[cfg(test)]` attribute tells the Rust compiler to only compile
// this module when running tests, such as with `cargo test`. This ensures
// our test code doesn't end up in our final production binary.
#[cfg(test)]
mod tests {
// Our test functions will go in here.
}
Deconstructing the New Code
You’ve just laid the foundation for all the tests in your parser module. Let’s review what you added:
#[cfg(test)]: As we discussed, this ensures the entiremod testsblock is only active during a test run.mod tests { ... }: This declares a new, private submodule namedtests. Because it’s a child module insideparser.rs, it has its own separate scope. This is important: right now, code inside this module cannot “see” theparseorparse_inline_elementsfunctions from its parent.
If you run cargo test in your terminal right now, the command will succeed! It will find that you have a test suite (even though it contains zero tests) and report that everything passed. You have successfully configured the testing harness for your parser.
Next Steps
You’ve created the test module, but it’s an empty, isolated room. It can’t yet access the parse function it is supposed to test. In the very next task, you will learn how to use the use super::*; statement to bring your parser’s functions into the scope of the tests module, making them available for testing.
Further Reading
To solidify your understanding of Rust’s testing capabilities and the principles behind unit testing, I highly recommend these resources:
- The Rust Book, Chapter 11: Writing Automated Tests: The definitive guide to testing in Rust. It covers the
#[test]attribute, assertions, and how to organize tests. - The
cfgattribute in The Rust Reference: A detailed, technical explanation of all the options available for conditional compilation. - Unit Tests, Mocks, and Stubs: A classic article explaining the different types of tests and their purposes.
Rust: Import Parent Module into Test Scope
Mục tiêu: Learn how to make functions from a parent module accessible within a nested test module in Rust by using the use super::\*; statement. This task explains the role of use, super, and the glob operator \* in managing module scope for testing.
Excellent! You have successfully laid the groundwork for your test suite by creating the tests module within src/parser.rs. You’ve used the #[cfg(test)] attribute to tell Rust that this code is special—it’s for testing only and should be excluded from your final application binary.
However, as we noted in the last task, this new module is like a sealed room. By default, it has its own private scope and cannot “see” the functions in its parent module, parser. If you were to try and call the parse() function from within this new module, the compiler would give you an error, saying it can’t find a function with that name. Our current task is to open the door and bring those functions into our testing environment.
Bridging the Gap: The use, super, and * Keywords
To solve this, we need to explicitly tell our tests module where to find the code it needs. Rust’s module system is hierarchical, and we use the use keyword to bring items from one part of the code into the scope of another.
To do this, we’ll use a very common and powerful line of code in Rust test modules: use super::*;. Let’s break down what each part of this statement does:
use: This keyword is a directive that says, “I want to use something from another module, so please make it available in the current scope.”super: This is a special path keyword that refers to the parent module. Since ourtestsmodule is defined insidesrc/parser.rs, its parent is theparsermodule itself.superis how we “look up” one level in the module hierarchy.::: This is the path separator, used to navigate through module hierarchies.*: This is the glob operator. It’s a wildcard that means “import all public items from this path.”
Putting it all together, use super::*; is a command that means: “Look in the parent module (super) and bring all of its public items (*) into the current scope.”
Let’s add this single, crucial line to your tests module at the bottom of src/parser.rs.
// src/parser.rs
// ... all of your existing parser functions (parse, parse_inline_elements) are above this line ...
#[cfg(test)]
mod tests {
// This line imports all public items from the parent module (the `parser` module)
// into the scope of this `tests` module. This makes functions like `parse()`
// directly available for us to use in our test functions below.
use super::*;
}
Deconstructing the Result
With this line in place, your test module is no longer isolated. It now has direct access to any pub items from its parent, the parser module.
This is a perfect moment to reinforce the concept of visibility (pub). Let’s consider what was imported:
pub fn parse(...): This function is public, so the*glob operator will import it. We can now callparse("...")directly from within a test.fn parse_inline_elements(...): This is a private helper function (it lacks thepubkeyword). The*glob operator will not import it. This is actually a good thing! The principle of unit testing is to test a module’s public API—the functions it promises to provide to the outside world. We test the overall behavior ofparse, and by doing so, we are implicitly testing the private helpers it relies on. We don’t need to test the private implementation details directly.
You have now fully prepared your testing environment. The tests module is configured, and it has access to the necessary functions it needs to test.
Next Steps
The stage is now perfectly set. With the parse function in scope, you are ready to write your very first test. In the next task, you will use the #[test] attribute to define a test function and the powerful assert_eq! macro to verify that your parser correctly converts a Markdown <h1> heading into its corresponding HTML tag.
Further Reading
To gain a deeper understanding of Rust’s powerful module and path system, which is fundamental to organizing any non-trivial Rust project, these resources are essential:
- The Rust Book, Chapter 7.4: Bringing Paths into Scope with the
useKeyword: A comprehensive guide to theusekeyword. - The Rust Book, Chapter 7.3: Referring to an Item in the Module Tree: This section explains paths, the
superkeyword, and how to navigate the module hierarchy. - Rust by Example:
usedeclaration: A concise, hands-on example of howuseworks.
Write Your First Unit Test for the H1 Parser
Mục tiêu: Create a unit test in Rust to verify the H1 Markdown parsing logic. This involves using the #[test] attribute to define a test function and the assert\_eq! macro to compare the parser’s output with the expected HTML string.
You have perfectly prepared the ground for testing. By creating the tests module and bringing the parent module’s functions into scope with use super::*;, you have built a dedicated workshop where you can now put your parser’s logic under the microscope. The environment is set, the tools are in place, and it’s time to write your first unit test.
Writing Your First Test: The Anatomy of Verification
At the core of unit testing are two fundamental concepts: defining a test case and asserting its outcome. In Rust, these are accomplished with two simple but powerful tools:
- The
#[test]Attribute: This is an attribute you place directly above a function. It acts as a flag, telling the Rust compiler and Cargo, “This function is a test case. When the user runscargo test, you should execute this function and report whether it succeeds or fails.” Any function marked with#[test]is automatically discovered and run by the test harness. -
Assertion Macros (
assert_eq!): How does a test “succeed” or “fail”? A test passes if it runs to completion without panicking. The easiest way to cause a panic when something is wrong is by using an assertion macro. The most common of these isassert_eq!(left, right). This macro compares theleftandrightvalues.- If they are equal, the macro does nothing, and the test function continues.
- If they are not equal, the macro will panic, which immediately stops the function and marks the test as failed. Crucially, it will print a helpful error message showing the difference between the “left” (actual) value and the “right” (expected) value, making debugging much easier.
Our goal is to create a test that calls your parse function with a simple H1 Markdown string and asserts that the output is exactly the HTML you expect.
Let’s add your first test function to the tests module at the bottom of src/parser.rs.
// src/parser.rs
// ... all of your existing parser functions are above this line ...
#[cfg(test)]
mod tests {
use super::*;
// The `#[test]` attribute marks this function as a test case.
// `cargo test` will find and execute this function.
#[test]
fn test_parse_h1() {
// We define the input Markdown we want to test.
let markdown_input = "# Hello";
// We define the exact HTML output we expect our parser to produce.
// It is critical that this includes the trailing newline (`\n`)
// because our `parse` function is designed to add it.
let expected_html = "<h1>Hello</h1>\n";
// The `assert_eq!` macro is the heart of the test.
// It compares the actual output of `parse(markdown_input)` with
// our `expected_html`. If they are not identical, the test will fail.
assert_eq!(parse(markdown_input), expected_html);
}
}
Deconstructing Your First Test
Let’s break down the test function you just wrote line by line.
#[test]: This attribute officially registerstest_parse_h1as a test case. Without this line, it would just be a regular, unused function.fn test_parse_h1(): The function signature is simple. Test functions in Rust take no arguments and have no return value. Their success or failure is determined by whether they panic. The naming conventiontest_feature_being_testedis a common and highly recommended practice.let markdown_input = "# Hello";: This is the Arrange part of a standard test structure (Arrange-Act-Assert). We are arranging the specific input data for our test case.let expected_html = "<h1>Hello</h1>\n";: This is also part of the Arrange step. We are defining the “ground truth”—the exact output we expect for our given input. It is crucial to be precise here. Based on the logic you built in the previous steps, your parser adds a\nafter each block-level tag, so we must include it in our expectation for the test to pass.assert_eq!(parse(markdown_input), expected_html);: This is the Act and Assert part.- Act: We act by calling the function we want to test:
parse(markdown_input). This produces the actual output. - Assert: The
assert_eq!macro performs the verification. It takes the actual output (the first argument) and compares it byte-for-byte against the expected output (the second argument).
- Act: We act by calling the function we want to test:
You have now written a clear, self-contained specification for a single behavior of your parser: “When given ‘# Hello’, the parse function must produce ‘
Hello
\n’”.
Running Your Test
Now for the rewarding part. Go to your terminal, in the root directory of your project, and run the test command:
cargo test
Cargo will compile your code (including the tests module, because the test configuration flag is set) and then run the test runner. You should see output similar to this:
Compiling markdown_converter v0.1.0 (/path/to/markdown_converter)
Finished test [unoptimized + debuginfo] target(s) in 0.50s
Running unittests src/main.rs (target/debug/deps/markdown_converter-...)
Running unittests src/parser.rs (target/debug/deps/markdown_converter-...)
running 1 test
test parser::tests::test_parse_h1 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The line test parser::tests::test_parse_h1 ... ok is the confirmation you’re looking for. It means your test ran without panicking, and your parser is behaving exactly as expected for this case.
Next Steps
Congratulations on writing your first unit test! You have now built a safety net that confirms your H1 parsing logic is correct. The pattern you’ve established here—Arrange, Act, Assert—is the template you will use for all subsequent tests. In the next tasks, you will expand your test suite by creating new test functions for paragraphs, bold text, italics, and links, ensuring every feature of your parser is verified and protected against future regressions.
Further Reading
To deepen your understanding of the tools and patterns you’ve just used, explore these resources:
- The Rust Book: How to Write Tests: The definitive guide, covering the
#[test]attribute and basic test structure. - The Rust Book: Checking Results with the
assert!Macro Family: A closer look atassert!,assert_eq!, andassert_ne!. - Arrange, Act, Assert - A Pattern for Writing Good Tests: A classic blog post explaining this universal and highly effective pattern for structuring your test functions.
Expand the Rust Unit Test Suite for a Markdown Parser
Mục tiêu: Add new, focused unit tests to a Rust-based Markdown parser. This task involves creating separate tests for paragraphs, bold text, italics, and links, following the Arrange-Act-Assert pattern to ensure the parser’s core features are working correctly.
Excellent work writing your first unit test! You have successfully created a test case for H1 headings, establishing a solid pattern of Arrange, Act, and Assert. This single test already provides immense value by acting as a permanent, automated check on your parser’s H1 logic.
Now, we will expand this safety net to cover the other features you’ve meticulously built. We’ll apply the exact same pattern to create separate, focused tests for paragraphs, bold text, italics, and links. Each new test function will be a small, independent expert at verifying one specific piece of your parser’s functionality.
Expanding the Test Suite: One Feature at a Time
By creating many small, focused test functions instead of one giant one, your test suite becomes easier to read and maintain. If a test fails, the function’s name (test_parse_italic, for example) immediately tells you which feature is broken, drastically speeding up the debugging process.
Let’s add the new tests to your tests module at the bottom of src/parser.rs.
Testing a Simple Paragraph
First, let’s verify the “default” case of your block-level parser: a line that isn’t a heading should become a paragraph.
// In src/parser.rs, inside `mod tests`
// ... test_parse_h1() is above this ...
#[test]
fn test_parse_paragraph() {
// Arrange: A simple line of text that is not a heading.
let markdown_input = "This is a standard paragraph.";
// Arrange: The expected output, wrapped in <p> tags with a trailing newline.
let expected_html = "<p>This is a standard paragraph.</p>\\n";
// Act & Assert: Call the parse function and compare its output to our expectation.
assert_eq!(parse(markdown_input), expected_html);
}
This test targets the else branch in your main parse loop, ensuring that plain text is correctly identified and wrapped.
Testing Bold Text
Now, let’s test your inline parser. This test is crucial because it verifies that the block-level logic (creating a <p> tag) and the first layer of the inline logic (creating <strong> tags) are working together correctly.
// In src/parser.rs, inside `mod tests`
// ... test_parse_paragraph() is above this ...
#[test]
fn test_parse_bold() {
// Arrange: A line containing bold Markdown syntax.
let markdown_input = "Some **bold** text.";
// Arrange: The expected HTML. Note that the parser first wraps the line in <p> tags,
// and the inline parser handles the `**...**` within that line.
let expected_html = "<p>Some <strong>bold</strong> text.</p>\\n";
// Act & Assert
assert_eq!(parse(markdown_input), expected_html);
}
Testing Italic Text
Similarly, we’ll add a test for italics. This verifies the second stage of your inline processing pipeline.
// In src/parser.rs, inside `mod tests`
// ... test_parse_bold() is above this ...
#[test]
fn test_parse_italic() {
// Arrange: A line containing italic Markdown syntax.
let markdown_input = "This is *very* important.";
// Arrange: The expected HTML with `<em>` (emphasis) tags.
let expected_html = "<p>This is <em>very</em> important.</p>\\n";
// Act & Assert
assert_eq!(parse(markdown_input), expected_html);
}
Testing Links
Finally, let’s write a test for the most complex piece of your parser: the link-parsing logic. A passing test here gives you high confidence that your stateful scanning algorithm is working as intended.
// In src/parser.rs, inside `mod tests`
// ... test_parse_italic() is above this ...
#[test]
fn test_parse_link() {
// Arrange: A line containing the Markdown link syntax.
let markdown_input = "Visit [the official website](https://rust-lang.org).";
// Arrange: The expected HTML with a correctly formed `<a>` (anchor) tag.
let expected_html = "<p>Visit <a href=\"https://rust-lang.org\">the official website</a>.</p>\\n";
// Act & Assert
assert_eq!(parse(markdown_input), expected_html);
}
The Complete Test Module
Your tests module in src/parser.rs should now look like this, containing a suite of five focused tests that cover the primary features of your parser.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_h1() {
let markdown_input = "# Hello";
let expected_html = "<h1>Hello</h1>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_paragraph() {
let markdown_input = "This is a standard paragraph.";
let expected_html = "<p>This is a standard paragraph.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_bold() {
let markdown_input = "Some **bold** text.";
let expected_html = "<p>Some <strong>bold</strong> text.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_italic() {
let markdown_input = "This is *very* important.";
let expected_html = "<p>This is <em>very</em> important.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_link() {
let markdown_input = "Visit [the official website](https://rust-lang.org).";
let expected_html = "<p>Visit <a href=\"https://rust-lang.org\">the official website</a>.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
}
Run Your Expanded Test Suite
Go to your terminal and run cargo test again. This time, the test runner will discover and execute all five of your test functions. The output should be a reassuring wall of green:
Compiling markdown_converter v0.1.0 (/path/to/markdown_converter)
Finished test [unoptimized + debuginfo] target(s) in 0.45s
Running unittests src/parser.rs (target/debug/deps/markdown_converter-...)
running 5 tests
test parser::tests::test_parse_bold ... ok
test parser::tests::test_parse_h1 ... ok
test parser::tests::test_parse_italic ... ok
test parser::tests::test_parse_link ... ok
test parser::tests::test_parse_paragraph ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
You have successfully created a comprehensive test suite for the “happy path” scenarios of your parser. You can now refactor your parsing logic or add new features with a high degree of confidence, knowing that if you accidentally break any of these core functionalities, your tests will immediately fail and alert you to the problem.
Next Steps
Your current tests are excellent for verifying correct behavior with well-formed input. However, what happens when the input is tricky or malformed? For example, what should your parser do with an empty input string, or a line with mixed and nested inline elements? In the next task, you will expand your suite to cover these important edge cases, making your parser even more robust and reliable.
Further Reading
To learn more about structuring tests and the philosophy behind testing, explore these excellent resources:
- Test-Driven Development (TDD): A software development process where you write the tests before you write the implementation code. This can help drive the design of your software. Martin Fowler provides a classic introduction.
- The Rust Book: Controlling How Tests Are Run: Learn how to run specific tests by name, run ignored tests, and more.
- Unit Tests vs. Integration Tests: Understand the difference between testing a small piece of code in isolation (what you’ve just done) and testing how multiple parts of your application work together.
Implement Edge Case Tests for a Rust Markdown Parser
Mục tiêu: Expand the test suite for a Rust Markdown parser by adding new tests to cover important edge cases. This includes testing empty input, mixed inline elements (bold, italic, link), and nested elements to ensure the parser is robust and handles complex scenarios gracefully.
You have done an absolutely outstanding job. By creating separate, focused tests for each feature, you’ve built a robust safety net for all the “happy path” scenarios. Your test suite now acts as a living document of your parser’s capabilities, and cargo test has become a powerful command to instantly verify that everything is working as intended.
Now, let’s take your testing skills to the next level by considering the real world, which is often messy and unpredictable. A great piece of software doesn’t just work with perfect input; it behaves gracefully and predictably when faced with unusual, malformed, or tricky input. These scenarios are often called edge cases, and testing for them is what separates a good program from a truly robust and reliable one.
Pushing the Boundaries: Testing Edge Cases
An edge case is a problem or situation that occurs only at an extreme (maximum or minimum) operating parameter. For our parser, this includes: * Empty input: What happens if the user tries to convert an empty file? * Mixed elements: How does the parser handle a single line with bold, italics, and a link? * Malformed syntax: What if a user starts a link with [ but never closes it? Does the program crash or handle it gracefully?
Your current test suite gives you confidence that your parser works. This next set of tests will give you confidence that your parser doesn’t break.
Let’s add a few more test functions to your tests module in src/parser.rs to cover these important scenarios.
Testing Empty Input
This is the most fundamental edge case. A program should not panic or fail when given nothing to do. Your parser should simply take an empty string and produce an empty string.
// In src/parser.rs, inside `mod tests`
// ... test_parse_link() is above this ...
#[test]
fn test_parse_empty_input() {
// Arrange: An empty string slice as input.
let markdown_input = "";
// Arrange: The expected output is also an empty string.
let expected_html = "";
// Act & Assert: Verify that parsing an empty string results in an empty string.
// This confirms the parser doesn't add any tags or newlines unnecessarily.
assert_eq!(parse(markdown_input), expected_html);
}
Testing Mixed Inline Elements
Your layered parsing pipeline was designed to handle this, but a dedicated test makes this guarantee explicit. This test will confirm that a line of text can pass through all three stages of your inline parser (bold, italic, link) and emerge correctly formatted.
// In src/parser.rs, inside `mod tests`
// ... test_parse_empty_input() is above this ...
#[test]
fn test_mixed_inline_elements() {
// Arrange: A complex line with bold, italic, and a link.
let markdown_input = "A line with **bold**, *italic*, and a [link](https://example.com).";
// Arrange: The expected HTML, showing all transformations applied correctly.
let expected_html = "<p>A line with <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>\\n";
// Act & Assert: This test validates the successful composition of all your
// inline parsing rules.
assert_eq!(parse(markdown_input), expected_html);
}
Testing Nested Inline Elements (within a link)
This is another great test of your pipeline’s robustness. Can you put italic text inside the text of a link? Let’s verify.
// In src/parser.rs, inside `mod tests`
// ... test_mixed_inline_elements() is above this ...
#[test]
fn test_nested_inline_elements() {
// Arrange: Markdown where the link text itself contains an italic element.
let markdown_input = "A link with [*italic text*](https://example.com).";
// Arrange: The expected HTML. The italic pass runs *before* the link pass,
// so the `[<em>italic text</em>](...)` is what the link parser sees and processes.
let expected_html = "<p>A link with <a href=\"https://example.com\"><em>italic text</em></a>.</p>\\n";
// Act & Assert: This confirms the ordering of your parsing stages is correct
// and produces the desired nested output.
assert_eq!(parse(markdown_input), expected_html);
}
This test works beautifully because of the pipeline you built:
- The bold pass leaves the string unchanged.
- The italic pass transforms it to
A link with [<em>italic text</em>](https://example.com). - The link pass then correctly identifies
<em>italic text</em>as the link’s text content.
Your Final, Robust Test Suite
With these additions, your test suite is now significantly more powerful. It covers not only the standard use cases but also the tricky edge cases that can often lead to bugs. Here is your complete tests module in src/parser.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_h1() {
let markdown_input = "# Hello";
let expected_html = "<h1>Hello</h1>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_paragraph() {
let markdown_input = "This is a standard paragraph.";
let expected_html = "<p>This is a standard paragraph.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_bold() {
let markdown_input = "Some **bold** text.";
let expected_html = "<p>Some <strong>bold</strong> text.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_italic() {
let markdown_input = "This is *very* important.";
let expected_html = "<p>This is <em>very</em> important.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_link() {
let markdown_input = "Visit [the official website](https://rust-lang.org).";
let expected_html = "<p>Visit <a href=\"https://rust-lang.org\">the official website</a>.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_parse_empty_input() {
let markdown_input = "";
let expected_html = "";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_mixed_inline_elements() {
let markdown_input = "A line with **bold**, *italic*, and a [link](https://example.com).";
let expected_html = "<p>A line with <strong>bold</strong>, <em>italic</em>, and a <a href=\"https://example.com\">link</a>.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
#[test]
fn test_nested_inline_elements() {
let markdown_input = "A link with [*italic text*](https://example.com).";
let expected_html = "<p>A link with <a href=\"https://example.com\"><em>italic text</em></a>.</p>\n";
assert_eq!(parse(markdown_input), expected_html);
}
}
Run cargo test one more time. You should see all 8 tests pass, giving you a tremendous amount of confidence in the code you’ve written.
Next Steps
You have successfully completed a crucial step in becoming a proficient software engineer. Your parser is not only feature-rich but also robust and protected by a comprehensive suite of automated tests. This well-tested foundation is now ready to be built upon.
While your generated HTML is structurally perfect, it probably looks quite plain in a web browser. In the next step of the project, you will address this by enhancing the visual appeal of the output. You will learn how to embed a default CSS stylesheet directly into the generated HTML file’s <head> section, instantly giving your converted documents a clean, modern, and professional look.
Further Reading
Testing is a deep and fascinating field. To continue your journey, you might want to explore more advanced testing concepts.
- Code Coverage: This is a metric that measures what percentage of your codebase is executed by your tests. Tools like
cargo-tarpaulincan help you find parts of your code that are not covered by any tests. - Property-Based Testing: Instead of writing individual examples, you define properties that your code should always satisfy (e.g., “for any string
s,parse(s)should never panic”). A library then generates hundreds of random inputs to try and find a counter-example. This is excellent for finding edge cases you didn’t think of. - The Test Data Builder Pattern: For more complex tests that require setting up intricate data structures, this pattern can make your tests much cleaner and more readable.
Run and Verify the Complete Rust Test Suite
Mục tiêu: Execute the complete test suite for the Rust parser using cargo test. Learn to interpret the output for both passing and intentionally failing tests to understand the power of an automated safety net.
You have reached a pivotal moment in your project. After meticulously crafting tests for the “happy paths” and pushing the boundaries with challenging edge cases, your test suite is now a powerful guardian of your parser’s logic. You’ve written the specifications, and now it’s time to run the final audit and witness the full power of Rust’s built-in testing framework.
This final task is about running the complete suite and understanding the feedback it provides. It’s the moment where all your hard work in this step culminates in a single, powerful command.
The Moment of Truth: Running Your Complete Test Suite
With your src/parser.rs file now containing a comprehensive collection of test cases—from simple headings to complex, nested inline elements—it’s time to execute them all at once.
Open your terminal in the root directory of your markdown_converter project and run the definitive command for testing in Rust:
cargo test
Cargo will spring into action. It will first compile your project, including the #[cfg(test)] module you’ve built. Then, it will execute every function marked with the #[test] attribute. If all your tests are correctly written, you should be greeted with a reassuring wall of green text, signifying success:
Compiling markdown_converter v0.1.0 (/path/to/markdown_converter)
Finished test [unoptimized + debuginfo] target(s) in 0.52s
Running unittests src/parser.rs (target/debug/deps/markdown_converter-...)
running 8 tests
test parser::tests::test_parse_bold ... ok
test parser::tests::test_parse_empty_input ... ok
test parser::tests::test_parse_h1 ... ok
test parser::tests::test_parse_italic ... ok
test parser::tests::test_parse_link ... ok
test parser::tests::test_mixed_inline_elements ... ok
test parser::tests::test_nested_inline_elements ... ok
test parser::tests::test_parse_paragraph ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Deconstructing the Success Report
This output is more than just a simple “pass”; it’s a detailed report card for your code. Let’s break it down:
running 8 tests: The test runner has discovered and is executing all eight test functions you’ve written insrc/parser.rs.test parser::tests::... ... ok: Each line represents one test function. Theokstatus confirms that the function completed without panicking, meaning all theassert_eq!macros within it found that the actual output matched the expected output.test result: ok. 8 passed; 0 failed; ...: This is the final summary. It gives you the high-level overview: a perfect score. You have successfully verified every piece of functionality you’ve built so far.
The True Value: Seeing a Test Fail
The real power of a test suite isn’t just seeing it pass; it’s seeing it fail when it’s supposed to. A failing test is not a problem—it’s your safety net working perfectly, catching a bug before it reaches your users.
Let’s simulate this. Go into src/parser.rs and intentionally introduce a bug. Find the line in your main parse function that handles H1 headings and change it slightly.
// In `pub fn parse` in src/parser.rs
// ...
} else if line.starts_with("# ") {
let raw_content = line.strip_prefix("# ").unwrap();
let processed_content = parse_inline_elements(raw_content);
// INTRODUCE A BUG: Change h1 to h1_BROKEN
let h1_tag = format!("<h1_BROKEN>{}</h1_BROKEN>\\n", processed_content);
html_output.push_str(&h1_tag);
// ...
Now, run cargo test again. This time, the output will be dramatically different and incredibly valuable:
running 8 tests
test parser::tests::test_parse_bold ... ok
test parser::tests::test_parse_empty_input ... ok
test parser::tests::test_parse_h1 ... FAILED
test parser::tests::test_parse_italic ... ok
# ... other tests pass ...
failures:
---- parser::tests::test_parse_h1 stdout ----
thread 'parser::tests::test_parse_h1' panicked at 'assertion failed: `(left == right)`
left: `<h1_BROKEN>Hello</h1_BROKEN>\n`,
right: `<h1>Hello</h1>\n`', src/parser.rs:205:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
parser::tests::test_parse_h1
test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Look at the failure report. It tells you:
- Exactly which test failed:
parser::tests::test_parse_h1. - Why it failed: The
assert_eq!panicked. - A perfect
diff: It shows you theleft(actual) value and theright(expected) value, highlighting the exact difference. You can see<h1_BROKEN>where<h1>was expected.
This is the power of your test suite. It’s a precise, automated tool that pinpoints regressions instantly.
Now, be sure to undo the change in src/parser.rs and run cargo test one last time to confirm everything is green again.
You have now completed a masterclass in unit testing. You’ve not only written tests but have also built a deep, practical understanding of why they are one of the most important tools in a software engineer’s toolkit.
Next Steps
Your parser is feature-complete, robust, and now guarded by a comprehensive suite of automated tests. This solid, reliable foundation is ready to be built upon.
The generated HTML, while structurally perfect, is visually plain. The next major step is to elevate the aesthetics of your output. You will learn how to create a simple CSS stylesheet and, using a powerful Rust macro, embed it directly into the <head> of your generated HTML files. This will instantly give your converted documents a clean, modern, and professional appearance, turning your powerful tool into one that produces beautiful results.
Further Reading
To become even more proficient with Cargo’s testing capabilities, explore these resources:
- The Rust Book: Controlling How Tests Are Run: Learn how to run a single test by name (
cargo test test_name), filter tests, and run ignored tests. - Showing Function Output: If you add
println!statements to your code or tests for debugging, they are hidden by default when tests pass. Learn how to show them. - Continuous Integration (CI): The automated test suite you’ve built is the foundation of a practice called CI, where tests are automatically run on a server every time new code is pushed. This is a standard practice in professional software development.