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);
}@Testannotation 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 String | Expected Output | Reason |
|---|---|---|
"hello" | false | Not a palindrome |
"level" | true | Reads same forwards and backwards |
"a" | true | Single character is palindrome |
"" (empty) | true | Empty 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 Method | Purpose |
|---|---|
assertEquals | Checks if expected value equals actual value |
assertNotEquals | Checks 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 --> DSetup and Teardown Methods
Use annotations to run code before and after each test method:
| Annotation | Purpose |
|---|---|
@Before | Runs setup code before each test |
@After | Runs 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 --> TBest Practices for Unit Testing
| Practice | Explanation |
|---|---|
| Clear test method names | Include input and expected output in method names for readability |
| Follow Arrange-Act-Assert | Organize test logic clearly in three steps |
| Use assertions properly | Choose matching assert methods to verify outputs |
| Cover edge cases | Test empty strings, single characters, and typical cases |
| Maintain high code coverage | Add tests to cover all lines of the function |
| Use setup/teardown wisely | Avoid repeating initialization code |
| Use parameterized tests | Simplify testing multiple input/output pairs |
Summary Table: Unit Testing Elements for Palindrome Function
| Element | Description | Example/Usage |
|---|---|---|
| Palindrome Function | Checks if input string reads same forwards & backwards | isPalindrome("level") → true |
@Test Annotation | Marks method as a test case | @Test public void testMethod() {} |
| Arrange-Act-Assert | Three-step testing pattern | Arrange inputs, Act by calling function, Assert expected results |
| Assertions | Verify output correctness | assertEquals(expected, actual) |
| Code Coverage | Percentage of code executed by tests | Aim for 70%-80%+ for good coverage |
@Before / @After | Setup and cleanup methods around each test | Initialize objects, reset state |
| Parameterized Tests | Run same test logic on multiple input/output pairs | Use @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.