Skip to the content.

Assembler 테스트 가이드

목적: Assembler의 단위 테스트 전략 (순수 Java 기반)

핵심: Domain → Response 변환만 테스트 (toDomain 테스트 없음!)


1) 테스트 전략

테스트 대상

Assembler는 Domain → Response 변환만 검증합니다:

✅ 테스트 항목:
1. Domain → Response 변환 검증
2. Domain → Detail Response 변환 검증
3. List<Domain> → List<Response> 변환 검증
4. 빈 List 처리 검증
5. null 처리 검증
6. 여러 Domain 조립 검증 (선택적)

❌ 테스트하지 않는 항목:
1. Command → Domain 변환 (Creator 테스트로!)
2. Query → Criteria 변환 (UseCase 테스트로!)
3. 비즈니스 로직 (Domain 테스트로!)
4. PageResponse 조립 (UseCase 테스트로!)

테스트 범위


2) 기본 템플릿

package com.ryuqq.application.{bc}.assembler;

import com.ryuqq.application.{bc}.dto.response.{Bc}Response;
import com.ryuqq.application.{bc}.dto.response.{Bc}DetailResponse;
import com.ryuqq.domain.{bc}.aggregate.{bc}.{Bc};
import com.ryuqq.domain.{bc}.vo.{Bc}Id;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;

import static org.assertj.core.api.Assertions.*;

/**
 * {Bc} Assembler 단위 테스트
 *
 * <p>Assembler는 Domain → Response 변환만 테스트합니다.</p>
 * <p>Command → Domain 변환은 Creator 테스트에서 검증합니다.</p>
 *
 * @author development-team
 * @since 3.0.0
 */
@Tag("unit")
@Tag("assembler")
@Tag("application-layer")
@DisplayName("{Bc} Assembler 단위 테스트")
class {Bc}AssemblerTest {

    private {Bc}Assembler assembler;
    private Clock fixedClock;

    @BeforeEach
    void setUp() {
        assembler = new {Bc}Assembler();
        fixedClock = Clock.fixed(
            Instant.parse("2025-01-01T10:00:00Z"),
            ZoneId.of("UTC")
        );
    }

    @Nested
    @DisplayName("toResponse")
    class ToResponseTest {

        @Test
        @DisplayName("Domain → Response 변환이 올바르게 동작해야 한다")
        void shouldConvertDomainToResponse() {
            // Given
            {Bc} domain = createTestDomain();

            // When
            {Bc}Response result = assembler.toResponse(domain);

            // Then
            assertThat(result).isNotNull();
            assertThat(result.id()).isEqualTo(domain.id().value());
            assertThat(result.status()).isEqualTo(domain.status().name());
            assertThat(result.createdAt()).isEqualTo(domain.createdAt());
        }
    }

    @Nested
    @DisplayName("toResponseList")
    class ToResponseListTest {

        @Test
        @DisplayName("List<Domain> → List<Response> 변환이 올바르게 동작해야 한다")
        void shouldConvertListCorrectly() {
            // Given
            List<{Bc}> domains = List.of(
                createTestDomain(),
                createTestDomain()
            );

            // When
            List<{Bc}Response> result = assembler.toResponseList(domains);

            // Then
            assertThat(result).hasSize(2);
            assertThat(result.get(0)).isNotNull();
            assertThat(result.get(1)).isNotNull();
        }

        @Test
        @DisplayName("빈 List를 전달하면 빈 List를 반환해야 한다")
        void shouldReturnEmptyListForEmptyInput() {
            // When
            List<{Bc}Response> result = assembler.toResponseList(List.of());

            // Then
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("null을 전달하면 빈 List를 반환해야 한다")
        void shouldReturnEmptyListForNull() {
            // When
            List<{Bc}Response> result = assembler.toResponseList(null);

            // Then
            assertThat(result).isEmpty();
        }
    }

    // ==================== Test Fixtures ====================

    private {Bc} createTestDomain() {
        return {Bc}.forNew(
            /* 필수 파라미터 */
            fixedClock
        );
    }
}

3) 실전 예시 (Order)

package com.ryuqq.application.order.assembler;

import com.ryuqq.application.order.dto.response.OrderResponse;
import com.ryuqq.application.order.dto.response.OrderDetailResponse;
import com.ryuqq.domain.order.aggregate.order.Order;
import com.ryuqq.domain.order.aggregate.order.OrderLineItem;
import com.ryuqq.domain.order.vo.OrderId;
import com.ryuqq.domain.order.vo.OrderStatus;
import com.ryuqq.domain.order.vo.Money;
import com.ryuqq.domain.order.vo.Quantity;
import com.ryuqq.domain.member.vo.MemberId;
import com.ryuqq.domain.product.vo.ProductId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;

