🧪 Tester view

Untested behaviours, edge cases, test gap inventory  ·  QA Engineer, SDET, Test Lead

Generated 4 days ago
⚡ Agent trace  ·  13 steps  ·  $0.0681  ·  click to expand
search_claims(query='test coverage', category='any')search_claims(query='error exception throw', category='behavioral')search_claims(query='edge case boundary limit', category='any')search_claims(query='null', category='behavioral')search_claims(query='validate check precondition', category='behavioral')search_claims(query='state transition side effect', category='behavioral')search_claims(query='parse format print', category='behavioral')search_claims(query='arithmetic plus minus multiply divide', category='behavioral')search_claims(query='plus minus multiplied divided negated', category='behavioral')search_claims(query='currency unit', category='behavioral')get_file_claims(file_path='src/main/java/org/joda/money/Money.java')get_file_claims(file_path='src/main/java/org/joda/money/BigMoney.java')get_file_claims(file_path='src/main/java/org/joda/money/format/MoneyFormatter.java')read_source_file(file_path='src/main/java/org/joda/money/Money.java', start_line=226)read_source_file(file_path='src/main/java/org/joda/money/Money.java', start_line=390)read_source_file(file_path='src/main/java/org/joda/money/Money.java', start_line=820)read_source_file(file_path='src/main/java/org/joda/money/Money.java', start_line=1050)read_source_file(file_path='src/main/java/org/joda/money/Money.java', start_line=1160)read_source_file(file_path='src/main/java/org/joda/money/Money.java', start_line=1310)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatter.java', start_line=170)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyAmountStyle.java', start_line=460)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatterBuilder.java', start_line=85)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatter.java', start_line=229)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatterBuilder.java', start_line=150)read_source_file(file_path='src/main/java/org/joda/money/format/MoneyFormatterBuilder.java', start_line=280)get_file_claims(file_path='src/test/java/org/joda/money/TestMoney.java')get_file_claims(file_path='src/test/java/org/joda/money/TestBigMoney.java')get_file_claims(file_path='src/test/java/org/joda/money/format/TestMoneyFormatter.java')read_source_file(file_path='src/main/java/org/joda/money/MoneyUtils.java', start_line=50)read_source_file(file_path='src/main/java/org/joda/money/format/SignedPrinterParser.java', start_line=1)get_file_claims(file_path='src/main/java/org/joda/money/format/MoneyAmountStyle.java')read_source_file(file_path='src/main/java/org/joda/money/format/SignedPrinterParser.java', start_line=60)get_file_claims(file_path='src/main/java/org/joda/money/format/AmountPrinterParser.java')get_file_claims(file_path='src/test/java/org/joda/money/format/TestMoneyFormatterBuilder.java')

Test Intelligence Report

High-Risk Untested Behaviours

1. MoneyUtils null-handling semantics are untested (HIGH)

File: src/main/java/org/joda/money/MoneyUtils.java:53-103

MoneyUtils.isZero(), isPositiveOrZero(), and isNegativeOrZero() all treat null as a valid value (returning true for zero checks). isPositive() and isNegative() return false for null. This is a deliberate design choice that silently accepts null — but there are no test claims for these utility methods at all. A consumer calling isPositive() on a null reference expecting an exception will silently get false, which could mask bugs.

Risk: HIGH — silent null acceptance in a utility class with no test coverage.

2. MoneyUtils.max(), min(), add(), subtract() null-handling is untested (HIGH)

File: src/main/java/org/joda/money/MoneyUtils.java:204-255

These methods accept null for either argument and return the non-null argument, or null if both are null. For example, max(null, someMoney) returns someMoney. subtract(null, someMoney) returns someMoney.negated(). These are non-obvious semantics with no test claims found.

Risk: HIGH — arithmetic utility methods with null-tolerant semantics that could silently produce wrong results.

3. MoneyFormatter.parse(text, startIndex) with partial parsing is untested (HIGH)

