Unit Testing in Java
Master unit testing concepts with visual diagrams covering mock objects, testing frameworks (Mockito, PowerMock), BDD, and test-driven development. Complete guide with testing flows and patterns.
Q1: Why Use Mock Objects in Unit Testing?
graph TB
UT[Unit Testing] --> Problem[Testing Challenges]
Problem --> DB[Database Dependencies]
Problem --> File[File System Access]
Problem --> Network[Network Calls]
Problem --> External[External APIs]
Solution[Mock Objects] --> Benefits[Key Benefits]
Benefits --> B1[Test in Isolation]
Benefits --> B2[Control Boundaries]
Benefits --> B3[No State Dependencies]
Benefits --> B4[Fast Execution]
style Solution fill:#4CAF50
Key Points:
- Mock objects simulate real dependencies without requiring actual infrastructure (databases, file systems, APIs)
- Enable true unit testing by testing only the code under test, not its collaborators
- Allow easy control of boundary conditions (null values, empty results, error scenarios)
- Tests run independently in any order because mocks eliminate shared state
- No I/O operations means tests execute in milliseconds instead of seconds
- Balance is crucial—over-mocking makes tests brittle and hard to understand
- Mock external boundaries (databases, APIs) but use real objects for internal logic
Q2: Mock Objects Complete Flow
sequenceDiagram
participant Test as Unit Test
participant Mock as Mock Framework
participant ClassUT as Class Under Test
participant Real as Real Dependency
Note over Test,Real: Setup Phase
Test->>Mock: Create Mock/Spy
Test->>Mock: Define Behavior
Note over Test,Real: Execution Phase
Test->>ClassUT: Call Method
ClassUT->>Mock: Call Dependency
Mock-->>ClassUT: Return Mock Data
ClassUT-->>Test: Return Result
Note over Test,Real: Verification Phase
Test->>Mock: Verify Interactions
Test->>Test: Assert Results
Three-Phase Testing Workflow:
- Setup Phase: Create mock objects and define behavior using
when(mock.method()).thenReturn(value) - Execution Phase: Call the method under test, which internally calls the mocked dependency
- Mock intercepts the call and returns pre-defined value—real dependency never touched
- Verification Phase: Verify results (correct return value?) and interactions (method called? how many times?)
- This pattern ensures comprehensive testing: control inputs, execute logic, verify outputs and behavior
- Real file system, database, or API is never accessed, making tests fast and reliable
- Example:
verify(mockService, times(1)).getUser()confirms method was called exactly once
Q3: Mock vs Stub vs Spy
graph TB
Testing[Testing Doubles] --> Mock[Mock Object]
Testing --> Stub[Stub Object]
Testing --> Spy[Spy Object]
Mock --> MF[Verifies Interactions<br/>Returns Values<br/>Behavior Testing]
Stub --> SF[Returns Values<br/>No Verification<br/>State Testing]
Spy --> SPF[Partial Mocking<br/>Real + Overrides<br/>Legacy Code]
style Mock fill:#2196F3
style Stub fill:#4CAF50
style Spy fill:#FF9900
Understanding Testing Doubles:
- Mock: Verifies behavior—tracks method calls and asserts "was this called exactly once?"
- Used for behavior-driven testing where you care about interactions
- Stub: Provides canned responses but doesn't verify anything
- Used for state-based testing where you only care about final result, not how it was achieved
- Spy: Partial mock that wraps real objects, overriding specific methods while keeping others real
- Useful for legacy code where you can't easily inject dependencies
- Interview Tip: Mocks verify behavior (how many times?), stubs just return values
- Choose based on what you're testing: interactions (mock) or state (stub)
Q4: Mocking Frameworks Comparison
graph TB
Frameworks[Java Mocking] --> Mockito[Mockito]
Frameworks --> PowerMock[PowerMock]
Frameworks --> EasyMock[EasyMock]
Mockito --> M[Most Popular<br/>Clean Syntax<br/>Standard Mocking]
PowerMock --> P[Extends Mockito<br/>Mock Static/Final<br/>Mock Private]
EasyMock --> E[Record-Replay<br/>Explicit Expectations]
style Mockito fill:#4CAF50
style PowerMock fill:#FF9900
Framework Comparison:
- Mockito: Industry standard with clean syntax:
when(service.getUser()).thenReturn(user) - Handles most testing scenarios, integrates seamlessly with JUnit
- Natural language verification:
verify(service, times(1)).getUser() - PowerMock: Extends Mockito to handle advanced scenarios Mockito can't
- Mocks static methods, constructors, final classes, and private methods
- Use sparingly—frequent need indicates design issues (tight coupling)
- EasyMock: Uses record-replay model with explicit expectations before execution
- More verbose syntax compared to Mockito
- Interview Strategy: Demonstrate Mockito knowledge first, mention PowerMock for edge cases
- Explain that needing PowerMock often signals design problems worth refactoring
Q5: BDD (Behavior-Driven Development)
graph TB
BDD[BDD] --> Focus[Two Perspectives]
Focus --> Business[Business View<br/>User Stories]
Focus --> Technical[Technical View<br/>Implementation]
Business --> Story[Given-When-Then]
Story --> Given[Given: Context]
Story --> When[When: Action]
Story --> Then[Then: Outcome]
Compare[BDD vs TDD]
Compare --> TDD[TDD: How Code Works]
Compare --> BDDView[BDD: How App Behaves]
style BDD fill:#4CAF50
BDD Key Concepts:
- Bridges gap between business requirements and technical implementation
- Uses common language both business and developers understand
- Given-When-Then Format: Creates executable specifications
- Given: Establishes initial context (user logged in, database has 5 records)
- When: Describes the action (user clicks submit, API receives request)
- Then: Specifies expected outcome (order created, email sent)
- Format is readable by non-technical stakeholders, serves as living documentation
- BDD tests are typically higher-level than unit tests, often covering integration scenarios
- Tools: JBehave and Cucumber allow writing tests in plain English that map to code
- Interview Point: BDD improves collaboration by creating shared understanding through executable specs
Q6: Testing Pyramid
graph TB
Pyramid[Testing Pyramid] --> Levels[Test Distribution]
subgraph Distribution[Recommended Distribution]
UI[UI Tests: 10%<br/>Slow, Expensive]
Integration[Integration: 20%<br/>Moderate Speed]
Unit[Unit Tests: 70%<br/>Fast, Cheap]
end
Unit --> U[Many Tests<br/>Fast Feedback<br/>Mock Dependencies]
Integration --> I[Test Interactions<br/>Real Components]
UI --> UI2[Critical Paths<br/>End-to-End]
style Unit fill:#4CAF50
style Integration fill:#FF9900
style UI fill:#2196F3
Pyramid Distribution:
- Unit Tests (70%): Base of pyramid—fast, cheap to write and maintain, immediate feedback
- Test individual components in isolation using mocks
- Integration Tests (20%): Verify components work together with real dependencies
- Slower but catch issues unit tests miss (database queries, API contracts)
- UI/E2E Tests (10%): Validate critical user journeys through entire system
- Slowest and most brittle—focus on happy paths and critical business flows
- Anti-Pattern: Inverted pyramid (mostly UI tests) leads to slow, flaky test suites
- Pyramid shape reflects both quantity and speed: more tests at bottom (fast), fewer at top (slow)
- Unit tests catch most bugs cheaply, higher-level tests verify integration and UX
Q7: TDD Red-Green-Refactor Cycle
graph LR
Red[🔴 Red<br/>Write Failing Test] --> Green[🟢 Green<br/>Make Test Pass]
Green --> Refactor[🔵 Refactor<br/>Improve Code]
Refactor --> Red
Red --> R1[Define Behavior]
Green --> G1[Minimum Code]
Refactor --> RF1[Clean Up]
style Red fill:#FFCDD2
style Green fill:#C8E6C9
style Refactor fill:#BBDEFB
TDD Three-Step Cycle:
- Red: Write failing test first, defining expected behavior before implementation
- Forces you to think about API and requirements upfront
- Test must fail initially to prove it's actually testing something
- Green: Write minimum code necessary to make test pass
- Don't worry about perfection—just make it work
- Validates your understanding of the requirement
- Refactor: Improve code quality while keeping tests green
- Remove duplication, improve names, extract methods
- Tests act as safety net, ensuring refactoring doesn't break functionality
- Benefits: Better design (testable code is well-designed), comprehensive coverage, confidence in refactoring
- Interview Point: TDD is about design, not just testing—tests drive API design
Q8: Complete Testing Workflow
graph TB
Start[Development] --> TDD{Use TDD?}
TDD -->|Yes| WriteTest[Write Test First]
TDD -->|No| WriteCode[Write Code]
WriteTest --> RunTest[Run Test - Fails]
RunTest --> Implement[Implement]
WriteCode --> WriteUnit[Write Unit Tests]
WriteUnit --> Mock[Use Mocks]
Implement --> TestPass{Pass?}
Mock --> TestPass
TestPass -->|No| Debug[Debug]
Debug --> TestPass
TestPass -->|Yes| Refactor[Refactor]
Refactor --> Coverage{Good Coverage?}
Coverage -->|No| WriteTest
Coverage -->|Yes| Deploy[Deploy]
style WriteTest fill:#4CAF50
style Deploy fill:#2196F3
Two Paths to Quality:
- TDD Path: Write test before implementation, ensuring every line has purpose and test
- Test fails initially (red), implement just enough to pass (green), then refactor
- Traditional Path: Write code first, then add tests to validate existing design
- Both paths converge at same quality standards: comprehensive tests, good coverage, passing tests
- Key Difference: TDD drives design through tests, traditional validates existing design
- Quality Standards: Tests must be independent (run in any order), fast (use mocks), comprehensive
- Cover positive cases, negative cases, and edge cases (null, empty, boundary values)
- Include coverage check—if coverage low, write more tests before deploying
- Interview Insight: Both approaches valid, but TDD prevents over-engineering by implementing only what tests require