0

在工作中,我尽可能多地练习测试驱动开发。我经常遇到的一件事是必须设置一堆 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. 我做错了吗?:) 我应该以另一种方式构建我的软件来完全避免这种情况吗?如果是这样,怎么做?
4

2 回答 2

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 的博客中借来的。

于 2013-05-03T15:07:59.893 回答
1

正如 Erik 正确指出的那样,解决此问题的常用模式是 TestDataBuilder 和 ObjectMother。这些也深入介绍了:Mark Seeman 的高级单元测试课程以及由测试指导的不断增长的面向对象软件,两者都非常好。

在实践中,我发现 Test Data Builder 模式几乎总是比 ObjectMother 模式产生更好、更易读的测试,除了在最简单的情况下(因为你经常需要大量的 objectmother 模式的重载)。

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

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

可能变成:

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

在某些情况下,这可能会导致更简单的测试设置,但并非在所有情况下都有效。

于 2013-05-11T08:45:10.950 回答