Project 4

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-based assertEquals correctly.
  • 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

  1. 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 * 4 returns 14, not 20).
    • At least 2 tests of parentheses (e.g., (2 + 3) * 4 returns 20).
    • At least 2 tests of associativity (e.g., 10 - 3 - 2 returns 5, not 9).
    • 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 throw IllegalArgumentException).
    • At least 1 test of whitespace handling (e.g., " 2 + 3 " returns 5).
    • At least 1 test of null input (must throw NullPointerException).
  2. Calculator.java — your implementation, written after the test file exists. Must pass all tests in CalculatorTest.java.

  3. CalculatorDemo.java — a small main that runs at least 5 sample expressions and prints results. The grader uses this for a quick sanity check.

  4. notes.txt with:

    • 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:

  1. File timestamps. CalculatorTest.java must show a creation date earlier than Calculator.java. (If you copy-pasted both at the same moment, the grader will know.)

  2. The “first failing run” log. Once your tests are written but Calculator.java is empty or stubbed, run the test class and save the output to a file called first-run.log. This proves the tests ran red before they ran green. Include first-run.log in your submission.

  3. 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)

CriterionPoints
CalculatorTest.java contains at least 12 tests10
Tests cover precedence, parens, associativity, division by zero, malformed input, whitespace, null15
Every test uses Arrange / Act / Assert structure clearly10
Tests use the correct assertion idioms (assertEquals, assertThrows)10
Calculator.java passes all tests20
first-run.log and final-run.log are included and tell a real story10
File timestamps show test-first ordering10
notes.txt includes test list, honest process notes, and reflection10
CalculatorDemo.java runs and prints results for at least 5 expressions5

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.2 is approximately 0.3 — use a tolerance!).
  • At least 1 test on integer-as-float operands ("3 / 2" returns 1.5, not 1).
  • At least 1 test on operator precedence with floats.
  • At least 1 test on division by zero (decide: does 1.0 / 0.0 return Double.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) and 2 * -3 (result: -6). The leading - parses as negation, not subtraction.
  • Exponentiation. A new operator ^. 2 ^ 3 returns 8. Higher precedence than * and /. Right-associative: 2 ^ 3 ^ 2 returns 2 ^ (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 (not 25).
  • 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:

  1. Identify the uncovered lines, write tests that exercise them, and re-run.
  2. Identify lines that can’t be reached (e.g., a default else branch that the type system prevents), and document them in notes.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.java
  • Calculator.java
  • CalculatorDemo.java
  • first-run.log
  • final-run.log
  • notes.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:

  1. Read notes.txt.
  2. Check file timestamps.
  3. Read both log files.
  4. Run CalculatorTest.java and confirm it’s green.
  5. Run CalculatorDemo.java for a quick sanity check.
  6. (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") returns 5. The test is assertEquals(5, c.evaluate("2 + 3")). The fact that you don’t yet know how evaluate works 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 / 0 already throws ArithmeticException. 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 one assertEquals. 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

  1. Run CalculatorTest.java. Confirm green.
  2. Run CalculatorDemo.java with a handful of inputs of your choosing (try one weird one).
  3. Re-read CalculatorTest.java end to end. Does each test name make sense? Does each body read cleanly?
  4. Re-read Calculator.java. Is there any line that no test would catch breaking? If so, add a test.
  5. Re-read notes.txt. Is the reflection honest and specific?
  6. Submit.
  7. 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.