Increasing Code Test Coverage by Adding Characterization Tests

Introduction

Welcome to the second lesson of the "Increasing Code Test Coverage" course! In our previous lesson, we explored the importance of code test coverage and how it ensures software quality and developer confidence. Today, we will delve into a specific type of testing known as characterization tests. These tests are invaluable when working with legacy code, as they help document the current behavior of a system without altering its functionality.

By the end of this lesson, you'll understand how to use characterization tests to increase code coverage effectively.

Understanding Characterization Tests

Characterization tests are designed to capture the existing behavior of a system. They are particularly useful when dealing with legacy code that lacks documentation or tests. The primary goal of these tests is not to find bugs but to document what the code currently does. This understanding is crucial when you need to make changes or refactor the code, as it provides a safety net that ensures you don't inadvertently alter the intended behavior.

Another important aspect of characterization tests is their ability to capture the subtle nuances of a system that aren't immediately apparent from class or method names alone.

For example, consider a method in an order processing system that calculates the total order amount and applies discounts. A characterization test for this method would verify that the current logic correctly calculates the total and applies discounts as expected, even if the logic is flawed. This way, you can confidently refactor the code, knowing that the test will alert you if the behavior changes unexpectedly.

Without tests, it's difficult to understand the code's behavior, making modifications risky. Characterization tests offer a solution by allowing you to explore and document the code's behavior safely. By writing tests that capture the current functionality, you create a baseline that helps identify unintended changes during future modifications.

Writing Characterization Tests

Writing characterization tests involves creating tests that reflect the current behavior of the code. Let's look at a practical example using the OrderProcessor class from our order processing system:

def processOrder(order: Order): Boolean =
  // ... processing logic before calculations ...
  
  val totalAmount = order.items.map(item => 
    item.price * item.quantity
  ).sum
  
  // Apply bulk order discount if applicable
  val (finalAmount, hasBulkDiscount) = if totalAmount >= BULK_ORDER_THRESHOLD then
    (totalAmount * (1 - BULK_ORDER_DISCOUNT), true)
  else
    (totalAmount, false)
    
  // ... additional logic before finalizing order ...
  
  val processedOrder = order.copy(
    orderTotal = finalAmount,
    hasBulkDiscount = hasBulkDiscount,
    processedAt = Some(LocalDateTime.now)
  )
  
  processedOrder.status == OrderStatus.Approved
it("should apply discount when order exceeds bulk threshold"):
  // Arrange
  val order = Order(
    items = List(OrderItem(price = BigDecimal(1000), quantity = 1))
  )
  val processor = new OrderProcessor

  // Act
  val processedOrder = processor.processOrder(order)

  // Assert
  assert(processedOrder.hasBulkDiscount)
  assert(processedOrder.orderTotal == BigDecimal(900)) // 10% discount applied
  assert(processedOrder.processedAt.isDefined)

In the above test, we arrange an order with a total amount that qualifies for a bulk discount. The test then verifies that the discount is applied correctly, capturing the current behavior of the method.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal