Test-First Calculator
Apologetic question: "How do we know what is true?"
Project 4 — The Test-First Calculator
“If the tests come first, the code grows up surrounded by verification. If the tests come after, the code grows up surrounded by hope.”
Chapter: 4 — Testing as Discipline
Due: End of Week 4
Submit: A link to your work — an OnlineGDB project URL or a public GitHub repo URL — containing your test file, your implementation, and a notes.txt. The grader will check file timestamps to confirm test-first. See Coding 1’s online-coding workflow appendix for the submission workflow and Appendix B for the JUnit 5 setup.
Allowed tools: Pen and paper, the Java 17 API docs, the JUnit 5 API docs, this textbook.
Not yet allowed: AI assistance of any kind. Phase 1 is AI-off, and this is the project where the AI-off rule matters most: we are training the test-writing muscle that makes AI partnership possible in Phase 2.
The Setup
A working expression calculator is a small classic problem. The user enters something like (3 + 4) * 2 - 5, and the calculator returns 9. Operators: +, -, *, /. Parentheses for grouping. Standard operator precedence: * and / bind tighter than + and -. Left-to-right associativity within a precedence level.
The whole calculator fits in maybe 150-250 lines of Java. The interesting work is not the calculator. The interesting work is writing the tests for the calculator before writing the calculator.
The apologetic frame: how do we know what is true? This week, the question gets a small, executable answer: we know what is true about a piece of code because we checked it — with a test that we wrote on purpose, that runs automatically, that fails informatively when the code drifts. Tests are the engineering version of prove all things; hold fast that which is good.
Learning Targets
By completing this project, you will demonstrate that you can:
- Write a JUnit 5 test suite that covers the documented behavior of a class.
- Use
assertEquals,assertThrows, and tolerance-basedassertEqualscorrectly. - Apply the Arrange / Act / Assert pattern consistently.
- Write tests before the implementation they test, demonstrably (file timestamps).
- Use the test suite as the criterion for “done.”
- Write tests for both happy-path and error-path behavior.
- (Medium) Test floating-point arithmetic with appropriate tolerances.
- (Hard) Maintain 100% line coverage of production code, verified with a coverage tool.
If you can do those eight things by Friday, you’ve earned a Normal completion.
What You Will Build
A class called Calculator with one main public method:
/**
* Evaluates an arithmetic expression and returns the integer result.
*
* <p>The expression supports the binary operators {@code +}, {@code -},
* {@code *}, and {@code /} on integer operands, with standard precedence
* ({@code *} and {@code /} bind tighter than {@code +} and {@code -}) and
* left-to-right associativity. Parentheses {@code (} and {@code )} may
* be used to override precedence. Whitespace is ignored.
*
* @param expression the expression to evaluate; must not be null
* @return the integer result
* @throws NullPointerException if {@code expression} is null
* @throws IllegalArgumentException if {@code expression} is malformed
* (e.g., unmatched parens, unexpected
* characters, two operators in a row,
* or empty)
* @throws ArithmeticException if the expression involves division by zero
*/
public int evaluate(String expression) { /* ... */ }
You may add helper methods (tokenizer, parser, etc.) as you see fit — but they should be private. The public API is evaluate(String) and the constructor.
You may use any algorithm: recursive descent parser, shunting-yard, or a hand-rolled approach. Pick what reads cleanly.
Normal Tier
Goal: A working integer calculator backed by a test suite of at least 12 tests, written before the implementation.
Required deliverables
-
CalculatorTest.java— your test suite, written first. Must contain at least 12 tests covering:- At least 3 tests of operator precedence (e.g.,
2 + 3 * 4returns14, not20). - At least 2 tests of parentheses (e.g.,
(2 + 3) * 4returns20). - At least 2 tests of associativity (e.g.,
10 - 3 - 2returns5, not9). - At least 1 test of division by zero (must throw
ArithmeticException). - At least 2 tests of malformed input (e.g.,
"2 + + 3","(2 + 3"— must throwIllegalArgumentException). - At least 1 test of whitespace handling (e.g.,
" 2 + 3 "returns5). - At least 1 test of null input (must throw
NullPointerException).
- At least 3 tests of operator precedence (e.g.,
-
Calculator.java— your implementation, written after the test file exists. Must pass all tests inCalculatorTest.java. -
CalculatorDemo.java— a smallmainthat runs at least 5 sample expressions and prints results. The grader uses this for a quick sanity check. -
notes.txtwith:- A list of the 12+ tests with one-line descriptions.
- A “Process” paragraph describing how you actually worked. Did you write all tests first, then all code? Or did you go test-by-test, red-green-refactor style? Either is fine — be honest.
- One paragraph reflection on what writing tests first revealed about the design.
Test-first verification
This is the part most students try to fake. The grader will check:
-
File timestamps.
CalculatorTest.javamust show a creation date earlier thanCalculator.java. (If you copy-pasted both at the same moment, the grader will know.) -
The “first failing run” log. Once your tests are written but
Calculator.javais empty or stubbed, run the test class and save the output to a file calledfirst-run.log. This proves the tests ran red before they ran green. Includefirst-run.login your submission. -
The final passing run log. After implementation, save the output to
final-run.log. This proves the tests went green.
Both log files are graded artifacts. If you skip them, you lose the test-first credit (a substantial fraction of the rubric).
Coach’s Note — Yes, this is more bureaucratic than usual. The bureaucracy exists because TDD is uniquely easy to fake — anyone can write tests after the fact and claim they wrote them first. The file timestamps and the two logs are how the discipline gets checked. Welcome to verification.
Grading rubric — Normal (out of 100)
| Criterion | Points |
|---|---|
CalculatorTest.java contains at least 12 tests | 10 |
| Tests cover precedence, parens, associativity, division by zero, malformed input, whitespace, null | 15 |
| Every test uses Arrange / Act / Assert structure clearly | 10 |
Tests use the correct assertion idioms (assertEquals, assertThrows) | 10 |
Calculator.java passes all tests | 20 |
first-run.log and final-run.log are included and tell a real story | 10 |
| File timestamps show test-first ordering | 10 |
notes.txt includes test list, honest process notes, and reflection | 10 |
CalculatorDemo.java runs and prints results for at least 5 expressions | 5 |
Medium Tier (+up to 25% extra credit)
Layer the following on top of a complete Normal.
M1. Floating-Point Calculator
Add a second public method:
/**
* Evaluates an arithmetic expression on floating-point operands.
*
* <p>Same syntax and semantics as {@link #evaluate(String)}, but operands
* may be integers or decimals (e.g., {@code "3.14 * 2"}), and division
* returns a floating-point quotient.
*
* @param expression the expression; must not be null
* @return the double result
* @throws NullPointerException if {@code expression} is null
* @throws IllegalArgumentException if the expression is malformed
*/
public double evaluateDouble(String expression) { /* ... */ }
Add at least 6 tests in CalculatorTest.java covering:
- At least 2 tests on basic float arithmetic (e.g.,
0.1 + 0.2is approximately0.3— use a tolerance!). - At least 1 test on integer-as-float operands (
"3 / 2"returns1.5, not1). - At least 1 test on operator precedence with floats.
- At least 1 test on division by zero (decide: does
1.0 / 0.0returnDouble.POSITIVE_INFINITY? Throw? Document your choice in the Javadoc, then test it). - At least 1 test on a malformed float input (e.g.,
"1.2.3").
Every floating-point assertEquals must use the three-argument tolerance overload. Forgetting this is the most common Medium-tier failure.
M2. Express the Spec in Tests
Add a section to notes.txt called “Spec → Tests Mapping”. For each @throws claim and each operator precedence rule in the Javadoc of evaluate (and evaluateDouble, if you did M1), list which test in CalculatorTest.java verifies that claim.
If you find a claim with no test, write one and add it to the list. If you find a test with no corresponding spec claim, decide whether the test is verifying behavior that should be in the spec — and if so, add it to the Javadoc.
This is the bookkeeping move that turns “I wrote some tests” into “I have a verified specification.”
Hard Tier (+up to 25% additional extra credit)
Hard tier requires Normal + Medium done well.
H1. Unary Minus and Exponentiation
Extend the calculator to support:
- Unary minus. Expressions like
-5 + 3(result:-2) and2 * -3(result:-6). The leading-parses as negation, not subtraction. - Exponentiation. A new operator
^.2 ^ 3returns8. Higher precedence than*and/. Right-associative:2 ^ 3 ^ 2returns2 ^ (3 ^ 2) = 512, not(2 ^ 3) ^ 2 = 64.
Add at least 6 tests covering:
- Leading unary minus:
-5 + 3 == -2. - Unary minus after operator:
2 * -3 == -6. - Unary minus with parens:
-(2 + 3) == -5. - Basic exponentiation:
2 ^ 3 == 8. - Exponent precedence:
2 + 3 ^ 2 == 11(not25). - Right-associativity:
2 ^ 3 ^ 2 == 512.
Update the Javadoc on evaluate to document these new operators.
H2. 100% Coverage
Run a coverage tool against your test suite. The two free options that work for this course:
- JaCoCo (Java Code Coverage), the industry standard. Available as a command-line jar and as a Maven/Gradle plugin. Setup details in Appendix B.
- OpenClover is another option if JaCoCo gives you trouble.
Run the tool, and submit the coverage report as coverage-report.html (or the equivalent in your tool’s format).
Target: 100% line coverage of Calculator.java. Every executable line in your implementation must be exercised by at least one test.
If you can’t reach 100%, there are two honest moves:
- Identify the uncovered lines, write tests that exercise them, and re-run.
- Identify lines that can’t be reached (e.g., a default
elsebranch that the type system prevents), and document them innotes.txt.
The Hard-tier grading is about the process of pursuing coverage as much as the percentage. A 96% report with a thoughtful notes.txt paragraph explaining the missing 4% is graded above a 100% report with no reflection.
Submission
Submit one URL via the course portal:
- OnlineGDB project with all required files, or
- GitHub repo link with all required files.
What the linked project must contain
For Normal:
CalculatorTest.javaCalculator.javaCalculatorDemo.javafirst-run.logfinal-run.lognotes.txt
For Medium, the evaluateDouble tests are in CalculatorTest.java and the notes.txt has the Spec → Tests Mapping section.
For Hard, add:
- The unary-minus and exponent tests (in
CalculatorTest.java) coverage-report.html(or directory)
The grader will:
- Read
notes.txt. - Check file timestamps.
- Read both log files.
- Run
CalculatorTest.javaand confirm it’s green. - Run
CalculatorDemo.javafor a quick sanity check. - (Hard) Open the coverage report.
Hints
-
“How do I write tests first when I don’t even know what the code looks like?” You don’t need the code’s shape to write the test — you need the spec. The spec says
evaluate("2 + 3")returns5. The test isassertEquals(5, c.evaluate("2 + 3")). The fact that you don’t yet know howevaluateworks is irrelevant; the test is about the contract. -
“My calculator passes the 12 tests but fails on some other input.” That’s a sign your test suite has gaps. Add tests for the failing input. The expansion of the test suite is the project — you don’t ship and then add tests; you add tests as you discover gaps and re-ship.
-
“My parser keeps getting more complicated.” It’s supposed to. Expression parsing is one of the classic introductory parsing problems. Two clean approaches: (1) shunting-yard to RPN, then evaluate the RPN; (2) recursive descent with one method per precedence level (
parseExpression,parseTerm,parseFactor). Either works. Pick one, commit, write tests for each step. -
“I have a divide-by-zero test, but my code crashes instead of throwing the right exception.” Java’s
int / 0already throwsArithmeticException. Your test should pass without you doing anything special. If it doesn’t, your code is catching the exception and rethrowing it as something else — find where. -
“My tests are giant — 50 lines each.” They shouldn’t be. A test is usually 3-5 lines. Most of the time the Arrange step is a single
new Calculator()line; the Act is one method call; the Assert is oneassertEquals. If a test is much longer, you’re testing too many things in one test. -
“How long should this take?” Normal: 6-10 hours (the parser is the work; the test suite is the discipline). Medium: add 2-4 hours. Hard: add 4-8 hours for unary minus, exponent, and coverage. If you’re past 20 hours total, message the instructor.
What Mastery Looks Like (Beyond the Rubric)
The rubric tells you what to do. Here’s what to aim for:
A great test suite reads like a specification you can execute. Every test name says what behavior is being checked. Every body is short, focused, and readable. A reader unfamiliar with Calculator could read just CalculatorTest.java and tell you what the calculator does.
A great Calculator.java looks small. Not minimal — small. Around 150-200 lines is the right range for the Normal tier; 250-300 with Medium and Hard. If your calculator is 500 lines, you’ve over-engineered. If it’s 50, you may have under-engineered.
A great notes.txt reflection identifies one specific thing the test-first discipline changed about your design. Generic reflections lose points. Specific ones earn full credit. Example of a great reflection: “Writing the malformed-input tests first made me realize I needed a separate validate(expression) helper before parse(expression), because trying to handle both inside the parser produced a mess.” Example of a bad reflection: “I learned that tests are important.”
A great Hard-tier coverage report shows you understood the report. Anyone can hit 100% by writing one trivial test per line. The interesting questions: which tests covered which lines? Which test was hardest to write? Which line was most fragile?
When You’re Done
- Run
CalculatorTest.java. Confirm green. - Run
CalculatorDemo.javawith a handful of inputs of your choosing (try one weird one). - Re-read
CalculatorTest.javaend to end. Does each test name make sense? Does each body read cleanly? - Re-read
Calculator.java. Is there any line that no test would catch breaking? If so, add a test. - Re-read
notes.txt. Is the reflection honest and specific? - Submit.
- Read Chapter 5. Next week is debugging — what to do when your tests fail on code you didn’t expect to fail.
Coach’s Note — Project 4 is the project most students underestimate. The calculator looks small. The discipline isn’t. The students who treat the test suite as the real deliverable — and the calculator as the consequence of writing the tests right — almost always cruise into the midterm. The students who write the calculator first and bolt on tests afterward almost always crash there. The pattern is so consistent I could predict it from the first commit. Don’t be the second kind.
See you on Monday.