[JAVA] Implement FizzBuzz problems in test-driven development (practice)

2017.07.02 Edited.

Continuing from the Preparation posted the other day, this time we will proceed with TDD for the FizzBuzz problem as a practical version.

Review of preparation and flow of practice

Benefits of TDD

FizzBuzz Problem Requirements

Requirements Contents
Requirement 1 Returns the value taken as an argument as a string
Requirement 2 However, if it is a multiple of 3, "Fizz" is returned.
Requirement 3 However, if it is a multiple of 5, "Buzz" is returned.
Requirement 4 However, if it is a multiple of both 3 and 5, it returns "FizzBuzz".
Requirement 5 Error if the argument is not a number from 1 to 100

Flow of practice

Since it would be redundant to introduce all requirements 1 to 5, only requirement 1 will be introduced in this article. The full program is posted at the end of the article.


Requirement 1: Returns the value taken as an argument as a string

First make sure the test fails

In the implementation of Requirement 1, the test class (FizzBuzzTest.java) and the test target class (FizzBuzz.java) were first implemented as follows.

FizzBuzzTest.java


public class FizzBuzzTest {
    @Test
public void Returns 1 if 1 is given as an argument() {
        FizzBuzz fizzbuzz = new FizzBuzz();
        assertEquals("1", fizzbuzz.response(1));
    }
}

FizzBuzz.java


public class FizzBuzz {
    //Returns null(Returns no value)
    public String response(int num) {
        return null;
    }
}

Move the cursor to FizzBuzzTest, right-click-> Run As…-> press JUnit Test, and when you run the test, the result will naturally be a red bar indicating the test failure.

4.JPG

This ** test failure ** is actually the point. When developing a new test target class like this time, you can check whether ** JUnit correctly judges the test success or failure **.

Also, if you add a new test to the ** existing method of the class under test and the test succeeds, it is possible that some of the additional functions have already been implemented. See through the sex.

Implement a program that will pass the test

Next, implement the method under test so that the test will pass.

FizzBuzz.java


    public String response(int num) {
        return "1";
    }

If you run the test as before, the test will, of course, succeed.

5.JPG

In TDD development, we start by ** writing a program that passes the test **, so until we get used to it, we will not narrow down the test cases from the beginning, and even if it is a little straightforward, we will build up to achieve each purpose one by one.

Now let's try it even if the argument is 2.

FizzBuzzTest.java


    @Test
public void Returns 1 if 1 is given as an argument() {
        FizzBuzz fizzbuzz = new FizzBuzz();
        assertEquals("1", fizzbuzz.response(1));
    }
    @Test
public void Returns 2 if 2 is given as an argument() {
        FizzBuzz fizzbuzz = new FizzBuzz();
        assertEquals("2", fizzbuzz.response(2));
    }

FizzBuzz.java


    public String response(int num) {
        return "1";
    }

The test execution result is as follows, and a red bar indicating test failure is displayed.

6.JPG

Implement a program that meets the requirements and passes the test

There are generally two possibilities from the added test failure:

Looking back at the execution result of JUnit, we can see from the following description that although we expected the execution result to be 2, the execution result was 1 and did not match the expected value.

org.junit.ComparisonFailure: expected:<[2]> but was:<[1]>

Therefore, modify the program under test FizzBuzz.java as follows according to the requirement: the value taken as an argument is returned as a character string.

FizzBuzz.java


    public String response(int num) {
        return String.valueOf(num);
    }

After confirming that the test is successful, the implementation of Requirement 1 is complete.

7.JPG

Similarly, FizzBuzzTest.java and FizzBuzz.java, which implement requirements 2 to 5 and narrow down the test cases of the equivalence class to one, are as follows.

FizzBuzzTest.java


public class FizzBuzzTest {
    private FizzBuzz fizzbuzz;

    @Before
public void instantiation() {
        fizzbuzz = new FizzBuzz();
    }

    @Test
public void Returns 1 if 1 is given as an argument() {
        assertEquals("1", fizzbuzz.response(1));
    }

    @Test
public void Returns Fizz with argument 3() {
        assertEquals("Fizz", fizzbuzz.response(3));
    }

    @Test
public void Returns Buzz with argument 5() {
        assertEquals("Buzz", fizzbuzz.response(5));
    }

    @Test
public void Returns FizzBuzz with argument 15() {
        assertEquals("FizzBuzz", fizzbuzz.response(15));
    }

    @Test(expected = IndexOutOfBoundsException.class)
If 0 is given to the public void argument, an error will occur.() {
        fizzbuzz.response(0);
    }

