From 17af7a323c64d7a8b852fc80e392e00724fe655b Mon Sep 17 00:00:00 2001 From: datpp Date: Fri, 9 Oct 2026 15:25:50 +0700 Subject: [PATCH 1/2] test: add white-box unit tests for Luhn.luhnCheck Cover the loop (zero, one and many iterations), both outcomes of the `i % 2 == 0` and `temp > 9` decisions, boundary values (8 / 10 after doubling), valid and invalid checksums, input immutability and null input. --- .../com/thealgorithms/others/LuhnTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/test/java/com/thealgorithms/others/LuhnTest.java diff --git a/src/test/java/com/thealgorithms/others/LuhnTest.java b/src/test/java/com/thealgorithms/others/LuhnTest.java new file mode 100644 index 000000000000..0d6c19d664b4 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/LuhnTest.java @@ -0,0 +1,111 @@ +package com.thealgorithms.others; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * White-box tests for {@link Luhn#luhnCheck(int[])}. + * + *

The method has one loop and two decisions: + *

+ * The tests below cover the loop executed zero, one and many times, both + * outcomes of each decision, and both outcomes of the final {@code sum % 10 == 0}. + */ +class LuhnTest { + + @Test + void testEmptyArraySkipsLoopAndIsValid() { + // Loop body never executes, sum stays 0 + assertTrue(Luhn.luhnCheck(new int[] {})); + } + + @Test + void testSingleZeroDigitIsValid() { + // One iteration: D1 true, D2 false (0 * 2 = 0) + assertTrue(Luhn.luhnCheck(new int[] {0})); + } + + @Test + void testSingleDigitDoubledWithoutSubtractionIsInvalid() { + // One iteration: D1 true, D2 false (4 * 2 = 8, upper boundary of temp <= 9) + assertFalse(Luhn.luhnCheck(new int[] {4})); + } + + @Test + void testSingleDigitDoubledWithSubtractionIsInvalid() { + // One iteration: D1 true, D2 true (5 * 2 = 10, lower boundary of temp > 9 -> 1) + assertFalse(Luhn.luhnCheck(new int[] {5})); + } + + @Test + void testTwoDigitsWithoutSubtraction() { + // i = 1: D1 false -> 2; i = 0: D1 true, D2 false -> 8; sum = 10 + assertTrue(Luhn.luhnCheck(new int[] {4, 2})); + } + + @Test + void testTwoDigitsWithSubtraction() { + // i = 1: D1 false -> 9; i = 0: D1 true, D2 true -> 10 - 9 = 1; sum = 10 + assertTrue(Luhn.luhnCheck(new int[] {5, 9})); + } + + @Test + void testTwoDigitsInvalidChecksum() { + // i = 1: D1 false -> 3; i = 0: D1 true, D2 false -> 8; sum = 11 + assertFalse(Luhn.luhnCheck(new int[] {4, 3})); + } + + @Test + void testMaxDigitDoubled() { + // i = 1: D1 false -> 0; i = 0: D1 true, D2 true -> 18 - 9 = 9; sum = 9 + assertFalse(Luhn.luhnCheck(new int[] {9, 0})); + // i = 1: D1 false -> 1; i = 0: D1 true, D2 true -> 9; sum = 10 + assertTrue(Luhn.luhnCheck(new int[] {9, 1})); + } + + @Test + void testValidCardNumbers() { + // Many iterations, all branch combinations exercised + assertTrue(Luhn.luhnCheck(new int[] {4, 5, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 7})); + assertTrue(Luhn.luhnCheck(new int[] {5, 2, 6, 5, 9, 2, 5, 1, 6, 1, 5, 1, 1, 4, 1, 2})); + assertTrue(Luhn.luhnCheck(new int[] {4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})); + } + + @Test + void testCardNumberWithTypoInLastDigitIsInvalid() { + assertFalse(Luhn.luhnCheck(new int[] {4, 5, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 4})); + } + + @Test + void testCardNumberWithSingleDigitErrorIsInvalid() { + // Error in a doubled position (index 0) + assertFalse(Luhn.luhnCheck(new int[] {3, 5, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 7})); + // Error in a non-doubled position (index 1) + assertFalse(Luhn.luhnCheck(new int[] {4, 6, 6, 1, 2, 6, 1, 2, 1, 2, 3, 4, 5, 4, 6, 7})); + } + + @Test + void testAllZerosIsValid() { + assertTrue(Luhn.luhnCheck(new int[16])); + } + + @Test + void testInputArrayIsNotModified() { + int[] digits = {5, 2, 6, 5, 9, 2, 5, 1, 6, 1, 5, 1, 1, 4, 1, 2}; + int[] copy = digits.clone(); + Luhn.luhnCheck(digits); + assertArrayEquals(copy, digits); + } + + @Test + void testNullInputThrowsNullPointerException() { + assertThrows(NullPointerException.class, () -> Luhn.luhnCheck(null)); + } +} From 75af89e546dad6b9a8f26d05ddec8f24fb994c71 Mon Sep 17 00:00:00 2001 From: datpp Date: Fri, 9 Oct 2026 15:38:30 +0700 Subject: [PATCH 2/2] test: drop null input case flagged by Infer Infer reports NULLPTR_DEREFERENCE for passing null to luhnCheck on purpose. The method does not document null handling, so the case is removed. --- src/test/java/com/thealgorithms/others/LuhnTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/com/thealgorithms/others/LuhnTest.java b/src/test/java/com/thealgorithms/others/LuhnTest.java index 0d6c19d664b4..2e2c294a3352 100644 --- a/src/test/java/com/thealgorithms/others/LuhnTest.java +++ b/src/test/java/com/thealgorithms/others/LuhnTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -103,9 +102,4 @@ void testInputArrayIsNotModified() { Luhn.luhnCheck(digits); assertArrayEquals(copy, digits); } - - @Test - void testNullInputThrowsNullPointerException() { - assertThrows(NullPointerException.class, () -> Luhn.luhnCheck(null)); - } }