Difference between Unit Testing and Integration Testing

Jump to

In software development, testing is the backbone of reliability—especially in enterprise and distributed domains like e-commerce, where a single bug can lead to cart abandonment or revenue loss. 

In this regard, differentiation between unit testing and integration testing, as a Software Engineer, is quintessential to devising an effective test strategy. While both tests are important, they serve different purposes and thus require different kinds of tools and approaches. 

In this guide, we’ll dissect these testing methodologies, illustrate their differences with Java examples, and explore libraries like JUnit, Mockito, and Spring Boot Test.

Types Of Testing:

We have overall 4 different major phases in testing:

  1. Unit Testing: Ensures individual components work correctly in isolation, catching logic errors early.
  2. Integration Testing: Verifies interactions between modules, detecting API or database integration issues.
  3. System Testing: Evaluates the entire system for functionality, performance, and usability gaps.
  4. UAT (User Acceptance Testing): Confirms the system meets business needs before final deployment.

What Is Unit Testing?

Unit testing refers to the validation of the smallest testable units of a system, such as methods or classes, in isolation from other components. The objective is to ensure each unit behaves predictably under different inputs, independently of external dependencies such as databases or APIs.

Example: Testing a Shopping Cart’s Price Calculation

Imagine an e-commerce ShoppingCartService that calculates the total price of items, applying discounts and taxes. Here’s how you’d unit-test it using JUnit 5 and Mockito to mock dependencies:

public class ShoppingCartServiceTest {
    @Mock
    private DiscountService discountService;
    @Mock
    private TaxCalculator taxCalculator;
   
    @InjectMocks
    private ShoppingCartService shoppingCartService;
   
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
   
    @Test
    void calculateTotalPrice_WithDiscountAndTax_ReturnsCorrectTotal() {
        // Arrange
        List<Item> items = List.of(new Item("Laptop", 1000.0));
        when(discountService.calculateDiscount(items)).thenReturn(100.0);
        when(taxCalculator.calculateTax(900.0)).thenReturn(90.0);
       
        // Act
        double total = shoppingCartService.calculateTotalPrice(items);
       
        // Assert
        assertEquals(990.0, total, "Total price calculation failed");
    }
}

This test class verifies ShoppingCartService using Mockito to mock dependencies (DiscountService and TaxCalculator).

Key Points:

  1. Mocks Setup:
    • @Mock creates mock objects.
    • @InjectMocks injects them into ShoppingCartService.
    • @BeforeEach initializes mocks.
  2. Test Logic (calculateTotalPrice):
    • A Laptop costs $1000.
    • Mocked discount: $100, leaving $900.
    • Mocked tax on $900: $90.
    • Expected total: $990.
  3. Assertion:
    • Ensures ShoppingCartService correctly computes total price.

This isolates business logic and avoids dependency on actual services. 

Key Tools:

  • JUnit 5: Framework for writing and running tests.
  • Mockito: Mocks external dependencies (e.g., DiscountService, TaxCalculator).

What Is Integration Testing?

Integration testing verifies interactions between multiple components or systems. It ensures modules like payment gateways, databases, or microservices work together seamlessly.

Example: Testing Checkout Process with Payment Gateway

Let’s test a CheckoutController that integrates PaymentService and OrderRepository. Using Spring Boot Test and TestContainers, we’ll validate the entire flow with a real database:

@SpringBootTest
@Testcontainers
public class CheckoutControllerIntegrationTest {
    @Autowired
    private CheckoutController checkoutController;
    
    @Container
    private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");
    
    @DynamicPropertySource
    static void configureDatabase(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Test
    void checkout_ValidOrder_CreatesOrderAndProcessesPayment() {
        // Arrange
        OrderRequest request = new OrderRequest("user123", 200.0, "CREDIT_CARD");
        
        // Act
        OrderResponse response = checkoutController.checkout(request);
        
        // Assert
        assertNotNull(response.getOrderId());
        assertEquals("PAID", response.getStatus());
    }
}

This integration test validates CheckoutController using Spring Boot, Testcontainers, and PostgreSQL.

Key Points:

  1. Setup:
    • @SpringBootTest loads the full Spring context.
    • @Testcontainers runs PostgreSQL in a container.
  2. Database Configuration:
    • @Container starts a PostgreSQL 13 instance.
    • @DynamicPropertySource dynamically sets DB properties.
  3. Test Logic (checkout method):
    • Creates an order request (user123, $200, CREDIT_CARD).
    • Calls checkoutController.checkout(request).
    • Asserts that an order is created and status is “PAID”.

This ensures the full checkout flow works correctly with a real database.

Key Tools:

  • Spring Boot Test: Bootstraps the application context for end-to-end testing.
  • TestContainers: Spins up real dependencies (e.g., PostgreSQL) in Docker containers.

Key Differences Between Unit and Integration Testing

AspectUnit TestingIntegration Testing
ScopeIsolated components (methods/classes).Interactions between components/systems.
SpeedFast (ms per test).Slower (seconds/minutes per test).
DependenciesMocked using tools like Mockito.Real or containerized dependencies.
ToolsJUnit, Mockito, TestNG.Spring Boot Test, TestContainers.
PurposeValidate logic correctness.Validate system behavior and flows.

Use Cases for Unit Testing

