Comprehensive Unit Testing Principles and JUnit Implementation and JUnit Strategies

Overview of Unit Testing Principles and Strategies

  • Definition of Unit Testing: Unit tests focus on one very small, isolated unit of code at a time to verify that a particular segment is working correctly.

  • The Objective of Testing: The primary goal is to find bugs. However, it is fundamentally impossible to exhaustively test every single possibility or input combination.

  • Coverage Metrics:

    • Statement Coverage: Ensuring that every line of code in a method is executed by the test suite. This helps ensure bugs do not escape simply because a section of code was never triggered.

    • Branch Coverage: Testing all possible outcomes of decision points (if-statements).

    • Condition Coverage: Testing that each individual condition within a branch returns both true and false.

  • Decision Tables: These help identify different possible conditions during execution and map them to their expected outputs, providing a structured way to design test cases.

Core Testing Focus Areas

  • Main Functionality: Focus on the obvious, "happy path" behavior.

    • Example: In a calculator, test that 2+2=42 + 2 = 4.

    • Passing these tests provides confidence that the software is usable for the majority of standard use cases.

  • Edge Cases: Investigating behavior at extreme boundaries where crashes are more likely.

    • Metaphor: In a linked list, an edge case is an empty list (head=nullhead = null). A remove method should be tested here to ensure it does not crash.

  • Boundary Testing: Pushing inputs to the limits of what the system can handle.

    • Example: In a calculator, testing the addition of max_int+max_int\text{max\_int} + \text{max\_int}.

    • Possible outcomes: The value might rollover (overflow), throw a computation error, or behave unexpectedly if it cannot fit into the allocated memory.

  • Exceptions and Error Handling: Testing that the code handles invalid input gracefully.

    • Example: If nullnull is passed as an input, the system should catch the error and provide a meaningful error message rather than crashing with a raw NullPointerException or NullReferenceException.

  • Exhaustive Spectrum Testing:

    • Usually impossible for primitive types like int, float, or string due to the infinite range of possibilities.

    • Possible for enums (e.g., a suit enum with 4 values: Hearts, Diamonds, Clubs, Spades). Every possibility can and should be tested.

The Arrange-Act-Assert (AAA) Pattern

  • Arrange: Set up the environment and create the necessary class instances.

    • Example: new IntLinkedList() or new Calculator().

  • Act: Execute the specific function or method you wish to test.

    • Example: Adding two numbers or inserting a node into a list.

  • Assert: Verify that the actual result matches the expected outcome.

    • If they do not match, the test fails, providing an error message to help track down the bug.

  • Isolation: Tests should remain isolated with as few moving parts as possible to prevent one bug in one unit from masking or complicating a bug in another.

Hazard Analysis in Testing

  • Definition: Examining the design to identify potential harms if something goes wrong.

  • Prioritization: Test cases should be prioritized based on hazards. This involves designing the program to avoid harm and specifically testing to ensure dangerous states cannot be reached.

Limitations of Unit Testing

  • Non-Functional Requirements: Unit tests cannot measure system performance, speed, responsiveness, or UI "feel."

  • Environment Stability: They do not test if the software runs smoothly across different operating systems (Linux vs. Windows) or mobile hardware.

  • Integration Issues: Unit tests do not verify that different units work correctly when combined; this requires Integration Testing.

  • Configuration and Syntax: Unit tests cannot catch missing libraries (e.g., JUnit not being installed) or syntactic errors like missing brackets. These are typically handled by compilers and IDEs.

Case Study: Live Coding an IntLinkedList Test Suite

  • The Target Code: A basic linked list storing integer data with methods: add(int x), hasValue(int x), isEmpty(), length(), and print().

  • Testing the add(int x) Method:

    • Code Path Analysis: The add method may have only one physical path (no loops or if-statements). However, its behavior changes based on the state of the head variable (which may be nullnull or point to an existing list).

    • Instructional Note: Using global variables (like head) makes testing harder because it complicates the analysis of variable states between lines, especially in parallel execution environments.

  • Dealing with Dependencies:

    • To test add, we often must use other methods like hasValue or print to verify the item was indeed added.

    • Isolation Strategy: Use the least complex/volatile dependency. print is considered volatile because changing the formatting (e.g., changing a - > arrow to a Unicode arrow) will break all tests relying on string comparison.

    • Robustness: If a test fails, it might be the add method or the dependency (e.g., hasValue) that is broken. Multiple tests using different dependencies help narrow this down.

Technical Implementation with JUnit

  • Decorations: Use the @Test annotation to mark a method as a test case.

  • Capturing Terminal Output:

    • Unit tests do not have direct access to the console.

    • Use an outputStreamCaptor to redirect system output to a string for assertion.

    • Use a beforeEach function to set up the captor before every test and a teardown method to clean up afterward.

  • Compiling and Running via Command Line:

    • Requires the JUnit standalone launcher (e.g., junit-platform-console-standalone-1.12.0.jar).

    • Compile: javac -cp ".;junit-standalone.jar" IntLinkedList*.java.

    • Execute: java -jar junit-standalone.jar --class-path . --select-class IntLinkedListTest.

  • Naming Conventions: Use descriptive names (e.g., addTestMultipleWithLength, testAddOnEmptyList). The @DisplayName or descriptive method names help identify failures.

  • Assertions with Custom Messages:

    • Syntax: assertEquals(expected, actual, "Custom Error Message").

    • Including data like the list.length() in the message helps debug failures faster.

Advanced Assignment Testing Examples

  • Nested Tests: Grouping tests by the state of the object (e.g., tests for an Empty Tree, Tree with One Node, Tree with Multiple Nodes).

  • Testing equals(): When overriding equals(Object o), tests must ensure the method doesn't crash when compared against a different type (e.g., comparing a Card object to a String).

  • Tree Height Logic: Noted discrepancy in height definitions (00 vs 11 for a single node). Testing should be consistent (e.g., checking that height doesn't change after an unsuccessful removal rather than asserting an absolute value).

  • Redundancy Avoidance: Do not run the exact same test logic under two different names; each test should provide unique insight or confidence.

Questions & Discussion

  • Student Question: Are there any questions about the IntLinkedList before moving on?

  • Professor Response: Briefly mentioned that Test 1 marking is complete and available via Moodle or at the FG Link reception. A warning was given regarding Question 2 of the test: many students provided Linked List code for a question specifically about Sets code.