import static org.assertj.core.api.Assertions.*;

/**
 * Order Assembler 단위 테스트
 *
 * <p>Assembler는 Domain → Response 변환만 테스트합니다.</p>
 * <p>Command → Domain 변환은 OrderCreator 테스트에서 검증합니다.</p>
 *
 * @author development-team
 * @since 3.0.0
 */
@Tag("unit")
@Tag("assembler")
@Tag("application-layer")
@DisplayName("Order Assembler 단위 테스트")
class OrderAssemblerTest {

    private OrderAssembler assembler;
    private Clock fixedClock;

    @BeforeEach
    void setUp() {
        assembler = new OrderAssembler();
        fixedClock = Clock.fixed(
            Instant.parse("2025-01-01T10:00:00Z"),
            ZoneId.of("UTC")
        );
    }

    @Nested
    @DisplayName("toResponse")
    class ToResponseTest {

        @Test
        @DisplayName("Domain → Response 변환이 올바르게 동작해야 한다")
        void shouldConvertDomainToResponse() {
            // Given
            Order order = createTestOrder();

            // When
            OrderResponse result = assembler.toResponse(order);

            // Then
            assertThat(result).isNotNull();
            assertThat(result.orderId()).isEqualTo(order.id().value());
            assertThat(result.customerId()).isEqualTo(order.customerId().value());
            assertThat(result.totalAmount()).isEqualTo(order.totalAmount().value());
            assertThat(result.status()).isEqualTo(order.status().name());
            assertThat(result.createdAt()).isEqualTo(order.createdAt());
        }

        @Test
        @DisplayName("모든 필드가 올바르게 매핑되어야 한다")
        void shouldMapAllFieldsCorrectly() {
            // Given
            Order order = createTestOrder();

            // When
            OrderResponse result = assembler.toResponse(order);

            // Then: 모든 필드 검증
            assertThat(result.orderId()).isNotNull();
            assertThat(result.customerId()).isNotNull();
            assertThat(result.totalAmount()).isNotNull();
            assertThat(result.status()).isNotNull();
            assertThat(result.createdAt()).isNotNull();
        }
    }

    @Nested
    @DisplayName("toDetailResponse")
    class ToDetailResponseTest {

        @Test
        @DisplayName("Domain → DetailResponse 변환이 올바르게 동작해야 한다")
        void shouldConvertToDetailResponse() {
            // Given
            Order order = createTestOrderWithLineItems();

            // When
            OrderDetailResponse result = assembler.toDetailResponse(order);

            // Then
            assertThat(result).isNotNull();
            assertThat(result.orderId()).isEqualTo(order.id().value());
            assertThat(result.lineItems()).hasSize(2);
        }

        @Test
        @DisplayName("LineItem 목록이 올바르게 변환되어야 한다")
        void shouldConvertLineItemsCorrectly() {
            // Given
            Order order = createTestOrderWithLineItems();

            // When
            OrderDetailResponse result = assembler.toDetailResponse(order);

            // Then
            assertThat(result.lineItems()).hasSize(2);
            assertThat(result.lineItems().get(0).productId()).isNotNull();
            assertThat(result.lineItems().get(0).quantity()).isPositive();
            assertThat(result.lineItems().get(0).unitPrice()).isNotNull();
        }
    }

    @Nested
    @DisplayName("toResponseList")
    class ToResponseListTest {

        @Test
        @DisplayName("List<Order> → List<OrderResponse> 변환이 올바르게 동작해야 한다")
        void shouldConvertListCorrectly() {
            // Given
            List<Order> orders = List.of(
                createTestOrder(Money.of(BigDecimal.valueOf(10000))),
                createTestOrder(Money.of(BigDecimal.valueOf(20000))),
                createTestOrder(Money.of(BigDecimal.valueOf(30000)))
            );

            // When
            List<OrderResponse> result = assembler.toResponseList(orders);

            // Then
            assertThat(result).hasSize(3);
            assertThat(result.get(0).totalAmount()).isEqualTo(BigDecimal.valueOf(10000));
            assertThat(result.get(1).totalAmount()).isEqualTo(BigDecimal.valueOf(20000));
            assertThat(result.get(2).totalAmount()).isEqualTo(BigDecimal.valueOf(30000));
        }