  1. Business Logic Validation
    • Example: Ensure a coupon applies a 20% discount only if the cart total exceeds $100.
  2. Edge Cases
    • Example: Verify tax calculation handles zero or negative values gracefully.
  3. Test-Driven Development (TDD)
    • Write tests before implementing features like inventory updates.

Use Cases for Integration Testing

  1. API Endpoints
    • Test if GET /products returns a paginated list correctly.
  2. Database Interactions
    • Validate that an order persists correctly with OrderRepository.
  3. Third-Party Services
    • Ensure payment processing via Stripe/PayPal works end-to-end.

Why Unit and Integration Tests Are Complementary

. Unit Tests → Catch Bugs Early

  • Test isolated components (e.g., a discount calculation method).
  • Run quickly and provide fast feedback on logic errors.
  • Example: Detecting a bug where a 10% discount is incorrectly applied as 1%.

Integration Tests → Validate System Behavior

  • Test how components interact (e.g., CheckoutController communicating with a payment gateway).
  • Catch real-world failures, like database misconfigurations or API timeouts.
  • Example: Ensuring a failed payment rolls back the order creation.

Together, they ensure robust, reliable software—unit tests verify correctness at a micro level, while integration tests confirm everything works end-to-end.

Why You Shouldn’t Have Unit Tests Without Integration Tests

🚨 Risk: “Working in isolation but failing together.”
Even if individual components work correctly in isolation, they might fail when integrated.

Example: Shopping Cart Works, But Checkout Fails

Scenario:

  • Unit Test confirms that ShoppingCartService correctly calculates totals.
  • BUT integration is broken—checkout fails due to misconfigured database credentials.

Unit Test (Passes)

@Test
void calculateTotalPrice_WithDiscountAndTax_ReturnsCorrectTotal() {
    ShoppingCartService cartService = new ShoppingCartService(discountService, taxCalculator);

    List<Item> items = List.of(new Item("Laptop", 1000.0));
    when(discountService.calculateDiscount(items)).thenReturn(100.0);
    when(taxCalculator.calculateTax(900.0)).thenReturn(90.0);

    double total = cartService.calculateTotalPrice(items);

    assertEquals(990.0, total, "Total price calculation should be correct");
}

Passes! The cart logic is correct.

Integration Test (Fails)

@Test
void checkout_WithValidOrder_FailsDueToDatabaseIssue() {
    OrderRequest request = new OrderRequest("user123", 200.0, "CREDIT_CARD");

    assertThrows(DatabaseConnectionException.class, () -> checkoutController.checkout(request));
}

Fails! The system can’t connect to the database, breaking the checkout process.

Lesson: Unit tests alone can create a false sense of security—you need integration tests to verify system behavior as a whole.


Why You Shouldn’t Have Integration Tests Without Unit Tests

🚨 Risk: Debugging nightmares.
If an integration test fails, it won’t tell you which part of the system is broken—is it the cart logic, payment service, or database?

Example: Checkout Fails, But Why?

Scenario:

  • An integration test fails, but we don’t know if the problem is with the cart logic, the payment gateway, or the database.
  • Without unit tests, debugging becomes painful.

Integration Test (Fails, But No Clarity)

@Test
void checkout_ValidOrder_Fails() {
    OrderRequest request = new OrderRequest("user123", 200.0, "CREDIT_CARD");

    OrderResponse response = checkoutController.checkout(request);

    assertEquals("PAID", response.getStatus(), "Order should be processed");
}

Fails! But why? The failure could be in:

  1. Cart logic (incorrect total calculation).
  2. Payment service (not processing payments).
  3. Database (failed to store the order).

Unit Test (Pinpoints the Bug in Discount Calculation)

@Test
void calculateDiscount_WhenNegativePrice_ThrowsException() {
    DiscountService discountService = new DiscountService();

    assertThrows(IllegalArgumentException.class, () -> discountService.calculateDiscount(-50.0));
}

This test catches the bug early—the discount service doesn’t handle negative prices, which breaks checkout!

💡 Lesson: Without unit tests, integration test failures become hard to debug—you need unit tests to isolate issues and integration tests to validate end-to-end flows. 


Conclusion

In any system, unit testing and integration testing are the two sides of the same coin. Unit tests verify the precision, while integration tests verify harmony. As an SDET, your toolkit should include:

  • JUnit/Mockito for unit tests in isolation.
  • Spring Boot Test/TestContainers for real-world scenario validation.

You will strike a balance that will create resilient systems, handling both the correctness of the logic and real-world complexity.

Leave a Comment

Your email address will not be published. Required fields are marked *

You may also like

unit testing vs integration testing

Difference between Unit Testing and Integration Testing

In software development, testing is the backbone of reliability—especially in enterprise and distributed domains like e-commerce, where a single bug can lead to cart abandonment or revenue loss.  In this

tech

How to use Redux in ReactJS with Real-life Examples

Almost every modern application you see on the internet is likely built using React.js. React is one of the most popular frontend libraries, and frameworks like Next.js enhance its functionality,

Kubernetes

What is CI/CD: CI/CD Pipelines

DevOps teams focus on reliable releases, faster delivery cycles, and reduced errors. Decision makers, on the other hand, seek a process that coordinates development tasks, ensures code quality, and limits

Scroll to Top