[learning cs] testing
Testing: Lessons from Building a Resume Generator1
Through the process of building an HTML resume generator, I learned firsthand why multiple levels of testing are essential.
TLDR: Proper testing is important and complex. From deciding what to test, how to test, what to test again, and when to test, etc., TDD is a important tool that is both challenging to orchestrate and rewarding. Needless to say, I'm curious how large projects coordinate testing and iteration.
The Four Pillars of Testing
1. Unit Testing: Testing Individual Components
Unit tests verify that individual functions work correctly in isolation. They're fast, focused, and catch bugs early in the development process.
Example from the resume generator:
- Testing
extract_email()
to ensure it correctly parses email addresses - Testing
extract_courses()
to handle whitespace variations - Testing
build_complete_html_string()
for exact HTML structure
What I learned:
Unit tests can give you false confidence. In my project, I had a unit test called test_build_complete_html_exact_structure
that used hardcoded HTML strings. It passed and was certainly useful—but it was testing in a vacuum.
The hidden danger: My unit test used hardcoded HTML while my actual application used builder functions. The unit test missed that email addresses were being obfuscated from user@domain.com
to user[aT]domain.com
by the builders. This formatting change was intentional in the real code but absent in my unit test's expectations. And this makes sense, my extractor's role is not to format and validate.
Key takeaway: Unit tests are excellent for catching logic errors and edge cases, but they can miss integration issues when they don't use the same code paths as production. The expectations from separation of concerns need to be tested a various levels.
2. Integration Testing: Testing Component Interactions
Integration tests verify that different parts of your system work together correctly. They catch bugs that only appear when components interact.
Example from the resume generator:
The test_build_complete_html_integration_with_section_builders
test used actual section builder functions (build_basic_info_section
, build_projects_section
, build_courses_section
) and then called build_complete_html_string()
to assemble the complete page.
What this caught:
This integration test immediately failed because it revealed the email obfuscation issue that the unit test missed. The test expected user@domain.com
but got user[aT]domain.com
because it was actually calling the section builders that performed this transformation.
Additional integration discoveries:
- Whitespace handling: The
extract_courses()
function worked with simple spaces but failed when the resume file contained tabs - Multi-dot domains: Email extraction broke on domains like
seas.upenn.edu
because the code naively took only the second segment after@
Key takeaway: Integration tests reveal the messy reality of how your components actually interact. They catch the bugs that happen when real data flows through multiple functions.
3. Full Pipeline Testing: Testing the Complete System
Full pipeline tests (also called end-to-end tests) verify that the entire system works from start to finish, just as a user would experience it.
Example from the resume generator:
The main()
function provided a complete pipeline test:
- Read a resume text file
- Extract all information (name, email, courses, projects)
- Build HTML sections
- Assemble complete HTML
- Write to an output file
- Verify the file renders correctly in a browser
What this caught:
Running the full pipeline through various "real world" test cases in the TestResumes
directory exposed issues that neither unit nor integration tests caught:
Self-closing tag syntax: Template used
charset=utf-8" />
with a space, tests expectedcharset=utf-8"/>
without. This caught that my Ruff linter reformatted some lines when I made a change to a template file. It took me some sleuthing to figure out where in the chain the issue was.Name extraction false positives: Failed when a valid name line contained the word "courses" in parentheses, e.g., "John Doe (courses coordinator)." "courses" was a delimiter for an extraction logic, but it also happened to be in a spot in an actual test file that tripped it up.
4. Manual Check in Browser
- CSS path issues: HTML files in nested directories (
TestResumes/subdirectory/
) needed../../style.css
instead ofstyle.css
Best Practices Learned.
1. Keep Accumulating Tests (Regression Testing)
As I fixed bugs, I kept previous full pipeline test cases running. This caught regressions—when a fix for one test case broke a previously passing test.
2. Test Data Matters
- Create test cases that cover edge cases
- Real-world formats
- Static tests is just a small cut out view, a minor subset of a large universe
The Bottom Line
I enjoyed TDD because it's one to think through a function first before just madly typing out code.
Building the resume generator taught me that testing is not optional, and one level is not enough:
- Unit tests give you confidence in your logic and catch obvious bugs quickly
- Integration tests reveal how your components actually interact and catch subtle transformation bugs
- Full pipeline tests ensure your system works as a whole and catches configuration, file I/O, and real-world usage issues
Each level complements the others.
I gave Claude Sonnet 4.5 a draft of the write-up and asked for it convert it to Markdown format and for suggestions to connect my thoughts. Then I edited Claude's feedback.↩