Recently, screen development using typescript has become the main focus. I thought about writing java for the first time in a long time, and since it was a great opportunity, I would like to practice TDD.
The development here is summarized below. https://github.com/ko-flavor/study-TDD/pull/2
I wrote the large table of contents and commits as much as possible. Please refer to those who want to compare with the code.
I borrowed the material from Kata-Log. The theme is to write a method to convert numbers to Roman numerals.
This area has nothing to do with TDD, so I'll break it down.
--Gradle project automatic generation
--Edit package name, class name, method name
--The package name is RomanNumerals
→ romanNumerals
--The class name is RomanNumeralConverter
--The method name is convert
as instructed
--Edit .gitignore
--I wanted to use jUnit5 and assertThat
, so edit build.gradle
--Set the contents of the test to fail ()
and you are ready to go.
For the time being, I made a to-do list with my own knowledge. When I make a to-do list, I'm wondering, "What if it's 0?" Or "How do you express a large number?" Well, for the time being, I'll close my eyes and implement it according to the following. (Sorry, the TODO list committed on github is garbled.)
-[] Convert from 1 to I -[] Convert from 2 to II -[] Convert from 3 to III -[] Convert from 4 to IV -[] Convert from 5 to V -[] Convert from 6 to VI -[] 9 → Convert to IX -[] Convert from 10 to X -[] 11 Convert to XI
Write the first test to commemorate.
@Test
public void test_one_is_converted_to_I() {
RomanNumeralConverter converter = new RomanNumeralConverter();
String result = converter.convert(1);
assertThat(result).isEqualTo("I");
}
Of course, I haven't implemented it, so the test will fail. I want to bring it to the GREEN bar as quickly as possible, so I implemented the method below.
public String convert(int i) {
return "I";
}
With this, I was able to worship the GREEN bar for the first time.
-[x] Convert from 1 to I
Now let's write the second test.
@Test
public void test_two_is_converted_to_II() {
RomanNumeralConverter converter = new RomanNumeralConverter();
String result = converter.convert(2);
assertThat(result).isEqualTo("II");
}
Now it's RED, hurry up to GREEN.
public String convert(int i) {
return i == 1 ? "I" : "II";
}
This completes the following of the TODO list.
-[x] Convert from 2 to II
As shown below, it is possible to do it even if the number is other than 1/2.
public String convert(int number) {
StringBuilder builder = new StringBuilder();
IntStream.rangeClosed(1, number).forEach(i -> builder.append("I"));
return builder.toString();
}
The tests have passed cleanly, so you can rest assured that the refactoring will not affect you.
The test class also moved the instance that was new
in each method to the field.
I created a third test.
@Test
public void test_three_is_converted_to_III() {
String result = this.converter.convert(3);
assertThat(result).isEqualTo("III");
}
It was already GREEN. Since the test method seems to be inlined, I modified the test class as follows.
@Test
public void test_three_is_converted_to_III() {
assertThat(this.converter.convert(3)).isEqualTo("III");
}
-[x] Convert from 3 to III
It seems that IV
is difficult, so I will start with the simple V
pattern.
@Test
public void test_five_is_converted_to_V() {
assertThat(this.converter.convert(5)).isEqualTo("V");
}
Naturally, the test will fail. I want to worship the GREEN bar at the fastest speed, so write the following code and pass it through.
public String convert(int number) {
StringBuilder builder = new StringBuilder();
if (number == 5) {
return "V";
}
IntStream.rangeClosed(1, number).forEach(i -> builder.append("I"));
return builder.toString();
}
-[x] Convert from 5 to V
If it is 5, I don't like the part of returning V
.
The process you want to do is to add V
if it is 5 or more and subtract 5 from the number to be converted.
I will drop it in the code as it is.
if (number >= 5) {
builder.append("V");
number = number - 5;
}
It's OK. The test has passed properly.
Describe the test below. The test will pass.
@Test
public void test_six_is_converted_to_VI() {
assertThat(this.converter.convert(6)).isEqualTo("VI");
}
** Isn't this test unnecessary? Of course, there is an argument that **. It's not just a matter of increasing the number of test methods. ..
Since it is different from the purpose of this time, I will omit it, When putting it into practice, I think it will be a source of discussion as a team.
There is no rule that you can't change the to-do list once you've made it, so I think that we should update it steadily in response to the discussion.
-[x] 6 → Convert to VI
At this point, I think you've gradually learned how to proceed with test-driven development. Create the following test, and of course the test will fail.
@Test
public void test_ten_is_converted_to_X() {
assertThat(this.converter.convert(10)).isEqualTo("X");
}
It seemed like I should use the case of V
, so
I added the following branches and passed the test.
if (number >= 10) {
builder.append("X");
number = number - 10;
}
-[x] Convert from 10 to X
I forgot to write an article and did various things. The final result is as follows.
StringBuilder builder;
int number;
public String convert(int numberToConvert) {
initVariables(numberToConvert);
appendX();
appendV();
appendI();
return builder.toString();
}
private void initVariables(int numberToConvert) {
this.builder = new StringBuilder();
this.number = numberToConvert;
}
private void appendX() {
if (number >= 10) {
builder.append("X");
number = number - 10;
}
}
Move StringBuilder and the number to be converted to the field,
Changed to initialize every time convert
.
However, due to this, multi-thread support is no longer possible. The caller needs to consider it.
I added the following to the test class so that the instance is new
every time.
@BeforeEach
public void setup() {
this.converter = new RomanNumeralConverter();
}
I don't think there is any need for explanation.
@Test
public void test_eleven_is_converted_to_XI() {
assertThat(this.converter.convert(11)).isEqualTo("XI");
}
-[x] 11 Convert to XI
Create the test below.
@Test
public void test_four_is_converted_to_IV() {
assertThat(this.converter.convert(4)).isEqualTo("IV");
}
In order to make it GREEN in a hurry, I implemented the following method and it became GREEN safely.
private void appendIV() {
if (number >= 4) {
builder.append("IV");
number = number - 4;
}
}
Similarly, we implemented 9 → IX conversion testing and logic.
-[x] Convert from 4 to IV -[x] 9 → Convert to IX
With this, all the to-do lists that were originally prepared are checked.
Now that I've learned more about Roman numeral specifications, I've revisited my to-do list. It seems that the conversion of Roman numerals is perfect if all of the following are met.
-[] Convert from 40 to XL -[] Convert from 50 to L -[] Convert from 90 to XC -[] Convert from 100 to C -[] 400 → Convert to CD -[] Convert from 500 to D -[] Convert from 900 to CM -[] Convert from 1000 to M -[] 3999 → Convert to MMMCMXCIX -[] An error will occur if the argument is 0 or less. -[] An error will occur if the argument is 4000 or more.
The following append Hoge
s have been increasing steadily, and the desire for commonality has increased.
private void appendIX() {
if (number >= 9) {
builder.append("IX");
number = number - 9;
}
}
private void appendV() {
if (number >= 5) {
builder.append("V");
number = number - 5;
}
}
Using a functional interface, I've declared the following and completely abolished methods like the one above. The implementation class is now about half the length.
private BiConsumer<Integer, String> appendRoman = (i, str) -> {
while (number >= i) {
builder.append(str);
number = number - i;
}
};
The methods previously declared in appendHoge
can now be described as follows.
appendRoman.accept(5, "V");
appendRoman.accept(4, "IV");
appendRoman.accept(1, "I");
At this point, implementation is insanely easy. It takes about 30 seconds to add and implement a test case.
-[x] Convert from 40 to XL -[x] Convert from 50 to L -[x] Convert from 90 to XC -[x] Convert from 100 to C -[x] 400 → Convert to CD -[x] Convert from 500 to D -[x] Convert from 900 to CM -[x] Convert from 1000 to M -[x] 3999 → Convert to MMMCMXCIX
I implemented the test below.
@Test
public void test_zero_cannot_be_converted() {
assertThrows(IllegalArgumentException.class, () -> this.converter.convert(0));
}
It's been a long time since I became RED, so I'll hurry to make it GREEN.
private void checkArgument(int numberToConvert) {
if (numberToConvert == 0) {
throw new IllegalArgumentException();
}
}
Similarly, the test for 4000 is also described, and it will be refactored after setting it to GREEN. The final result is as follows.
private void checkArgument(int numberToConvert) {
if (!(0 < numberToConvert && numberToConvert < 4000)) {
throw new IllegalArgumentException("Argument must be between 1 and 3999.");
}
}
-[x] An error will occur if the argument is 0 or less. -[x] Error if the argument is 4000 or more
By the way, if you look at the execution result of jUnit, there are a lot of test cases. There are 20 tests written in parallel, and you can see what you are doing by looking at each method, but ** I don't know if I can test it at a glance. ** **
So refactor.
You can structure your test class from jUnit5 with the @ Nested
annotation.
Use this to refactor your test class for working documentation.
Well, it's a lot easier to understand than 20 tests lined up in parallel!
end.
When I noticed it, it became a big article. .. Also, despite the fact that I chose it properly, I learned a lot and I was absorbed in it. After all, it's fun to make something that works by yourself.
I've been touching mainly on typescript recently, so I was happy to realize that the functional interface could be abstracted without any hesitation.
However, I feel that there are many parts that cannot be easily conveyed by sentences alone. I would like to continue to spread TDD through live coding, pair pros, mob pros, etc.!
Recommended Posts