Unit Test with Example

Unit Testing

Unit testing verifies whether a small piece of code (a unit) works correctly by testing it with multiple input values and checking the output.

Palindrome Function to Test

A palindrome function takes a string input and returns `true` if the string reads the same forwards and backwards; otherwise, it returns `false`.

Example:

  • Input: "level" → Output: true (palindrome)
  • Input: "hello" → Output: false (not palindrome)

Diagram — How the Palindrome Check Works

flowchart LR
    A["Input string\n(e.g. 'level')"] --> B["Reverse the string\n('level' → 'level')"]
    B --> C{"Original ==\nReversed?"}
    C -->|Yes| D["true — palindrome"]
    C -->|No| E["false — not a palindrome"]


Writing Unit Tests

Steps to Write a Unit Test

1. Define a test method with a special annotation @Test to indicate this method is a test case.
2. Inside the test method, follow the Arrange-Act-Assert (AAA) pattern:

  • Arrange: Set up any objects, inputs, or dependencies needed.
  • Act: Execute the functionality that needs testing.
  • Assert: Verify the output matches expected results.

Diagram — Arrange-Act-Assert (AAA) Pattern

flowchart LR
    A["Arrange\nSet up objects & inputs"] --> B["Act\nCall the function"]
    B --> C["Assert\nVerify actual == expected"]

Example Test Method Structure

@Test
public void testPalindrome() {
    // Arrange
    Helper helper = new Helper();

    // Act
    boolean result = helper.isPalindrome("hello");

    // Assert
    assertEquals(false, result);
}

  • @Test annotation marks the method as a test case.
  • assertEquals(expected, actual) checks if the function's output matches expected output.

Example — The isPalindrome unit under test

public class Helper {
    public boolean isPalindrome(String input) {
        String reversed = new StringBuilder(input).reverse().toString();
        return input.equals(reversed);
    }
}


Testing the Palindrome Function: Key Points

Input StringExpected OutputReason
"hello"falseNot a palindrome
"level"trueReads same forwards and backwards
"a"trueSingle character is palindrome
"" (empty)trueEmpty string considered palindrome

  • Run multiple test cases to ensure the function works correctly for different input types.
  • Naming test methods clearly helps understand which inputs and expected outputs are tested.

Diagram — Test Cases Covering Edge Cases

flowchart TD
    A["isPalindrome() test cases"] --> B["'hello' → false\ntypical non-palindrome"]
    A --> C["'level' → true\ntypical palindrome"]
    A --> D["'a' → true\nedge case: single char"]
    A --> E["'' → true\nedge case: empty string"]


Assertions Variations

Assertion MethodPurpose
assertEqualsChecks if expected value equals actual value
assertNotEqualsChecks if expected value is not equal to actual value

Use assertions to confirm the function's return matches the expected output for each input tested.

Example

@Test
public void testAssertionVariations() {
    Helper helper = new Helper();

    assertEquals(true, helper.isPalindrome("level"));      // expected == actual
    assertNotEquals(true, helper.isPalindrome("hello"));   // expected != actual
}


Running Tests and Interpreting Results

Tests are executed in the testing framework (e.g., JUnit).

  • Pass: Actual output matches expected output.
  • Fail: Actual output differs; indicates potential bug.
  • Test results show which inputs passed or failed.
  • Good test method names help quickly identify which input caused failure.


Code Coverage in Unit Testing

Code coverage measures how many lines of your code are executed by your tests.

  • Higher code coverage means more of your code is tested.
  • Aim for at least 70-80% coverage to ensure sufficient testing.
  • Coverage tools highlight:
  • Green lines: Covered by tests.
  • Red lines: Not covered, may need additional test cases.
  • Example: If coverage is 54%, more test cases are needed to cover missing lines.

Diagram — Code Coverage Concept

