[Project] 프로젝트에서 Test Fixture를 어떻게 사용할 지 고민해보기

이상혁·2024년 11월 18일
0

Project

목록 보기
11/12
post-thumbnail

들어가며

프로젝트를 진행을 하면서 테스트 코드를 작성을 해왔습니다. 그러면 자연스럽게 테스트를 위한 Fixture를 쓰게 되었습니다. 사용을 하면서 아무 생각 없이 사용을 하고 있었습니다. 그런데 사용을 하면서 Fixture를 작성을 하면서 코드가 길어지고 가독성이 떨어지는 것을 느끼게 되었습니다. 그래서 Test Fixture를 어떻게 사용을 하면 좋은 지 고민을 하게 되면서 공부를 하게 되었습니다.

Test Fixture가 무언가요?

그러면 위에서 언급을 한 Test Fixture는 무엇일까요?
Test Fixture는 테스트를 하기 전에 테스트에 필요한 환경이나 상태를 설정을 하는 것을 말합니다. 예를 들어 보겠습니다.

만약 제가 event를 생성을 하는 test를 한다고 한다면 category의 정보와 event location, 이벤트 장소의 정보를 필요로 합니다. 즉, event 생성 test 전에 미리 그 정보를 설정을 해주어야 한다는 것이지요. 이렇게 test를 하기 전에 필요한 정보나 환경을 설정을 해주는 것입니다.

여기서 한 가지 중요한 것은 테스트 환경을 일관성 있게 고정된 환경으로 제공을 해야 한다는 것입니다. 이 말 뜻은 모든 테스트가 동일한 환경에서 독립적으로 실행이 될 수 있어야 한다는 의미입니다. 만약에 계속 변동이 되는 환경이 주어지게 된다면 이는 테스트에 대한 신뢰성이 떨어지게 되고 다른 환경을 제공을 해야 하니 중복 되는 코드가 늘어나게 됩니다. 이는 좋은 테스트라고 볼 수 없습니다.

그러므로 Test Fixture를 만들어서 일관되고 고정된 환경을 모든 테스트에 적용을 한다면 코드 중복이 줄어들고 테스트의 신뢰성을 높힐 수 있습니다.

Test Fixture를 사용해보기

먼저, 기존의 사용을 했던 Test Fixture를 살펴보겠습니다.
간단하게 상황을 설명을 하자면 event를 생성하는 테스트와 수정하는 테스트를 진행을 하고자 합니다. 이 때, 두 테스트에 모두 필요로 하는 것이 eventLocation입니다. 그리고 event 또한 필요합니다.

@DisplayName("이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.")
@Test
void createEventTest() {
	// given
	EventRequestDTO dto = EventRequestDTO.builder()
		.eventLocationId(1L)
		.eventName("대한민축 vs 일본 축구 친선경기")
		.eventDescription("축구 경가")
		.date(LocalDate.of(2024, 10, 15))
		.startTime(LocalTime.of(8, 0))
		.build();

	EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, "서울 월드컵 경기장", 50000);
	EventEntity eventEntity = EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntitySeoul)
                .eventName("대한민축 vs 일본 축구 친선경기")
                .eventDescription("축구 경기")
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();

	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntity));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	EventEntity result = eventService.createEvent(dto);

	// then
	assertThat(result).extracting("eventName", "eventDescription", "date", "startTime")
		.contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
	assertThat(result).extracting("eventLocation")
		.isEqualTo(eventLocationEntitySeoul);
}


@DisplayName("수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.")
@Test
void updateEventTest() {
	// given
	EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, "서울 월드컵 경기장", 50000);
	EventLocationEntity eventLocationEntitySuwon = new EventLocationEntity(2L, "수원 빅버드 경기장", 50000);

	EventEntity eventEntity = EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntitySeoul)
                .eventName("대한민축 vs 일본 축구 친선경기")
                .eventDescription("축구 경기")
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();
	EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName("대한민축 vs 일본 축구 친선경기")
                .build();

	eventEntity.update(dto, eventLocationEntitySuwon);

	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

	BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	eventService.updateEvent(1L, dto);

	// then
	assertThat(eventEntity).extracting("eventName", "eventDescription", "date", "startTime")
}
              

