🧪 Tester view
Untested behaviours, edge cases, test gap inventory · QA Engineer, SDET, Test Lead
⚡ 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.