        @Test
        @DisplayName("빈 List를 전달하면 빈 List를 반환해야 한다")
        void shouldReturnEmptyListForEmptyInput() {
            // When
            List<OrderResponse> result = assembler.toResponseList(List.of());

            // Then
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("null을 전달하면 빈 List를 반환해야 한다")
        void shouldReturnEmptyListForNull() {
            // When
            List<OrderResponse> result = assembler.toResponseList(null);

            // Then
            assertThat(result).isEmpty();
        }

        @Test
        @DisplayName("각 Response의 ID가 원본 Domain과 일치해야 한다")
        void shouldPreserveIdMapping() {
            // Given
            Order order1 = createTestOrder();
            Order order2 = createTestOrder();
            List<Order> orders = List.of(order1, order2);

            // When
            List<OrderResponse> result = assembler.toResponseList(orders);

            // Then
            assertThat(result.get(0).orderId()).isEqualTo(order1.id().value());
            assertThat(result.get(1).orderId()).isEqualTo(order2.id().value());
        }
    }

    @Nested
    @DisplayName("toSummaryResponse (여러 Domain 조립)")
    class ToSummaryResponseTest {

        @Test
        @DisplayName("Order와 Member를 조합하여 SummaryResponse를 생성해야 한다")
        void shouldCombineMultipleDomains() {
            // Given
            Order order = createTestOrder();
            String memberName = "홍길동";  // UseCase에서 조회한 값

            // When
            // OrderSummaryResponse result = assembler.toSummaryResponse(order, memberName);

            // Then
            // assertThat(result.memberName()).isEqualTo(memberName);
            // assertThat(result.orderId()).isEqualTo(order.id().value());
        }
    }

    // ==================== Test Fixtures ====================

    private Order createTestOrder() {
        return createTestOrder(Money.of(BigDecimal.valueOf(50000)));
    }

    private Order createTestOrder(Money totalAmount) {
        return Order.reconstitute(
            OrderId.of(1L),
            MemberId.of(100L),
            totalAmount,
            OrderStatus.CREATED,
            List.of(),
            fixedClock.instant(),
            fixedClock.instant(),
            fixedClock
        );
    }

    private Order createTestOrderWithLineItems() {
        List<OrderLineItem> lineItems = List.of(
            createTestLineItem(1L, 10000, 2),
            createTestLineItem(2L, 20000, 1)
        );

        return Order.reconstitute(
            OrderId.of(1L),
            MemberId.of(100L),
            Money.of(BigDecimal.valueOf(40000)),
            OrderStatus.CREATED,
            lineItems,
            fixedClock.instant(),
            fixedClock.instant(),
            fixedClock
        );
    }

    private OrderLineItem createTestLineItem(Long id, int price, int quantity) {
        return OrderLineItem.reconstitute(
            OrderLineItemId.of(id),
            ProductId.of(id * 10),
            "상품" + id,
            Money.of(BigDecimal.valueOf(price)),
            Quantity.of(quantity)
        );
    }
}

4) Do / Don’t

❌ Bad Examples

// ❌ Spring Context 로딩 (불필요!)
@SpringBootTest
class OrderAssemblerTest {
    // Assembler는 Spring 의존성이 없어서 Context 불필요!
}

// ❌ Mock 사용 (Assembler는 의존성이 없음!)
@ExtendWith(MockitoExtension.class)
class OrderAssemblerTest {
    @Mock private Order order;  // 실제 Domain 객체 사용!
}

// ❌ Command → Domain 변환 테스트 (Creator 테스트로!)
@Test
void toDomain_ShouldConvertCommandToDomain() {
    PlaceOrderCommand command = new PlaceOrderCommand(...);
    Order order = assembler.toDomain(command);  // ❌ Assembler에 이 메서드 없음!
}

// ❌ 비즈니스 로직 테스트 (Domain 테스트로!)
@Test
void toResponse_WithBusinessLogic() {
    Order order = createTestOrder();
    order.confirm();  // ❌ 비즈니스 로직은 Domain Test에서!
    OrderResponse result = assembler.toResponse(order);
}

// ❌ PageResponse 조립 테스트 (UseCase 테스트로!)
@Test
void toPageResponse_ShouldConvert() {
    // ❌ PageResponse 조립은 UseCase 책임!
}

// ❌ Port/Repository Mock 사용 (Assembler는 의존성 없음!)
@Test
void toResponse_WithMockedPort() {
    when(memberQueryPort.findById(any())).thenReturn(member);  // ❌
}

✅ Good Examples

// ✅ 순수 Java 단위 테스트
@Tag("unit")
@Tag("assembler")
@Tag("application-layer")
class OrderAssemblerTest {
    private OrderAssembler assembler;

    @BeforeEach
    void setUp() {
        assembler = new OrderAssembler();  // 직접 인스턴스 생성
    }
}

// ✅ 실제 Domain 객체 사용
@Test
void toResponse_ShouldConvertCorrectly() {
    // Given: 실제 Domain 객체
    Order order = Order.reconstitute(...);

    // When
    OrderResponse result = assembler.toResponse(order);

    // Then
    assertThat(result.orderId()).isEqualTo(order.id().value());
}

