At a previous DDD roundtable, the question "If you're doing DDD in a static language, don't you need a test?" Was asked, and more people were surprised to hear "yes / maybe". So I decided to sort it out a bit.
In conclusion, my theory is, "Of course, the benefits of compilation are great, but stopping thinking doesn't solve everything, and there is a lot of learning, so it's better to write."
For example, a modification that increases ʻelse if` is rather dangerous if there is only a type.
Plan.java
public enum Plan {
PLAN_1,
PLAN_2,
PLAN_3;
}
Price.java
public class Price {
private final int value;
public static Price of(Plan plan) {
if (plan == Plan.PLAN_1)
return new Price(100);
else if (plan == Plan.PLAN_2)
return new Price(200);
else
return new Price(300);
}
}
I added PLAN_4
to this.
I wanted the price to be 400 yen, but I forgot to add ʻelse if`.
public enum Plan {
PLAN_1,
PLAN_2,
PLAN_3,
+ PLAN_4;
}
Of course, the compilation goes through, and PLAN_4
costs 300 yen.
scared. very scary.
It's a silly example, but it's not a joke.
It is not uncommon for products to remember to add ʻelse if, such as
price,
detailed item,
email text,
system code, etc. according to
Plan`.
** It is rather reckless to prevent this kind of leakage on a large scale. ** **
Writing a test should be a little better.
If all the elements of the enum (Plan.values ()
) are well entwined in the test code, it can be detected.
PriceTest.groovy
//The test code used groovy and spock
class PriceTest extends Specification {
def "#plan -> #exp"() {
expect:
Price.of(plan) == new Price(exp)
where:
plan || exp
Plan.PLAN_1 || 100
Plan.PLAN_2 || 200
Plan.PLAN_3 || 300
}
def guard() {
expect:
Plan.values().length == 3
}
}
It's simple, but if a test that depends on Plan
contains == 3
, you'll notice when Plan
increases or decreases.
I don't use ʻelse` for price judgment.
Price.java
else if (plan == Plan.PLAN_3)
return new Price(300);
else
throw new RuntimeException("match error");
If you have test code, you will notice it.
On the contrary, if you do it well, you can prevent such bugs perfectly.
Example 1
MailService.java
public class MailService {
public void send(String itemName, String userName) {
String mailBody = MailBodyFactory.create(itemName, userName);
...
}
}
Example 2
FooService.java
public class FooService {
FooRepository fooRepository;
public void replace(Foo usingOne) {
Foo newOne = Foo.newOne();
fooRepository.replace(newOne, usingOne);
}
}
What's wrong is ...
Example 1
MailFactory.java
public class MailBodyFactory {
public static String create(String userName, String itemName) {
return String.format("%s%Thank you for purchasing s", userName, itemName);
}
}
Example 2
FooRepository.java
public interface FooRepository {
void replace(Foo usingOne, Foo newOne);
}
The order of the arguments was reversed.
Did you think "Do you know!"? But most of the time, I don't remember the order of the argument of the product code, and it is unexpectedly very tiring to implement it carefully.
In addition, this kind of thing is very bad because it moves poorly.
Don't abuse String
and create classes for ʻUserName and ʻItemName
.
Then you can never reverse it.
Foo
seems to have created a domain class, but let's divide it by state, for example.
public interface FooRepository {
- void replace(Foo usingOne, Foo newOne);
+ void replace(UsingFoo usingFoo, NewFoo newFoo);
}
This will never be the opposite. (For the class by state, I wrote this article together with momentum → [Let's stop the god entity that expresses all the status in one class!](Https://qiita.com/suzuki-hoge/items / d3ea5940898d85bbc03e)))
It's a rough guide, but I think it's better to type ** where you're trying to explain with variable names **.
It's much easier to get the red line, and the editor also thinks wisely about complement candidates.
By the way, I wrote a similar story before, so if you are interested, please take a look there as well. It feels good to have different classes for the same table values
String
For example, this code
CampaignCode.java
public enum CampaignCode {
CODE_A, CODE_B;
public static CampaignCode create() {
if (LocalDateTime.now().getDayOfMonth() < 15) {
return CODE_A;
} else {
return CODE_B;
}
}
}
When I try to write test code, now
gets in the way and I should say" Oh, I can't write the test ... ?? ".
This means that the product you make will only work with now
, so it will interfere with the testing of other parts, and it will not be flexible in mocking and integration tests.
** The test code uses that method first **, so it's a good chance to notice something like "that? Is this method difficult to use?"
(In this example, now
should be passed as an argument)
was ʻempty
,Stream
is done twice.Foo using
For example, in the case of enum, warnings and compilation errors are issued depending on the language, but I thought that the main flow of the venue was "Does compilation eliminate the need for test code?", So I thought about writing it as an antithesis. I did.
This article is my personal theory.
If you want to imitate the words of a person you heard recently, if you think "I heard you don't need a test, is it really?", Write the test obediently. I think. That's it.
Recommended Posts