현재 코드는 evnet를 생성을 하는 테스트와 수정을 하는 테스트 두 가지 경우가 있습니다. 각각의 메소드에 동일한 eventLocation을 만들어 주었습니다. 하지만 메소드 레벨에서 만들어 주었기 때문에 독립적으로 동작이 가능한 것을 알 수 있습니다.

위 코드에서 동일한 값이 독립적으로 각각의 테스트에 영향을 주지 않지만 같은 코드가 반복이 되면서 중복이 되는 것을 알 수 있습니다. 이를 한 번 개선을 헤보겠습니다.

중복되는 Test Fixture 개선을 해보기

setUp으로 중복 해결하기

첫 번째 방법으로는 setUp 메소드를 사용을 하는 것입니다.

@BeforeEach
void setUp() {
 
 	EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, "서울 월드컵 경기장", 50000);
    EventLocationEntity eventLocationEntitySuwon = new EventLocationEntity(2L, "수원 빅버드 경기장", 50000);
    
    EventEntity eventEntity = EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntity)
                .eventName("대한민축 vs 일본 축구 친선경기")
                .eventDescription("축구 경기")
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();
        
}

@DisplayName("이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.")
@Test
void createEventTest() {
	// given
	EventRequestDTO dto = EventRequestDTO.builder()
		.eventLocationId(1L)
		.eventName("대한민축 vs 일본 축구 친선경기")
		.eventDescription("축구 경가")
		.date(LocalDate.of(2024, 10, 15))
		.startTime(LocalTime.of(8, 0))
		.build();

	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	EventEntity result = eventService.createEvent(dto);

	// then
	assertThat(result).extracting("eventName", "eventDescription", "date", "startTime")
		.contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
	assertThat(result).extracting("eventLocation")
		.isEqualTo(eventLocationEntitySeoul);
}


@DisplayName("수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.")
@Test
void updateEventTest() {
	// given
	EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName("대한민축 vs 일본 축구 친선경기")
                .build();

	eventEntity.update(dto, eventLocationEntitySuwon);

	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

	BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	eventService.updateEvent(1L, dto);

	// then
	assertThat(eventEntity).extracting("eventName", "eventDescription", "date", "startTime")
}

event와 eventLocation을 setUp 메소드로 빼주고 BeforeEach 어노테이션을 달아 주었습니다. BeforeEach 어노테이션을 달아주면 각각 테스트가 실행되기 전에 setUp 메소드를 실행을 시켜줍니다. 각 테스트 메소드는 독립적이기 때문에 setUp 메소드의 값들이 서로 영향을 주지 않고 독립적으로 주어집니다. 그리고 setUp에서 동일한 값들을 관리하기 때문에 중독도 줄어 들고 가독성도 좋아진 것을 확인을 할 수 있습니다.

private 메소드를 사용해서 Test Fixture를 만들기

다른 방법은 클래스 내부에 private 메소드를 사용을 해서 필요한 값을 리턴을 하는 메소드를 만듭니다.

private EventEntity getEventEntity(EventLocationEntity eventLocationEntity, String eventName) {
	return EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntity)
                .eventName(eventName)
                .eventDescription("축구 경기")
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();
}

private EventLocationEntity getEventLocationEntity(Long eventLocationId, String eventLocationName) {
	return new EventLocationEntity(eventLocationId, eventLocationName, 50000);
}

이렇게 클래스 내부에 event를 반환하는 메소드와 eventLocation을 반환을 하는 매소드를 만들어주는 것입니다.

