Introduction and Overview

In this lesson, we'll deepen our understanding of the Test-Driven Development (TDD) mindset using Scala 3 and ScalaTest by focusing on the Red-Green-Refactor cycle. We'll work through a practical example centered on a calculateTotal function, guiding you through thinking with tests, prioritizing test writing, and leveraging TDD to enhance code clarity, reliability, and maintainability.

Using Scala and ScalaTest, we'll follow these steps:

  • Begin with the Red phase by identifying and writing failing tests for the calculateTotal function, which will compute the total price of items in a shopping cart.
  • Move to the Green phase to implement the minimal code required to pass each test, ensuring that the calculateTotal function behaves as expected.
  • Enter the Refactor phase to improve the code structure and readability of calculateTotal, employing Scala’s functional programming features for aggregation while keeping tests green.
  • Utilize ScalaTest as the testing framework to efficiently integrate the TDD approach within our Scala project.

By working through this example, you'll gain practical experience with TDD principles and develop the calculateTotal function in a way that showcases how TDD fosters code quality and robustness.

Example: 'calculateTotal' Function (Red Phase)

Let's begin by writing tests for a function named calculateTotal (from the Cart class, which we'll explore soon), designed to compute the total price of items in a shopping cart. This is where you engage with the Red phase: Write a failing test.

When crafting methods that operate over collections, it’s important to determine the interface first from a test perspective. Here's one way we might write this in Scala.

  • We know we want a method called calculateTotal, so our test uses it.
  • For now, we know that we want an empty list as input to return 0, so we can write that test.
  • Using assert ensures clarity and simplicity in checking test results.

Upon running these tests, you’ll see they fail, marking the Red phase and defining the next steps for development.

Example: Passing the Tests (Green Phase)

Now, let's move to the Green phase, where we implement the minimal code required to pass these tests.

Here’s the calculateTotal method in a new Cart class:

  • The calculateTotal method takes a List of CartItem objects and simply returns 0.0.
  • This minimal implementation ensures the first test passes, aligning with the requirements for an empty cart.

Running the test suite will now confirm that all tests pass, demonstrating that our function satisfies the initial condition.

Example: Write Another Test (Red)

Next, we refine our approach to handle specific CartItem instances. We'll introduce name, price, and quantity attributes within the CartItem class. The total will be price * quantity. Let's write a new test for this:

This test defines the expected behavior for a single item, which will initially fail, guiding us to the Green phase.

Example: Make It Pass Again (Green)

Now we need to think about how to make these tests pass. What is the minimum necessary to get this to pass?

  • If the list is non-empty, the method calculates the total for the first item (price * quantity).
  • Otherwise, it returns 0.0.

Running the tests again will confirm that both tests now pass, marking us Green!

Example: Refactor

Now we ask ourselves: is there anything we can do to make this code better? One thing we might do is eliminate the repetition of accessing the first item in the list.

Another thing we might do is ensure each CartItem has well-defined properties.

In the above snippet, we define the CartItem as a case class with three necessary properties: name, price, and quantity. By using a case class, the properties are immutable by default, and we also get additional benefits like value-based equality, a readable toString, and the ability to create modified copies easily with the copy method.

When we run the tests again, we're still green. The code is more expressive now and reflects the properties of CartItem more clearly, so let's do the Red-Green-Refactor loop again!

Example: Write a Failing Test (Red)

The existing code works! But it won't be very useful to only use the first item. If we add one more test, we can generalize more:

This test will fail, signaling that our current implementation needs improvement.

Example: Make the Test Pass (Green)

To satisfy this test, we’ll enhance the implementation to handle multiple items:

  • This implementation uses a for loop to iterate through all items and calculate their total price.
  • The total variable accumulates the sum, ensuring correctness.

This does the job, and we're Green again. I can't help but feel like we could have written that code a bit better. Now that we have tests that cover everything we want this function to do, let's move to the Refactor step!

Example: Refactor!

When we look at the calculateTotal function, it is clear that this is an "aggregate function." We can refactor the code to leverage Scala's functional capabilities for aggregation:

  • The map function transforms each CartItem into its total price (price * quantity).
  • The sum function aggregates these values to produce the final total.

I like this code a lot better! Best of all, our tests tell us that we're still Green! We've successfully refactored the code.

Summary and Preparation for Practice

Throughout this lesson, we emphasized writing tests before implementation and adhering to the Red-Green-Refactor cycle. Here's a summary of our process:

  • Red Phase: We started with failing tests, defining behavior like returning 0 for an empty cart or computing the total for specific CartItem examples.
  • Green Phase: We implemented minimal solutions to ensure tests passed, incrementally building functionality for multiple CartItem instances.
  • Refactor Phase: We enhanced code clarity and aggregation using Scala’s functional features like map and sum.

By following these TDD steps, we ensured clarity, reliability, and continuous improvement in our implementation. Practice exercises will further reinforce how TDD fosters maintainable and robust software development in Scala.

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