    @Test(expected = IndexOutOfBoundsException.class)
If 101 is given to the public void argument, an error will occur.() {
        fizzbuzz.response(101);
    }

    @Test
public void Returns Buzz if 100 is given as an argument() {
        assertEquals("Buzz", fizzbuzz.response(100));
    }
}

FizzBuzz.java


public class FizzBuzz {
    public String response(int num) {
        if(num < 1 || num > 100) {
            throw new IndexOutOfBoundsException();
        }

        StringBuilder result = new StringBuilder();

        if(num % 3 == 0) {
            result.append("Fizz");
        }
        if(num % 5 == 0) {
            result.append("Buzz");
        }
        if(result.length() == 0) {
            result.append(String.valueOf(num));
        }

        return result.toString();
    }
}

Bonus: Test class structuring

From the above FizzBuzzTest.java test method name, you can know what the test is to check, so you can finish it as it is, but it is easier to understand ** which requirement each test method corresponds to ** Finally, ** structure the test class **.

Test class structuring is to use JUnit's @RunWith (Enclosed.class) annotation to set an inner class for each test category to classify test cases. [^ 1] [^ 2]

This test can be divided into the following categories according to the requirements.

Item number category
1 Arguments are not multiples of 3 and 5
2 Multiples with only 3 arguments
3 Multiples with only 5 arguments
4 Arguments are multiples of both 3 and 5
5 Argument is invalid boundary value(Not a number from 1 to 100)
6 Argument is a valid boundary value(Numbers from 1 to 100)

Set the inner class for each of the above categories, and finally rewrite FizzBuzzTest.java as follows.

FizzBuzzTest.java


@RunWith(Enclosed.class)
public class FizzBuzzTest {
    
    
public static class argument is not a multiple of 3 and 5{
        FizzBuzz fizzbuzz = new FizzBuzz();

        @Test
public void Returns 1 if 1 is given as an argument() {
            assertEquals("1", fizzbuzz.response(1));
        }
    }

public static class A multiple of 3 arguments only{
        FizzBuzz fizzbuzz = new FizzBuzz();

        @Test
public void Returns Fizz with argument 3() {
            assertEquals("Fizz", fizzbuzz.response(3));
        }
    }

public static class A multiple of 5 arguments only{
        FizzBuzz fizzbuzz = new FizzBuzz();

        @Test
public void Returns Buzz with argument 5() {
            assertEquals("Buzz", fizzbuzz.response(5));
        }
    }

public static class arguments are multiples of 3 and 5{
        FizzBuzz fizzbuzz = new FizzBuzz();

        @Test
public void Returns FizzBuzz with argument 15() {
            assertEquals("FizzBuzz", fizzbuzz.response(15));
        }
    }

public static class argument is invalid Boundary value{
        FizzBuzz fizzbuzz = new FizzBuzz();

        @Test(expected = IndexOutOfBoundsException.class)
If 0 is given to the public void argument, an error will occur.() {
            fizzbuzz.response(0);
        }

        @Test(expected = IndexOutOfBoundsException.class)
If 101 is given to the public void argument, an error will occur.() {
            fizzbuzz.response(101);
        }
    }

public static class argument is a valid boundary value{
        FizzBuzz fizzbuzz = new FizzBuzz();

        @Test
public void Returns 1 if 1 is given as an argument() {
            assertEquals("1", fizzbuzz.response(1));
        }

        @Test
public void Returns Buzz if 100 is given as an argument() {
            assertEquals("Buzz", fizzbuzz.response(100));
        }
    }
}

Of course, after rewriting, run the test to make sure everything is successful.

構造化.PNG

I think that the test results also make it easier to understand which requirements the test cases are for, rather than the test cases being lined up in a row.

This is the end of the practical edition. This time it was TDD for relatively simple requirements, but in the future I will definitely try TDD when implementing more complex requirements and see its effects and limitations.

References


[^ 1]: The @RunWith (Enclosed.class) annotation recognizes all inner classes as being tested and will execute the methods with the @Test annotation in each inner class.

[^ 2]: If you don't like the inner class, you can create a test method for each test category and express the test case by setting multiple assertions.

Recommended Posts

Implement FizzBuzz problems in test-driven development (practice)
Implement FizzBuzz problem in test-driven development (preparation)
Test-driven development in the functional language Elm
FizzBuzz in Java
Implement CustomView in code
Implement markdown in Rails
How to implement one-line display of TextView in Android development