File: src/main/java/org/joda/money/format/MoneyFormatter.java:277-288

The low-level parse(CharSequence, int) method returns a MoneyParseContext that may be in an error state, partially parsed, or incomplete. The test suite only tests the high-level parseBigMoney() and parseMoney() methods. There are no tests that call parse(text, startIndex) directly and inspect the returned context for error state, partial parsing, or index position.

Risk: HIGH — the low-level API is the foundation for all parsing but has no direct test coverage.

4. MoneyFormatter.print(Appendable, BigMoneyProvider) IOException wrapping is untested (MEDIUM)

File: src/main/java/org/joda/money/format/MoneyFormatter.java:183-189

The print() method catches IOException from printIO() and wraps it in MoneyFormatException. The test file TestMoneyFormatter.java:536-539 defines an IOAppendable class that always throws IOException, but there is no test claim that actually uses it to verify the wrapping behavior.

Risk: MEDIUM — the IOException-to-MoneyFormatException conversion path is defined but not exercised.

5. MoneyFormatterBuilder.appendLiteral(null) silently returns this (MEDIUM)

File: src/main/java/org/joda/money/format/MoneyFormatterBuilder.java:156-162

When appendLiteral is called with null or empty string, it silently returns this without appending anything. There is no test that verifies this silent-no-op behavior for null input.

Risk: MEDIUM — silent failure could mask programming errors where a null literal is passed unintentionally.


Edge Case Inventory

1. Zero-amount optimizations in BigMoney

File: src/main/java/org/joda/money/BigMoney.java:247-249

Multiple arithmetic methods (plus, minus, plusMajor, minusMajor, plusMinor, minusMinor, negated, multipliedBy) return this when the operand is zero or the multiplier is one. This is an optimization that changes object identity semantics — callers holding a reference to the original object will see the same instance returned.

2. Android zero-double workaround

File: src/main/java/org/joda/money/BigMoney.java:262-267

BigMoney.of(CurrencyUnit, double) returns zero for a zero double value to avoid a bug in stripTrailingZeros() on Android before v30. This is a platform-specific workaround with no test coverage for the Android-specific path.

3. BigMoney.equals() considers scale

File: src/main/java/org/joda/money/BigMoney.java:1695-1705

BigMoney.equals() compares both currency and amount including scale, so USD 30.00 and USD 30 are not equal. This differs from compareTo() which ignores scale. The Money.equals() delegates to BigMoney.equals(), inheriting this behavior. The Money Javadoc claims scale is taken into account, but the actual behavior depends on the underlying BigMoney implementation.

4. MoneyAmountStyle getters return null for negative internal values

File: src/main/java/org/joda/money/format/MoneyAmountStyle.java:305-307, 464-466, 502-504

When internal integer fields are negative (sentinel for "not set"), getter methods return null instead of the actual value. This affects getGroupingSize(), getExtendedGroupingSize(), and character getters.

5. MoneyAmountStyle.equals() excludes extendedGroupingSize

File: src/main/java/org/joda/money/format/MoneyAmountStyle.java:626-644

The equals() method compares all fields except extendedGroupingSize. Two styles that differ only in extended grouping size will be considered equal but may have different hashCode() values (since hashCode() also excludes it). This is a potential contract violation.

6. SignedPrinterParser.parse() edge case: non-zero amount parsed by zero formatter

File: src/main/java/org/joda/money/format/SignedPrinterParser.java:86-89

When the zero formatter matches but the parsed amount is non-zero, the amount is forced to zero (context.setAmount(BigDecimal.ZERO)). This is tested in TestMoneyFormatterBuilder.java:839 but is a subtle behavior that could surprise users.

7. MoneyFormatterBuilder.appendCurrencySymbolLocalized() creates printer-only formatter

File: src/main/java/org/joda/money/format/MoneyFormatterBuilder.java:143-145

Appending a localized currency symbol appends null as the parser, making the resulting formatter unable to parse currency symbols. This is documented but easy to miss.


