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.
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 |
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.
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.
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.
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.
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.
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.
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();
}
}
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.
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.
[^ 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