Tests are usually seen as tools for preventing bugs. That is true. Tests check whether code behaves as expected and help prevent changes from breaking existing behavior.
But in DDD, tests can do more than that. Good domain tests document domain rules. By reading only test names and scenarios, we can understand what rules the domain has.
If DDD values Ubiquitous Language, tests are also an important place where that language is used. When the words used in meetings, method names in code, and scenarios in tests use the same language, the model becomes stronger.
Test names can become domain sentences
Think about tests for a subscription service. Test names like these are technical:
cancel_success
changePlan_fail
renew_test
We can roughly know what they verify, but domain rules are not very visible.
The following names are closer to domain sentences:
An_expired_subscription_cannot_change_plans
A_member_who_already_used_a_free_trial_cannot_start_another_free_trial
When_payment_fails_the_subscription_enters_a_grace_period
A_canceled_subscription_cannot_be_canceled_again
These test names are not just verification names. They speak domain rules. A developer who newly joins can understand important policies just by looking at the test list.
A good test name should reveal the domain rule rather than the implementation method.
Given-When-Then creates domain scenarios
Domain tests fit well with the Given-When-Then structure.
Given: when a certain domain situation is given
When: when a certain domain behavior happens
Then: a certain result should happen
For example, we can test subscription cancellation.
@Test
void full_refund_is_available_when_canceling_within_7_days_after_payment_and_without_usage_history() {
// given
Subscription subscription = SubscriptionFixture.activePaidSubscription()
.paidAt(daysAgo(3))
.withoutUsage()
.build();
// when
RefundDecision decision = refundPolicy.decide(subscription, now());
// then
assertThat(decision.isFullRefund()).isTrue();
}
This test does not simply check a method return value. It expresses one sentence of refund policy in code.
When Given-When-Then is used well, tests become a form that can be discussed even with domain experts. We can ask, “Is full refund correct when payment was made within 7 days and there is no usage history?”
Domain model tests should be fast and clear
Domain model tests should be as fast and clear as possible. They should run without a database, web server, or external API.
For example, testing the state transition of Subscription does not require a real DB.
@Test
void subscription_can_expire_after_the_expiration_date_has_passed() {
Subscription subscription = SubscriptionFixture.active()
.period(endedYesterday())
.build();
subscription.expire(now());
assertThat(subscription.status()).isEqualTo(EXPIRED);
}
This test directly verifies the rule of a domain object. It runs quickly, and when it fails, the cause is easy to find.
If domain tests are slow and complex, developers will stop running them often. Domain rules change often, so tests that verify those rules should be lightweight.
Application Service tests and Domain Model tests are different
Application Service tests verify use case flow. They check which Repository is queried, which domain object is called, what is saved, and which events are published.
Domain Model tests verify the rules themselves. They check whether a subscription can be canceled, what result a plan change policy produces, and how free trial availability is judged.
Distinguishing the two matters.
Domain Model Test
- An expired subscription cannot change plans.
- Payment failure transitions to a grace period.
- A free trial is available only once.
Application Service Test
- When a plan change is requested, load the subscription, change it, and save it.
- After successful subscription renewal, publish a SubscriptionRenewed event.
- When cancellation is requested, request follow-up entitlement revocation.
If every domain rule is verified only through Application Service tests, tests become heavy and the location of rules becomes unclear. Conversely, use case flow cannot be verified only with domain object tests.
Tests also need separated responsibilities.
Tests reinforce Ubiquitous Language
Test code contains many names. Test names, Fixture names, method names, variable names, and assertion messages are all part of the language.
Subscription subscription = activePaidSubscription();
CancelReason reason = CancelReason.byMember("No longer using it");
subscription.cancelByMember(reason, now);
This code repeats the domain language. As tests increase, that language settles into the team.
Conversely, tests that use only technical names do not reinforce Ubiquitous Language.
Subscription s = fixture1();
s.updateStatus(2);
assertThat(s.getStatus()).isEqualTo(3);
This test can verify behavior, but it does not explain the domain.
A good test fails as a domain sentence when it fails.
A_member_who_already_used_a_free_trial_cannot_start_another_free_trial
If this sentence breaks, we need to check not only the implementation but also the policy itself.
Build Fixtures in domain language
Fixtures are important in domain tests. If the code that creates test data is complex, the intent of the test becomes blurred.
A bad Fixture lists field values rather than domain meaning.
new Subscription(id, memberId, planId, "A", start, end, null, false);
A good Fixture creates a domain situation.
Subscription subscription = SubscriptionFixture.activePaidSubscription()
.withPlan(PRO)
.paidAt(daysAgo(3))
.withoutUsage()
.build();
This code creates the situation “an active paid subscription that was paid three days ago and has no usage history.” The Given part of the test reads like a domain sentence.
Fixture is not simple convenience code. It is a language for expressing domain situations in tests.
Tests push design
When writing domain tests, design problems also become visible.
If too many mocks are needed for a test, the domain object may know too many external dependencies. If the whole Application Service must be started just to test one rule, the rule may not be in the right place. If the test name is clear but the code cannot express that name, method names or model structure may need another look.
For example, suppose we want to write this test:
An_expired_subscription_cannot_change_plans
If the actual code can verify this only through several services, Repositories, and external API mocks, the rule may be scattered too far outside.
A good domain model makes important rules easy to test directly.
Closing
Tests are tools for preventing bugs. But in DDD, tests can also become documents that explain the domain model.
Good domain tests speak domain rules more than implementation details. Test names become domain sentences, Given-When-Then becomes domain scenarios, and Fixtures become a language for creating domain situations.
When tests use Ubiquitous Language, code, documents, and conversations get closer. New developers can learn the domain by reading tests, and existing developers can quickly see which rules a policy change breaks.
In DDD, tests are not just a safety net. Tests are one of the most concrete ways to confirm that domain knowledge is alive inside the code.