Skip to the content.

Command Factory Test Guide — 단위 테스트

CommandFactory는 Command → Domain 변환PersistBundle 생성을 담당합니다.

순수 변환 로직이므로 단위 테스트만 작성합니다.


1) 테스트 전략

테스트 유형 목적 범위
단위 테스트 변환 로직 검증 CommandFactory만

테스트 포인트

항목 검증 내용
Command → Domain 변환 모든 필드 올바르게 매핑
하위 객체 변환 중첩 Command 처리
PersistBundle 생성 여러 객체 올바르게 묶음
null 처리 선택 필드 null 안전
컬렉션 변환 List 원소 변환 정확성

2) 테스트 구조

application/
└─ src/
   ├─ main/java/
   │  └─ com/ryuqq/application/{bc}/factory/command/
   │      └─ {Bc}CommandFactory.java
   └─ test/java/
      └─ com/ryuqq/application/{bc}/factory/command/
          └─ {Bc}CommandFactoryTest.java

3) 단위 테스트 예시

기본 테스트

package com.ryuqq.application.order.factory.command;

import com.ryuqq.application.order.dto.command.PlaceOrderCommand;
import com.ryuqq.application.order.dto.command.OrderItemCommand;
import com.ryuqq.application.order.dto.bundle.OrderPersistBundle;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.aggregate.OrderItem;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;

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

@DisplayName("OrderCommandFactory 단위 테스트")
class OrderCommandFactoryTest {

    private OrderCommandFactory factory;

    @BeforeEach
    void setUp() {
        factory = new OrderCommandFactory();
    }

    @Nested
    @DisplayName("create 테스트")
    class CreateTest {

        @Test
        @DisplayName("PlaceOrderCommand를 Order로 변환한다")
        void shouldConvertCommandToOrder() {
            // given
            OrderItemCommand itemCommand = new OrderItemCommand(
                1001L,  // productId
                2,      // quantity
                new BigDecimal("10000")  // unitPrice
            );

            PlaceOrderCommand command = new PlaceOrderCommand(
                100L,  // customerId
                List.of(itemCommand)
            );

            // when
            Order order = factory.create(command);

            // then
            assertThat(order).isNotNull();
            assertThat(order.getCustomerId().value()).isEqualTo(100L);
            assertThat(order.getItems()).hasSize(1);
        }

        @Test
        @DisplayName("여러 OrderItem을 변환한다")
        void shouldConvertMultipleItems() {
            // given
            List<OrderItemCommand> itemCommands = List.of(
                new OrderItemCommand(1001L, 2, new BigDecimal("10000")),
                new OrderItemCommand(1002L, 3, new BigDecimal("20000")),
                new OrderItemCommand(1003L, 1, new BigDecimal("30000"))
            );

            PlaceOrderCommand command = new PlaceOrderCommand(100L, itemCommands);

            // when
            Order order = factory.create(command);

            // then
            assertThat(order.getItems()).hasSize(3);
        }

        @Test
        @DisplayName("OrderItem의 모든 필드가 올바르게 변환된다")
        void shouldConvertAllFieldsCorrectly() {
            // given
            OrderItemCommand itemCommand = new OrderItemCommand(
                1001L,
                5,
                new BigDecimal("15000")
            );

            PlaceOrderCommand command = new PlaceOrderCommand(
                100L,
                List.of(itemCommand)
            );

            // when
            Order order = factory.create(command);

            // then
            OrderItem item = order.getItems().get(0);
            assertThat(item.getProductId().value()).isEqualTo(1001L);
            assertThat(item.getQuantity().value()).isEqualTo(5);
            assertThat(item.getUnitPrice().value()).isEqualByComparingTo(new BigDecimal("15000"));
        }
    }

    @Nested
    @DisplayName("createBundle 테스트")
    class CreateBundleTest {

        @Test
        @DisplayName("Order와 OutboxEvent를 PersistBundle로 묶는다")
        void shouldCreateBundleWithOrderAndEvent() {
            // given
            PlaceOrderCommand command = new PlaceOrderCommand(
                100L,
                List.of(new OrderItemCommand(1001L, 1, new BigDecimal("10000")))
            );

            // when
            OrderPersistBundle bundle = factory.createBundle(command);

            // then
            assertThat(bundle).isNotNull();
            assertThat(bundle.order()).isNotNull();
            assertThat(bundle.outboxEvent()).isNotNull();
            assertThat(bundle.outboxEvent().getEventType()).isEqualTo("OrderPlaced");
        }

        @Test
        @DisplayName("PersistBundle의 OutboxEvent는 aggregateId가 null이다")
        void shouldCreateOutboxEventWithNullAggregateId() {
            // given
            PlaceOrderCommand command = new PlaceOrderCommand(
                100L,
                List.of(new OrderItemCommand(1001L, 1, new BigDecimal("10000")))
            );

            // when
            OrderPersistBundle bundle = factory.createBundle(command);

            // then
            assertThat(bundle.outboxEvent().getAggregateId()).isNull();
        }

        @Test
        @DisplayName("enrichWithId로 ID 할당 후 새 Bundle이 반환된다")
        void shouldEnrichWithIdReturnNewBundle() {
            // given
            PlaceOrderCommand command = new PlaceOrderCommand(
                100L,
                List.of(new OrderItemCommand(1001L, 1, new BigDecimal("10000")))
            );
            OrderPersistBundle originalBundle = factory.createBundle(command);

            // when
            OrderPersistBundle enrichedBundle = originalBundle.enrichWithId(new OrderId(999L));

            // then
            assertThat(enrichedBundle).isNotSameAs(originalBundle);
            assertThat(enrichedBundle.outboxEvent().getAggregateId()).isEqualTo(999L);
        }
    }