@DisplayName("이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.")
@Test
void createEventTest() {
	// given
	EventRequestDTO dto = EventRequestDTO.builder()
		.eventLocationId(1L)
		.eventName("대한민축 vs 일본 축구 친선경기")
		.eventDescription("축구 경가")
		.date(LocalDate.of(2024, 10, 15))
		.startTime(LocalTime.of(8, 0))
		.build();

	EventLocationEntity eventLocationEntitySeoul = getEventLocationEntity(1L, "서울 월드컵 경기장");
	EventEntity eventEntity = getEventEntity(eventLocationEntity, dto.getEventName());


	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntity));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	EventEntity result = eventService.createEvent(dto);

	// then
	assertThat(result).extracting("eventName", "eventDescription", "date", "startTime")
		.contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
	assertThat(result).extracting("eventLocation")
		.isEqualTo(eventLocationEntitySeoul);
}


@DisplayName("수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.")
@Test
void updateEventTest() {
	// given
	EventLocationEntity eventLocationEntitySeoul = getEventLocationEntity(1L, "서울 월드컵 경기장");
	EventLocationEntity eventLocationEntitySuwon = getEventLocationEntity(2L, "수원 빅버드 경기장");


	EventEntity eventEntity = getEventEntity(eventLocationEntity, dto.getEventName());

	EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName("대한민축 vs 일본 축구 친선경기")
                .build();

	eventEntity.update(dto, eventLocationEntitySuwon);

	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

	BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	eventService.updateEvent(1L, dto);

	// then
	assertThat(eventEntity).extracting("eventName", "eventDescription", "date", "startTime")
}

event와 eventLocation을 반환을 하는 메소드를 분리를 하고 이를 테스트 하는 메소드에서 사용을 합니다. 이로 인해서 가독성이 높아지고 각 테스트 메소드 별로 독립성을 유지를 할 수 있습니다.

별도의 클래스를 만들어서 관리 해주기

이번 방법은 별도의 클래스를 만들어 주는 것입니다.

public class CommonTestFixture {

    public static EventEntity getEventEntity(EventLocationEntity eventLocationEntity, String eventName) {
        return EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntity)
                .eventName(eventName)
                .eventDescription("축구 경기")
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();
    }

    public static EventLocationEntity getEventLocationEntity(Long eventLocationId, String eventLocationName) {
        return new EventLocationEntity(eventLocationId, eventLocationName, 50000);
    }
}

자주 사용이 되는 Test Fixture 같은 경우에는 따로 클래스를 만들어 관리를 해줄 수 있습니다. 왜냐하면 이 Test Fixture들이 event를 만드는 곳에서만 사용을 하는 것이 아닐 수도 있습니다. 나중에 좌석과 관련된 테스트를 진행을 할 수도 있고 티켓을 만드는 테스트를 진행을 할 수 있습니다. 즉, 외부 Test Fixture를 만들어서 여러 곳에서 사용을 할 수 있도록 만들어 주는 것입니다. 이 때 메소드는 static으로 만들어서 CommonTestFixture의 인스턴스 없이 사용할 수 있도록 해줍니다.

@DisplayName("이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.")
@Test
void createEventTest() {
	// given
	EventRequestDTO dto = EventRequestDTO.builder()
		.eventLocationId(1L)
		.eventName("대한민축 vs 일본 축구 친선경기")
		.eventDescription("축구 경가")
		.date(LocalDate.of(2024, 10, 15))
		.startTime(LocalTime.of(8, 0))
		.build();

	EventLocationEntity eventLocationEntitySeoul = CommonTestFixture.getEventLocationEntity(1L, "서울 월드컵 경기장");
	EventEntity eventEntity = CommonTestFixture.getEventEntity(eventLocationEntity, dto.getEventName());


	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntity));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	EventEntity result = eventService.createEvent(dto);

	// then
	assertThat(result).extracting("eventName", "eventDescription", "date", "startTime")
		.contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
	assertThat(result).extracting("eventLocation")
		.isEqualTo(eventLocationEntitySeoul);
}