Error Handling Gaps

1. MoneyFormatter.parse(text, startIndex)StringIndexOutOfBoundsException vs IndexOutOfBoundsException

File: src/main/java/org/joda/money/format/MoneyFormatter.java:279-280

The Javadoc says IndexOutOfBoundsException, but the implementation throws StringIndexOutOfBoundsException. While StringIndexOutOfBoundsException extends IndexOutOfBoundsException, consumers catching the documented type will work, but those specifically checking for StringIndexOutOfBoundsException may have brittle tests.

2. AmountPrinterParser.parse() — silent error on malformed input

File: src/main/java/org/joda/money/format/AmountPrinterParser.java:129-178

When parsing fails (e.g., first character is not a sign, digit, or decimal point), the method calls context.setError() and returns. The error is silent — no exception is thrown at the parse level. The error is only surfaced later when parseBigMoney() checks result.isError(). This means partial parsing can proceed with an error state that may not be checked.

3. AmountPrinterParser.parse() — grouping character handling

File: src/main/java/org/joda/money/format/AmountPrinterParser.java:129-178

If the last character parsed is a grouping character, the position is decremented to exclude it. However, consecutive grouping characters are skipped silently. This means "1,,234" would parse as "1234" without error — a potential data integrity issue.

4. MoneyFormatter.print(Appendable, BigMoneyProvider) — IOException swallowed

File: src/main/java/org/joda/money/format/MoneyFormatter.java:183-189

The IOException is caught and wrapped in MoneyFormatException, but the original exception is preserved as the cause. If the Appendable throws IOException for a transient reason (e.g., network failure on a Writer), the caller gets a MoneyFormatException which may not be the right abstraction for IO failures.

5. Money.total(Money...) — null element handling

File: src/main/java/org/joda/money/Money.java:245-256

The method checks monies.length == 0 and throws IllegalArgumentException, but null elements within the array are only caught when total.plus(monies[i]) is called, which will throw NullPointerException from BigMoney.plus(). The error message is not user-friendly.


Suggested Test Scenarios

Scenario 1: MoneyUtils.isZero(null) returns true

Given a null BigMoneyProvider
When MoneyUtils.isZero(null) is called
Then it returns true
File: src/main/java/org/joda/money/MoneyUtils.java:53-55

Scenario 2: MoneyUtils.max(null, someMoney) returns the non-null argument

Given a null Money and a non-null Money with value GBP 10.00
When MoneyUtils.max(null, gbp10) is called
Then it returns gbp10
File: src/main/java/org/joda/money/MoneyUtils.java:204-212

Scenario 3: MoneyUtils.subtract(null, someMoney) returns negated value

Given a null Money and a non-null Money with value GBP 10.00
When MoneyUtils.subtract(null, gbp10) is called
Then it returns gbp10.negated() (GBP -10.00)
File: src/main/java/org/joda/money/MoneyUtils.java:225-233

Scenario 4: MoneyFormatter.parse("GBP 123.45", 0) returns valid context

Given a formatter with currency code and amount
When formatter.parse("GBP 123.45", 0) is called
Then the returned MoneyParseContext has isError() == false, isFullyParsed() == true, and toBigMoney() equals GBP 123.45
File: src/main/java/org/joda/money/format/MoneyFormatter.java:277-288

Scenario 5: MoneyFormatter.parse("GBP 123.45 extra", 0) returns partially parsed context

Given a formatter with currency code and amount
When formatter.parse("GBP 123.45 extra", 0) is called
Then the returned MoneyParseContext has isError() == false but isFullyParsed() == false and getIndex() points to the start of " extra"
File: src/main/java/org/joda/money/format/MoneyFormatter.java:277-288

Scenario 6: BigMoney.of(CurrencyUnit, 0.0d) returns zero on Android-like platforms

Given a currency unit (e.g., GBP) and a double value of 0.0
When BigMoney.of(GBP, 0.0d) is called
Then it returns a BigMoney with amount zero and scale matching the currency's decimal places
File: src/main/java/org/joda/money/BigMoney.java:262-267

