DAMP over DRY

Often people will talk about Don’t Repeat Yourself as a way of minimising the amount of duplication/repetition of tasks. In general it is a great thing to keep in mind. However, when it comes to writing tests, often it is preferable to favour expressiveness at the ’expense’ of repetition. This is referred to as Descriptive And Meaningful Phrases.

It’s a balance. You don’t want to have a lot of repeated set up in every test which is then subtly different in a single test. The reader will get blind to the set up and is unlikely to notice the subtle difference - it could be extremely confusing! In that case, it would be better to extract the common setup into a well named helper method, perhaps with a good description that can be easily visible from the tests in the IDE.

Example — When Duplication Helps

These tests share duplicated setup, but the setup is short enough that the differences — customer type, expected total, and discount label — are immediately obvious. Extracting a helper here would actually hurt readability by hiding the details that make each scenario meaningful. Each test tells its own complete story at a glance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

@Test
void calculatesDiscountedPriceForPartners() {
    var partnerCustomer = new Customer("Alice", CustomerType.PARTNER);
    var order = new Order(partnerCustomer);
    order.addItem(new Item("Sofa", 950.00));
    order.addItem(new Item("Blanket", 50.00));

    var invoice = new InvoiceGenerator().generate(order);

    assertEquals(750, invoice.getTotal()); // 25% partner discount
    assertEquals("Partner Discount Applied", invoice.getDiscountLabel());
}

@Test
void calculatesFullPriceForStandardCustomer() {
    var regularCustomer = new Customer("Bob", CustomerType.STANDARD);
    var order = new Order(regularCustomer);
    order.addItem(new Item("Sofa", 950.00));
    order.addItem(new Item("Blanket", 50.00));

    var invoice = new InvoiceGenerator().generate(order);

    assertEquals(1000.00, invoice.getTotal()); // no discount
    assertNull(invoice.getDiscountLabel());
}

Antipattern — Spot the Difference

However, when tests have large, nearly identical setup blocks, the one thing that actually varies gets buried. The reader has to play “spot the difference” across walls of code, making the tests harder to understand, not easier.

This example is not descriptive and meaningful — the important detail is hidden in the noise of the duplicated code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

@Test
void approvesLoanForHighCreditScore() {
    var applicant = new Applicant();
    applicant.setFirstName("Jane");
    applicant.setLastName("Smith");
    applicant.setAge(35);
    applicant.setAnnualIncome(75000);
    applicant.setEmploymentStatus(EmploymentStatus.FULL_TIME);
    applicant.setYearsEmployed(5);
    applicant.setCreditScore(780);
    applicant.setExistingDebt(10000);
    applicant.setRequestedAmount(200000);
    applicant.setLoanTerm(30);
    applicant.setPropertyValue(250000);

    var result = new LoanAssessor().assess(applicant);

    assertTrue(result.isApproved());
}

@Test
void rejectsLoanForLowCreditScore() {
    var applicant = new Applicant();
    applicant.setFirstName("Jane");
    applicant.setLastName("Smith");
    applicant.setAge(35);
    applicant.setAnnualIncome(75000);
    applicant.setEmploymentStatus(EmploymentStatus.FULL_TIME);
    applicant.setYearsEmployed(5);
    applicant.setCreditScore(420); // easy to miss!
    applicant.setExistingDebt(10000);
    applicant.setRequestedAmount(200000);
    applicant.setLoanTerm(30);
    applicant.setPropertyValue(250000);

    var result = new LoanAssessor().assess(applicant);

    assertFalse(result.isApproved());
}

The only meaningful difference between these two tests is setCreditScore(780) vs setCreditScore(420), but it’s buried in the middle of twelve identical lines. A well-named builder method fixes this — the important detail jumps out immediately:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

@Test
void approvesLoanForHighCreditScore() {
    var applicant = aTypicalApplicant().withCreditScore(780).build();

    var result = new LoanAssessor().assess(applicant);

    assertTrue(result.isApproved());
}

@Test
void rejectsLoanForLowCreditScore() {
    var applicant = aTypicalApplicant().withCreditScore(420).build();

    var result = new LoanAssessor().assess(applicant);

    assertFalse(result.isApproved());
}

Here, the DRY refactoring into aTypicalApplicant() actually improves expressiveness — it removes noise and highlights what matters. This is DRY and DAMP working together.