A. Fixture란.
- Fixture는 테스트가 실행하는 환경을 위해 필요한 설정을 완료해 주는 것입니다. 소프트웨어 테스트에서는 테스트에 쓰일 수 있도록 필요한 환경과 설정을 완료 해 준 객체라고 할 수 있을 것 같습니다.
- Fixture 라는 용어는 소프트웨어 테스트에서만 쓰이지 않고 회로나 디바이스 테스트에서도 쓰이며 전기 신호를 일정하게 컨트롤 하거나 물리적 장치를 고정하는 용도로 쓰이는 것을 말합니다. 즉, 테스트 할 수 있는 환경을 만들어 놓은 것입니다.
- Fixture는 Test Double을 만들어서 사용할 수도 있고 실제 값을 가진 객체를 사용할 수도 있는데 목에서 실제 값을 가진 객체를 이용하는 방법까지의 개인적인 여정을 다뤘습니다.
- Fixture를 셋업하는 방식은 3가지 방법이 있습니다.
- In-line: 테스트 마다 만드는 방법입니다.
- Delegate: 별도의 메서드에서 가져오는 것입니다. 정적팩토리 메서드로 가지고 오는 방법이 있습니다.
- Implicit: 테스트 실행시 자동으로 만드는 법입니다. Mockito에서는 beforeEach, afterEach를 사용해 자동화 할 수 있습니다.
B. Fixture를 사용하면서 생겼던 문제.
@Getter
@Setter(AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public final class CheckIn {
public static final String CHECK_IN_TITLE = "체크인 요청";
private final Long id;
private final HelpDetail helpDetail;
private final Progress progress;
@Builder(access = AccessLevel.PRIVATE)
private CheckIn(@NonNull Long id, @NonNull HelpDetail helpDetail, @NonNull Progress progress) {
this.id = id;
this.helpDetail = helpDetail;
this.progress = progress;
}
- 개인적으로 실제 값을 가진 Fixture를 사용하려 하며 문제가 되었던 것은, 제가 객체의 캡슐화를 Strict하게 지키고 싶은 생각이 강했기 때문입니다.
- Setter들은 캡슐화를 위해 private으로 두는 것까지는 문제가 되지 않았지만 @Builder나 생성자도 클래스 밖에서 의도하지 않은 용도로 사용될까봐 private으로만 두고 싶었습니다.
- 이러한 상황은 테스트코드를 만드는게 불편함으로 작용했습니다.
C. Fixture 사용기
1. Mock Fixture
addProductForm = mock(AddProductForm.class, withSettings().lenient());
addProductItemForm = mock(AddProductItemForm.class, withSettings().lenient());
given(addProductForm.getName()).willReturn("testProduct");
given(addProductForm.getDescription()).willReturn("testDescription");
given(addProductItemForm.getName()).willReturn("testProductItem");
given(addProductItemForm.getPrice()).willReturn(1000);
given(addProductItemForm.getCount()).willReturn(10);
- 목객체 세팅이 복잡해 질때도 있지만 처음에는 제가 겪었던 문제를 해결해 주었던 Mock은 만능처럼 보였습니다.
- 그러나 이전 포스트에서 처럼 거짓 양성(False Positive)을 겪은 후에는 실제 값을 Fixture로 사용하는 것이 좋겠다고 생각했습니다.
2. 상속
//Fixture의 대상
@Builder(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
}
public class MemberFixture extends Member {
public static Member create() {
return Member.builder()
.email(Variables.TEST_EMAIL)
.password(Variables.TEST_PASSWORD)
.name(Variables.TEST_NAME)
.phone(Variables.TEST_PHONE)
.address(Variables.TEST_LOCATION)
.locationServiceEnabled(Variables.TEST_IS_LOCATION_SERVICE_ENABLED)
.point(Variables.TEST_POINT)
.build();
}
- 그 다음으로 적용을 한것이 상속을 이용해 실제 값을 사용하는 것입니다.
- Fixture를 만들다가 값을 제대로 넣지 않는 실수를 할까봐 걱정이 되었는데 이렇게 함으로서 값들을 빼놓지 않고 받을 수 있습니다.
- 다만, 이 경우 테스트 코드 때문에 프로덕션 코드를 변경하여야 합니다. 프로덕션 코드에서 부모의 생성자와 빌더를 private이 아닌 protected로 열어 주어야 합니다.
- 이 방법은 꽤나 마음에 들었지만 protected로 풀어 놓는 점이 협업시 어떤 의도인지 다른 개발자들이 오해하기 쉽다고 생각했습니다.
3. 프로덕션 코드에서 정적 팩토리 메서드로 사용.
//For Test
public static CheckIn createForTest() {
return CheckIn.builder()
.id(1L)
.helpDetail(HelpDetail.createForTest())
.progress(Progress.createForTest())
.build();
}
- 어차피 2번 방법이 프로덕션 코드에 영향을 준다면 코드를 변경하는 것보다는 테스트 코드만 추가로 있는 것이 좋겠다고 생각했습니다.
- 이 경우 테스트 코드가 프로덕션 코드로 들어가기는 하지만 프로덕션 코드를 테스트 코드때문에 변경을 할 필요는 없습니다.
4. 현재 방법 1: Builder를 public하게 사용.
@Builder //access level을 풀어 주었습니다.
private CheckIn(@NonNull Long id, @NonNull HelpDetail helpDetail, @NonNull Progress progress) {
this.id = id;
this.helpDetail = helpDetail;
this.progress = progress;
}
- 그러다가 조금은 염려를 내려놓고 빌더를 public으로 풀어주기로 했습니다.
- 이유는 제가 객체 필드의 널관리에 신경쓰면서 빌더를 생성자에 사용하여서 생성시 필드값을 잘 관리를 잘한다면 객체가 불완전한 상태로 생길 일도 없고 그래서 의도하지 않은 객체가 생성되는 경우가 적을 것이라고 생각했습니다.
- 또한 코드 리뷰도 하는 상황이라면, 의도에 맞지 않게 애매하게 쓰이는 경우가 더욱 적을 것이라고 생각했습니다.
5. 현재 방법 2: FixtureMonkey.
FixtureMonkey sut = FixtureMonkey.builder()
.objectIntrospector(new ConstructorPropertiesArbitraryIntrospector())
.defaultNotNull(true)
.build();
HelpDetail helpDetailRandom = sut.giveMeOne(HelpDetail.class);
HelpDetail helpDetail1 = sut.giveMeBuilder(HelpDetail.class)
.set("helpRegisterId", 1L)
.set("title", "title")
.set("start", LocalDateTime.now())
.set("end", LocalDateTime.now().plusMinutes(10))
.set("placeId", "placeId")
.set("reward", 100L).sample();
- Fixture를 만들어주는 라이브러리를 사용하는 것도 괜찮다고 생각했습니다.
- 사실 내부적으로는 리플랙션을 사용하는 방법이고 1번에서 2번으로 넘어갈때 리플랙션은 고려해본 방법이기는 했습니다.
- 구현이 까다로워서 사용이 어려웠지만 확실한 캡슐화를 지키며 편의성도 생각한다면 이 방법도 괜찮다고 생각했습니다.
- 그렇지만 리플랙션을 이용하려면 정확한 필드 이름을 항상 넣어주어야 하는 불편함이 조금 있습니다.
- 다행히 이름이 다르면 에러를 내어 줍니다.
- 그러나 이 방법도 객체의 생성시 널 관리를 안해주면 의도치 않은 객체나 불완전한 객체가 생성되기 쉽습니다.
- 프로덕션 코드에 영향을 안주며 캡슐화도 전혀 해치지 않기에 좋은 방법이라고 생각하지만, 현업에서 적용하려면 팀간의 합의가 있어야 할 듯 합니다.
D. 결론
Fixture를 만들 때 대부분의 예제에서 그냥 Builder를 통해서 구현한 경우가 많아서 염려가 되었습니다. 안전하면서도 편리하게 사용하려다 보니 이런 여정을 겪은 것 같습니다. 널 관리를 한다는 전재 조건이 있기는 하지만 결국 빌더를 public하게 사용하는 방법이 가장 편리한것 같기는 합니다. 다만, 확실한 캡슐화를 위해서는 개인 프로젝트에서는 FixtureMonkey를 사용하고 팀에서 선택할 수 있다면 Fixture Monkey를 추천해 볼 것 같습니다.
출처
Demystifying Fixtures and Test Doubles: Spies, Stubs, Mocks and London vs. Detroit by Jamie Ingram
Iango님의 테스트 픽스처(Test Fixture)를 어떻게 만드는 것이 좋은 걸까?
올리브영 테크블로그: 윤노트님의 TestFixture를 쉽게 생성해 주는 라이브러리가 있다?