Scenario 7: MoneyFormatter.print() wraps IOException from custom Appendable

Given a formatter that can print and an Appendable that always throws IOException
When formatter.print(brokenAppendable, gbpMoney) is called
Then it throws MoneyFormatException with the original IOException as the cause
File: src/main/java/org/joda/money/format/MoneyFormatter.java:183-189

Scenario 8: AmountPrinterParser.parse() with consecutive grouping characters

Given a formatter with grouping style and a text "1,,234"
When the formatter parses the text
Then it should either reject the input (set error) or parse it as "1234" — the current behavior is to silently skip consecutive grouping characters
File: src/main/java/org/joda/money/format/AmountPrinterParser.java:129-178

Scenario 9: Money.equals() with same numeric value but different scale

Given Money.of(GBP, new BigDecimal("10.00")) and Money.of(GBP, new BigDecimal("10.0"))
When equals() is called
Then it returns false because the underlying BigMoney considers scale
File: src/main/java/org/joda/money/Money.java:1314-1323, BigMoney.java:1695-1705

Scenario 10: MoneyAmountStyle with only extendedGroupingSize differing

Given two MoneyAmountStyle instances that differ only in extendedGroupingSize
When equals() is called
Then it returns true (field is excluded from equality check)
File: src/main/java/org/joda/money/format/MoneyAmountStyle.java:626-644


Complex Code Paths

1. SignedPrinterParser.parse() — 3-way branching with state merge

File: src/main/java/org/joda/money/format/SignedPrinterParser.java:61-94

This method creates three child parse contexts (positive, zero, negative), parses each, then selects the "best" result based on longest match. It then applies post-processing: - If zero formatter matched but amount is non-zero → force to zero (line 87-88) - If negative formatter matched but amount is positive → negate the amount (line 90-91)

Cyclomatic complexity: ~12 (3 child contexts × error check + best selection + 2 post-processing conditions)

Risk: HIGH — the interaction between formatters and the forced-zero/negate logic is subtle and has only one edge-case test.

2. AmountPrinterParser.print() — localized digit shifting and grouping

File: src/main/java/org/joda/money/format/AmountPrinterParser.java:46-112

This method: 1. Localizes the style per locale 2. Handles negative sign and absolute value 3. Shifts digit characters for non-'0' zero characters 4. Inserts grouping characters using isPreGroupingPoint and isPostGroupingPoint 5. Handles forced decimal point

Cyclomatic complexity: ~15 (sign handling + zero char check + grouping style switch + pre/post grouping logic)

Risk: HIGH — the digit shifting and grouping logic has many edge cases (extended grouping, Indian numbering system, forced decimal point).

3. AmountPrinterParser.parse() — character-by-character parsing with state

File: src/main/java/org/joda/money/format/AmountPrinterParser.java:129-178

This method: 1. Handles first character specially (sign, digit, or decimal point) 2. Loops through subsequent characters converting digits, handling decimal point (once only), skipping grouping characters 3. Decrements position if last character was a grouping character 4. Converts parsed buffer to BigDecimal

Cyclomatic complexity: ~10 (first char branching + loop with multiple conditions + trailing grouping adjustment)

Risk: MEDIUM — the grouping character skipping logic could accept malformed input.

4. MoneyFormatter.parseBigMoney() — three error conditions

File: src/main/java/org/joda/money/format/MoneyFormatter.java:229-243

After parsing, this method checks three distinct error conditions: 1. result.isError() → parse error at index 2. !result.isFullyParsed() → trailing unparsed text 3. !result.isComplete() → missing currency or amount

Each produces a different error message. The third condition (!isComplete()) is only reachable if the parser returns success without finding both currency and amount — a state that may be impossible with standard formatters but could occur with custom parsers.

Risk: MEDIUM — the third error path may be dead code or untested with custom parsers.