@DisplayName("수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.")
@Test
void updateEventTest() {
	// given
	EventLocationEntity eventLocationEntitySeoul = CommonTestFixture.getEventLocationEntity(1L, "서울 월드컵 경기장");
	EventLocationEntity eventLocationEntitySuwon = CommonTestFixture.getEventLocationEntity(2L, "수원 빅버드 경기장");


	EventEntity eventEntity = CommonTestFixture.getEventEntity(eventLocationEntity, dto.getEventName());

	EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName("대한민축 vs 일본 축구 친선경기")
                .build();

	eventEntity.update(dto, eventLocationEntitySuwon);

	BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

	BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

	BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

	// when
	eventService.updateEvent(1L, dto);

	// then
	assertThat(eventEntity).extracting("eventName", "eventDescription", "date", "startTime")
}

이제 테스트 코드에서 바로 사용을 하기만 하면 됩니다. 외부 클래스를 이용을 하면 코드의 가독성이나 어느 곳에서 사용이 가능하도록 할 수 있습니다.

어느 방법을 사용할까요?

위에 3가지의 방법에 대해서 알아 보았습니다. 이제 3가지 방법중 어느 방법을 쓰는 것이 좋을까요?
저의 생각을 말하자면 setUp으로 Test Fixture를 만드는 방법은 지양을 한다고 이야기하고 싶습니다.
이유를 말하기 전에 3가지 방법은 상황에 따라 적합한 방법이 있고 setUp방법을 지양한다고 해서 setUp방법이 나쁜 방법은 아니라고 말하고 싶습니다.

이유를 말하자면 setUp방식의 경우 가독성이 두 방법보다 떨어진다고 생각을 합니다. 그 이유는 setUp이라는 메소드를 만들어서 거기서 필요한 정보나 환경을 한번에 설정을 해주는데 각 테스트마다 어떤 정보가 일일히 확인을 해야 합니다. 이는 오히려 불편함을 초래합니다. 그리고 필요가 없는 정보를 생성을 하게 됩니다. setUp 메소드는 모든 테스트를 할 때 실행이 됩니다. 그런데 setUp메소드에서 만든 정보가 테스트에 필요없는 정보가 있게 되면 낭비가 되게 됩니다. 마지막으로 만약 setUp에서 하나의 정보를 변경을 하게 된다면 변경된 정보가 다른 테스트에도 영향을 미치게 됩니다.

EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, "서울 월드컵 경기장", 50000);

setUp 메소드 예시에 있었던 event location입니다. 현재 서울 월드컵 경기장으로 만들어져 있습니다.

EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, "포항 스틸야드 경기장", 50000);

그리고 이를 포항 스틸야드 경기장으로 변경을 했습니다. 그렇게 되면 모든 테스트가 서울 월드컵 경기장에서 포항 스틸야드 경기장으로 변경이 되는 것입니다. 물론 지금 테스트에서 스틸야드 경기장으로 변경을 한다고 해서 그게 문제가 될 것은 없지만 만약 영향을 주는 정보라면 오히려 다른 테스트에 영향을 줘서 테스트 간에 독립적인 테스트가 안 될 수 있습니다.

그래서 저는 setUp으로 Test Fixture를 하는 것을 지양을 합니다. 물론 아예 안 쓴다는 것이 아니라 가급적 사용을 안하겠다 라는 것입니다.

마무리

이번 포스트에서는 Test Fixture에 대해서 알아보고 어떤 사용을 할 수 있는지 알아보았습니다. 이번 포스트의 내용은 정답은 아니고 지극히 개인적으로 고민을 해봤을 때 난 이렇게 쓰는 게 좋을 것 갔다는 의견입니다. 그러니 가볍게 보시고 개인적인 생각인 것을 참고 주시면 감사하겠습니다.

profile
꾸준히!

0개의 댓글