flowchart LR
    A["isPalindrome() source lines"] --> B["🟢 Covered lines\nexecuted by at least one test"]
    A --> C["🔴 Uncovered lines\nnot exercised by any test"]
    B --> D["Coverage % = Covered / Total lines"]
    C --> D


Setup and Teardown Methods

Use annotations to run code before and after each test method:

AnnotationPurpose
@BeforeRuns setup code before each test
@AfterRuns cleanup code after each test

Useful to initialize objects used in multiple tests or reset states.

Example

public class PalindromeTest {

    private Helper helper;

    @Before
    public void setUp() {
        helper = new Helper();   // runs before every @Test method
    }

    @Test
    public void testLevel() {
        assertEquals(true, helper.isPalindrome("level"));
    }

    @Test
    public void testHello() {
        assertEquals(false, helper.isPalindrome("hello"));
    }

    @After
    public void tearDown() {
        helper = null;   // runs after every @Test method
    }
}

Diagram — Test Lifecycle

flowchart TD
    A["@Before\nsetUp()"] --> B["@Test\ntestLevel()"]
    A --> C["@Test\ntestHello()"]
    B --> D["@After\ntearDown()"]
    C --> E["@After\ntearDown()"]


Parameterized Testing

Why Parameterized Tests?

  • Avoid duplicate code when testing the same logic with different inputs.
  • Run the same test logic multiple times with varying parameters.

How to Implement Parameterized Tests

1. Define a class to hold input and expected output as properties.

public class TestParams {
    String input;
    boolean expected;

    public TestParams(String input, boolean expected) {
        this.input = input;
        this.expected = expected;
    }
}

2. Use a special annotation like @RunWith(Parameterized.class) to run parameterized tests.
3. Provide a method returning a list of test parameters.

@Parameters
public static List<TestParams> data() {
    return Arrays.asList(
        new TestParams("hello", false),
        new TestParams("level", true),
        new TestParams("a", true)
    );
}

4. Write a test method that consumes these parameters.

@Test
public void testPalindromeParameterized() {
    Helper helper = new Helper();
    assertEquals(expected, helper.isPalindrome(input));
}

This reduces repeated code and improves test maintenance.

Diagram — Parameterized Test Execution

flowchart LR
    A["data()\nlist of TestParams"] --> B1["('hello', false)"]
    A --> B2["('level', true)"]
    A --> B3["('a', true)"]
    B1 --> T["testPalindromeParameterized()\nruns once per parameter set"]
    B2 --> T
    B3 --> T


Best Practices for Unit Testing

PracticeExplanation
Clear test method namesInclude input and expected output in method names for readability
Follow Arrange-Act-AssertOrganize test logic clearly in three steps
Use assertions properlyChoose matching assert methods to verify outputs
Cover edge casesTest empty strings, single characters, and typical cases
Maintain high code coverageAdd tests to cover all lines of the function
Use setup/teardown wiselyAvoid repeating initialization code
Use parameterized testsSimplify testing multiple input/output pairs


Summary Table: Unit Testing Elements for Palindrome Function

ElementDescriptionExample/Usage
Palindrome FunctionChecks if input string reads same forwards & backwardsisPalindrome("level") → true
@Test AnnotationMarks method as a test case@Test public void testMethod() {}
Arrange-Act-AssertThree-step testing patternArrange inputs, Act by calling function, Assert expected results
AssertionsVerify output correctnessassertEquals(expected, actual)
Code CoveragePercentage of code executed by testsAim for 70%-80%+ for good coverage
@Before / @AfterSetup and cleanup methods around each testInitialize objects, reset state
Parameterized TestsRun same test logic on multiple input/output pairsUse @RunWith(Parameterized.class)


The correctness of the palindrome function is verified by testing multiple inputs and expected outputs using the AAA pattern with proper annotations and assertions. Code coverage reports guide adding tests to cover all possible logic branches. Setup/teardown methods automate repetitive tasks before and after tests. Parameterized tests optimize multiple input testing, improving code clarity and maintenance.