处理测试中的代码重复

时间:2013-05-03 10:26:50

标签: testing tdd installation dto

在工作中,我尽可能地练习测试驱动开发。我经常最终得到的一件事是必须设置一堆DTO,当这些结构稍微复杂时,这就变成了很多代码。这个问题是代码通常非常具有反复性,我觉得它会分散测试的主要目的。例如,使用一个稍微修改和浓缩的例子(在java,jUnit + mockito中):

class BookingServiceTest {

    private final static int HOUR_IN_MILLIS = 60 * 60 * 1000;
    private final static int LOCATION_ID = 1;
    @Mock
    private BookingLocationDao bookingLocationDao;

    @InjectMocks
    private BookingService service = new BookingService();

    @Test
    public void yieldsAWarningWhenABookingOverlapsAnotherInTheSameLocation() {
        // This part is a bit repetetive over many tests:
        Date now = new Date()
        Location location = new Location()
        location.setId(LOCATION_ID);

        Booking existingBooking = new Booking()
        existingBooking.setStart(now);
        existingBooking.setDuration(HOUR_IN_MILLIS);
        existingBooking.setLocation(location);
        // To here

        when(bookingLocationDao.findBookingsAtLocation(LOCATION_ID))
            .thenReturn(Arrays.asList(existingBooking));

        // Then again setting up a booking :\
        Booking newBooking = new Booking();
        newBooking.setStart(now);
        newBooking.setDuration(HOUR_IN_MILLIS / 2);
        newBooking.setLocation(location);               


        // Actual test...
        BookingResult result = service.book(newBooking);

        assertThat(result.getWarnings(), hasSize(1));
        assertThat(result.getWarnings().get(0).getType(), is(BookingWarningType.OVERLAPING_BOOKING));
    }

}

在这个例子中,设置并不复杂,所以我不会想太多。但是,当需要更复杂的输入时,用于设置方法输入的代码往往会增长。在几个测试中使用的类似输入会加剧问题。将设置代码重构为单独的TestUtil类有点帮助。那么问题是,在几个月后编写新测试时,找到这些实用程序类有点困难,这会导致重复。

  1. 处理这种“复杂”DTO的好方法是什么,以尽量减少测试设置中的代码重复?
  2. 在使用类似代码时,如何确保找到提取的TestUtilities?
  3. 我做错了吗? :)我应该以另一种方式构建我的软件,以避免这种情况吗?如果是这样,怎么样?

2 个答案:

答案 0 :(得分:2)

有几种模式可以解决这种情况:

有关这些模式的深入讨论,请查看优秀的书籍"Growing Object-oriented Software Guided by Tests"

测试数据生成器

为每个要为其实例化/设置的类创建一个构建器类。该类包含一组方法,用于设置在特定状态下构建的对象。通常这些辅助方法会将一个实例返回给构建器类,以便可以以流畅的方式链接调用。

// Example of a builder class:
public class InvoiceBuilder {
    Recipient recipient = new RecipientBuilder().build();
    InvoiceLines lines = new InvoiceLines(new InvoiceLineBuilder().build());
    PoundsShillingsPence discount = PoundsShillingsPence.ZERO;

    public InvoiceBuilder withRecipient(Recipient recipient) {
        this.recipient = recipient;
        return this;
    }

    public InvoiceBuilder withInvoiceLines(InvoiceLines lines) {
        this.lines = lines;
        return this;
    }

    public InvoiceBuilder withDiscount(PoundsShillingsPence discount) {
        this.discount = discount;
        return this;
    }

    public Invoice build() {
        return new Invoice(recipient, lines, discount);
    }
}

// Usage:
Invoice invoiceWithNoPostcode = new InvoiceBuilder()
    .withRecipient(new RecipientBuilder()
        .withAddress(new AddressBuilder()
            .withNoPostcode()
            .build())
        .build())
    .build();

对象母亲

对象母亲是一个为不同的常见场景提供预制测试数据的类。

Invoice invoice = TestInvoices.newDeerstalkerAndCapeInvoice();

以上示例来自Nat Pryce's blog

答案 1 :(得分:1)

正如Erik正确地指出解决这个问题的常用模式是TestDataBuilder和ObjectMother。这些内容也在以下内容中进行了深入介绍:Mark Seemans advanced unit testing course以及通过测试引导的不断增长的面向对象软件,两者都非常好。

在实践中,我发现测试数据生成器模式几乎总是会导致比ObjectMother模式更好,更易读的测试,除非在最简单的情况下(因为您经常需要对象母模式的过多数量的重载)。

您可以使用的另一个技巧是将测试对象构建器模式的多组设置组合成单个方法,例如。

Invoice invoiceWithNoPostcode = new InvoiceBuilder()
    .withRecipient(new RecipientBuilder()
        .withAddress(new AddressBuilder()
            .withNoPostcode()
            .build())
        .build())
    .build();

可能成为:

new InvoiceBuilder().WithNoPostCode().Build();

在某些情况下,这可能会导致更简单的测试设置,但并不适用于所有情况。