    @Nested
    @DisplayName("복잡한 변환 테스트")
    class ComplexConversionTest {

        @Test
        @DisplayName("AddressCommand를 Address로 변환한다")
        void shouldConvertAddressCommand() {
            // given
            AddressCommand addressCommand = new AddressCommand(
                "123 Main St",
                "Seoul",
                "12345",
                "Korea"
            );

            PlaceOrderCommand command = new PlaceOrderCommand(
                100L,
                List.of(new OrderItemCommand(1001L, 1, new BigDecimal("10000"))),
                addressCommand
            );

            // when
            Order order = factory.create(command);

            // then
            Address address = order.getShippingAddress();
            assertThat(address.getStreet()).isEqualTo("123 Main St");
            assertThat(address.getCity()).isEqualTo("Seoul");
            assertThat(address.getZipCode()).isEqualTo("12345");
            assertThat(address.getCountry()).isEqualTo("Korea");
        }

        @Test
        @DisplayName("빈 아이템 리스트는 빈 Order 아이템이 된다")
        void shouldHandleEmptyItemList() {
            // given
            PlaceOrderCommand command = new PlaceOrderCommand(100L, List.of());

            // when
            Order order = factory.create(command);

            // then
            assertThat(order.getItems()).isEmpty();
        }
    }
}

4) 테스트 체크리스트

Command → Domain 변환

PersistBundle 생성

엣지 케이스


5) Do / Don’t

✅ Good

// ✅ Good: 순수 단위 테스트 (Mock 불필요)
@BeforeEach
void setUp() {
    factory = new OrderCommandFactory();  // 직접 생성
}

// ✅ Good: 모든 필드 검증
assertThat(order.getCustomerId().value()).isEqualTo(100L);
assertThat(order.getItems()).hasSize(1);

// ✅ Good: PersistBundle 구성 요소 검증
assertThat(bundle.order()).isNotNull();
assertThat(bundle.outboxEvent()).isNotNull();

// ✅ Good: VO 값 검증
assertThat(item.getProductId().value()).isEqualTo(1001L);

// ✅ Good: enrichWithId 불변성 검증
assertThat(enrichedBundle).isNotSameAs(originalBundle);

❌ Bad

// ❌ Bad: Mock 사용 (Factory는 순수 변환)
@Mock
private SomePort somePort;  // ❌ Factory는 Port 의존 안 함

// ❌ Bad: 비즈니스 로직 테스트 (Domain 책임)
assertThat(order.calculateTotal()).isEqualTo(...);  // ❌ Domain 테스트

// ❌ Bad: 트랜잭션 테스트 (Factory는 트랜잭션 없음)
@Transactional
void testCreate() { ... }  // ❌

// ❌ Bad: 데이터베이스 접근 테스트
@SpringBootTest
class OrderCommandFactoryTest { ... }  // ❌ 단위 테스트로 충분

6) Fixture 활용

TestFixtures 사용

import com.ryuqq.fixture.application.PlaceOrderCommandFixture;

@DisplayName("OrderCommandFactory 단위 테스트")
class OrderCommandFactoryTest {

    private OrderCommandFactory factory;

    @BeforeEach
    void setUp() {
        factory = new OrderCommandFactory();
    }

    @Test
    @DisplayName("기본 Command를 Order로 변환한다")
    void shouldConvertDefaultCommand() {
        // given
        PlaceOrderCommand command = PlaceOrderCommandFixture.create();

        // when
        Order order = factory.create(command);

        // then
        assertThat(order).isNotNull();
    }

    @Test
    @DisplayName("커스텀 Command를 Order로 변환한다")
    void shouldConvertCustomCommand() {
        // given
        PlaceOrderCommand command = PlaceOrderCommandFixture.builder()
            .customerId(999L)
            .itemCount(5)
            .build();

        // when
        Order order = factory.create(command);

        // then
        assertThat(order.getCustomerId().value()).isEqualTo(999L);
        assertThat(order.getItems()).hasSize(5);
    }
}

Fixture 정의

// application/src/testFixtures/java/com/ryuqq/fixture/application/PlaceOrderCommandFixture.java
public final class PlaceOrderCommandFixture {

    private PlaceOrderCommandFixture() {}

    public static PlaceOrderCommand create() {
        return builder().build();
    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private Long customerId = 100L;
        private int itemCount = 1;
        private BigDecimal unitPrice = new BigDecimal("10000");

        public Builder customerId(Long customerId) {
            this.customerId = customerId;
            return this;
        }

        public Builder itemCount(int itemCount) {
            this.itemCount = itemCount;
            return this;
        }

        public Builder unitPrice(BigDecimal unitPrice) {
            this.unitPrice = unitPrice;
            return this;
        }

        public PlaceOrderCommand build() {
            List<OrderItemCommand> items = IntStream.range(0, itemCount)
                .mapToObj(i -> new OrderItemCommand(
                    1000L + i,
                    1,
                    unitPrice
                ))
                .toList();

            return new PlaceOrderCommand(customerId, items);
        }
    }
}

7) 관련 문서


작성자: Development Team 최종 수정일: 2025-12-04 버전: 1.0.0