// ✅ Domain → Response 변환만 테스트
@Test
void toResponse_ShouldMapAllFields() {
    Order order = createTestOrder();

    OrderResponse result = assembler.toResponse(order);

    assertThat(result.orderId()).isEqualTo(order.id().value());
    assertThat(result.status()).isEqualTo(order.status().name());
}

// ✅ List 변환 테스트
@Test
void toResponseList_ShouldConvertList() {
    List<Order> orders = List.of(createTestOrder(), createTestOrder());

    List<OrderResponse> result = assembler.toResponseList(orders);

    assertThat(result).hasSize(2);
}

// ✅ 경계 조건 테스트
@Test
void toResponseList_ShouldHandleEmptyAndNull() {
    assertThat(assembler.toResponseList(List.of())).isEmpty();
    assertThat(assembler.toResponseList(null)).isEmpty();
}

5) 테스트 시나리오

Domain → Response 변환

@Test
@DisplayName("Domain 필드가 Response에 올바르게 매핑되어야 한다")
void toResponse_ShouldMapAllFields() {
    // Given
    Order order = createTestOrder();

    // When
    OrderResponse result = assembler.toResponse(order);

    // Then
    assertThat(result.orderId()).isEqualTo(order.id().value());
    assertThat(result.customerId()).isEqualTo(order.customerId().value());
    assertThat(result.totalAmount()).isEqualTo(order.totalAmount().value());
    assertThat(result.status()).isEqualTo(order.status().name());
    assertThat(result.createdAt()).isEqualTo(order.createdAt());
}

Nested Response 변환

@Test
@DisplayName("중첩 객체가 올바르게 변환되어야 한다")
void toDetailResponse_ShouldConvertNestedObjects() {
    // Given
    Order order = createTestOrderWithLineItems();

    // When
    OrderDetailResponse result = assembler.toDetailResponse(order);

    // Then
    assertThat(result.lineItems()).hasSize(2);
    assertThat(result.lineItems().get(0).productId()).isNotNull();
}

빈 컬렉션 처리

@Test
@DisplayName("빈 List를 전달하면 빈 List를 반환해야 한다")
void toResponseList_ShouldHandleEmptyList() {
    // When
    List<OrderResponse> result = assembler.toResponseList(List.of());

    // Then
    assertThat(result).isEmpty();
}

Null 처리

@Test
@DisplayName("null을 전달하면 빈 List를 반환해야 한다")
void toResponseList_ShouldHandleNull() {
    // When
    List<OrderResponse> result = assembler.toResponseList(null);

    // Then
    assertThat(result).isEmpty();
}

6) 테스트 Fixtures

Clock 고정

private Clock fixedClock;

@BeforeEach
void setUp() {
    fixedClock = Clock.fixed(
        Instant.parse("2025-01-01T10:00:00Z"),
        ZoneId.of("UTC")
    );
}

Domain 생성 헬퍼

private Order createTestOrder() {
    return Order.reconstitute(
        OrderId.of(1L),
        MemberId.of(100L),
        Money.of(BigDecimal.valueOf(50000)),
        OrderStatus.CREATED,
        List.of(),
        fixedClock.instant(),
        fixedClock.instant(),
        fixedClock
    );
}

private Order createTestOrderWithLineItems() {
    List<OrderLineItem> lineItems = List.of(
        createTestLineItem(1L, 10000, 2),
        createTestLineItem(2L, 20000, 1)
    );

    return Order.reconstitute(
        OrderId.of(1L),
        MemberId.of(100L),
        Money.of(BigDecimal.valueOf(40000)),
        OrderStatus.CREATED,
        lineItems,
        fixedClock.instant(),
        fixedClock.instant(),
        fixedClock
    );
}

7) 체크리스트

Assembler 테스트 작성 시:


8) 성능 고려사항

빠른 실행

Assembler 테스트는 외부 의존성이 없어 밀리초 단위로 실행됩니다:

@Test
@DisplayName("Assembler 테스트는 10ms 이하로 실행되어야 한다")
void assembler_ShouldExecuteQuickly() {
    // Given
    long startTime = System.currentTimeMillis();
    Order order = createTestOrder();

    // When
    OrderResponse response = assembler.toResponse(order);
    List<OrderResponse> responses = assembler.toResponseList(List.of(order, order));

    // Then
    long duration = System.currentTimeMillis() - startTime;
    assertThat(duration).isLessThan(10);  // 10ms 이하
}

9) 관련 문서


작성자: Development Team 최종 수정일: 2025-12-04 버전: 3.0.0 (Domain → Response 변환 전용)