Introduction to Managing Dependencies in TDD

In earlier lessons, we explored the fundamentals of Test-Driven Development (TDD), including the Red-Green-Refactor cycle and setting up a testing environment. Now, we’ll focus on managing dependencies, an essential practice for isolating units of code and ensuring tests are reliable and independent.

We’ll use traits in Scala to manage dependencies. Traits serve as contracts for abstractions, allowing you to decouple components and make them easier to test. This lesson will demonstrate how to apply the Red-Green-Refactor cycle to dependency management using Scala and ScalaTest.

Understanding Dependencies and Traits

Dependencies are external components that your code interacts with. Managing them effectively ensures that your tests are independent of these external factors. This can be achieved using traits, which allow us to define abstractions and swap implementations easily.

For example, a logger that a class uses can be abstracted as a trait. This lets us replace the actual logger with a mock during testing, focusing solely on the logic under test.

trait Logger:
  def log(message: String): Unit

The Logger trait defines a single log method. This abstraction allows different logging implementations, such as a real logger in production or a mock logger in tests.

Building UserManager with Dependency Injection

Let’s create a UserManager class that depends on the Logger trait. To keep it decoupled, we’ll inject the logger dependency through the constructor.

import scala.collection.mutable.ListBuffer

class UserManager(logger: Logger):
  private val users = ListBuffer[String]()

  def addUser(username: String): Unit =
    users += username
    logger.log(s"User $username added")

  def getUsers: List[String] =
    users.toList

By injecting the logger, UserManager remains flexible, allowing different implementations of Logger to be provided. This makes the class easier to test.

Testing with a Mock Logger

To test UserManager without using a real logger, we can create a MockLogger that captures log messages for verification.

import scala.collection.mutable.ListBuffer

class MockLogger extends Logger:
  private val logs = ListBuffer[String]()

  def log(message: String): Unit =
    logs += message

  def getLogs: List[String] =
    logs.toList

The MockLogger stores log messages in memory. This allows us to verify that UserManager interacts with the logger as expected.

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.*

class UserManagerSpec extends AnyFunSpec with Matchers:

  describe("UserManager"):

    it("should add a user successfully"):
      val mockLogger = MockLogger()
      val userManager = UserManager(mockLogger)

      userManager.addUser("john")

      userManager.getUsers should contain("john")

    it("should log a message when a user is added"):
      val mockLogger = MockLogger()
      val userManager = UserManager(mockLogger)

      userManager.addUser("john")

      mockLogger.getLogs shouldEqual List("User john added")

The UserManagerSpec verifies two things:

  1. Users are added to the internal list correctly.
  2. A log message is generated when a user is added.
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