Full Stack • Java • System Design • Cloud • AI Engineering

CodeQuality2026-06-17

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