Introduction to Generalization in TDD

Welcome back to our course on Test-Driven Development (TDD) using Kotlin. In our previous lesson, we introduced the fundamentals of TDD along with the Red-Green-Refactor workflow. Now, we will elevate our TDD skills by focusing on generalizing solutions and enhancing the complexity of our testing scenarios.

As a brief reminder, TDD involves a repetitive cycle known as Red-Green-Refactor:

  • Red: Write a failing test to clarify the new functionality you aim to implement.
  • Green: Develop the smallest amount of code needed to make that test pass.
  • Refactor: Clean up the code, enhancing its quality while maintaining its functionality and ensuring all tests remain passing.

In this lesson, we will expand upon the sum function, demonstrating how to generalize it while following these TDD principles.

Examining the Current Code Structure

Before we dive into coding, let's review our current setup. You are already familiar with the sum function, which is implemented in Calculator.kt and its corresponding test in CalculatorTest.kt:

Kotlin
1import org.junit.jupiter.api.Test 2import kotlin.test.assertEquals 3 4class CalculatorTest { 5 6 @Test 7 fun sumShouldAddTwoNumbersCorrectly() { 8 val calculator = Calculator() 9 val result = calculator.sum(2, 3) 10 assertEquals(5, result) 11 } 12}
Kotlin
1class Calculator { 2 3 fun sum(a: Int, b: Int): Int { 4 return 5 5 } 6}

This existing setup serves as a foundation. Now, we'll focus on expanding your understanding by generalizing the approach using TDD principles. Understanding where you've come from will help ensure future changes enhance our function without straying too far from the core logic.

Example: Red Phase - Adding a New Failing Test

To embrace the Red phase, let's introduce a new test case that will fail.

Update CalculatorTest.kt to include more input scenarios:

Kotlin
1class CalculatorTest { 2 3 @Test 4 fun sumShouldAddTwoNumbersCorrectly() { 5 val calculator = Calculator() 6 val result = calculator.sum(2, 3) 7 assertEquals(5, result) 8 } 9 10 @Test 11 fun sumShouldAddTwoMoreNumbersCorrectly() { 12 val calculator = Calculator() 13 val result = calculator.sum(5, 6) 14 assertEquals(11, result) 15 } 16}

By including a new scenario with new inputs, this step is intentionally set to fail to define our target goal clearly. Running this test will confirm that additional functionality needs addressing.

Example: Green Phase - Implementing a Solution to Pass Tests

Now, let's transition to the Green phase, where our goal is to ensure all tests pass, including our new one. We can update our sum function to reflect a generic response because it is the minimal solution.

Kotlin
1class Calculator { 2 3 fun sum(a: Int, b: Int): Int { 4 return a + b 5 } 6}

When running the tests, you should see:

Plain text
1All tests passed.

Success! The Green phase is complete, illustrating how effective writing minimal code to pass tests can be.

Example: Refactor Phase - Generalizing the Solution

Finally, let's advance into the Refactor stage. The sum function is very simple, so there's nothing we can make better with the implementation. We do have a lot of duplication in our tests, however.

Let's introduce "parameterized testing." A parameterized test is a single test that has several inputs and expected outputs. It makes it easy to add many test cases without duplicating too much test code:

Kotlin
1import org.junit.jupiter.params.ParameterizedTest 2import org.junit.jupiter.params.provider.Arguments 3import org.junit.jupiter.params.provider.MethodSource 4import java.util.stream.Stream 5import kotlin.test.assertEquals 6 7class CalculatorTest { 8 9 @ParameterizedTest 10 @MethodSource("sumTestCases") 11 fun sumShouldAddTwoNumbersCorrectly(a: Int, b: Int, expected: Int) { 12 val calculator = Calculator() 13 val result = calculator.sum(a, b) 14 assertEquals(expected, result) 15 } 16 17 companion object { 18 @JvmStatic 19 fun sumTestCases(): Stream<Arguments> = Stream.of( 20 Arguments.of(2, 3, 5), 21 Arguments.of(5, 6, 11) 22 ) 23 } 24}

In this refactored test, we use @ParameterizedTest, which allows a single test method to run multiple times with different sets of input data. The @MethodSource annotation specifies the name of a companion object's static method (in this case, sumTestCases) that provides the input data. This method returns a Stream of Arguments, with each Arguments instance representing a single set of inputs and the expected output.

For now, there's no functional change — but strategically refactoring in this way offers benefits, maintaining clarity as complexity escalates over iterations.

Review and Preparation for Practice

In this lesson, we delved into using TDD to generalize solutions in Kotlin by:

  • Reviewing and examining the initial state of the sum function and its associated test cases.
  • Engaging in the Red phase by adding a new failing test to identify necessary functionality improvements.
  • Advancing to the Green phase by implementing minimal code changes to pass the new test.
  • Moving to the Refactor phase, where we utilized parameterized testing to reduce test duplication and improve maintainability.

In preparation for your practice exercises, remember to focus on clear test scenarios and consider how your implementation can be generalized for future enhancements. Happy coding!

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