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 .
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 (). A
removemethod 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 .
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 is passed as an input, the system should catch the error and provide a meaningful error message rather than crashing with a raw
NullPointerExceptionorNullReferenceException.
Exhaustive Spectrum Testing:
Usually impossible for primitive types like
int,float, orstringdue 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()ornew 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(), andprint().Testing the
add(int x)Method:Code Path Analysis: The
addmethod may have only one physical path (no loops or if-statements). However, its behavior changes based on the state of theheadvariable (which may be 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 likehasValueorprintto verify the item was indeed added.Isolation Strategy: Use the least complex/volatile dependency.
printis 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
addmethod 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
@Testannotation to mark a method as a test case.Capturing Terminal Output:
Unit tests do not have direct access to the console.
Use an
outputStreamCaptorto redirect system output to a string for assertion.Use a
beforeEachfunction 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@DisplayNameor 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 overridingequals(Object o), tests must ensure the method doesn't crash when compared against a different type (e.g., comparing aCardobject to aString).Tree Height Logic: Noted discrepancy in height definitions ( vs 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
IntLinkedListbefore 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.