인터페이스 테스트하기
이 문서에서 배울 내용
- 인터페이스에 대한 테스트 데이터를 생성하는 방법
- 어떤 구현체를 사용할지 지정하는 방법
- 인터페이스 구현체의 속성을 커스터마이징하는 방법
- 다양한 인터페이스 테스팅 접근법 중 선택하는 방법
인터페이스 테스팅의 중요성
이 섹션에서는 소프트웨어 개발에서 인터페이스가 왜 중요한지, 그리고 왜 인터페이스 테스팅이 가치있는지 배웁니다.
인터페이스는 현대 소프트웨어 개발에서 다음과 같은 여러 중요한 이유로 필수적인 역할을 합니다:
실제 애플리케이션에서 인터페이스가 중요한 이유
느슨한 결합(Loose Coupling): 인터페이스는 컴포넌트들이 서로의 내부 세부사항을 알 필요 없이 계약을 맺을 수 있게 합니다. 이는 애플리케이션의 각 부분 간의 의존성을 줄입니다.
의존성 주입(Dependency Injection): 인터페이스는 의존성 주입을 쉽게 만들어, 구현체를 테스트 시 대안이나 목(mock)으로 대체할 수 있게 합니다.
유연성과 확장성(Flexibility and Extensibility): 인터페이스를 사용하는 코드를 변경하지 않고도 새로운 구현체를 추가할 수 있습니다. 이는 개방-폐쇄 원칙(확장에는 열려있고, 수정에는 닫혀있음)을 따릅니다.
향상된 테스트 용이성(Better Testability): 인터페이스를 사용하면 실제 구현체를 테스트 대역(test doubles)으로 대체할 수 있어, 단위 테스트가 훨씬 관리하기 쉬워집니다.
실제 예제: ProductInfo 인터페이스
실제 애플리케이션에서 인터페이스가 어떻게 사용되는지 예시를 통해 살펴보겠습니다:
// 게터가 있는 상품 정보 인터페이스
public interface ProductInfo {
String getName(); // 상품 이름 가져오기
BigDecimal getPrice(); // 상품 가격 가져오기
String getCategory(); // 상품 카테고리 가져오기
boolean isAvailable(); // 상품 재고 여부 확인
int getStockQuantity(); // 재고 수량 가져오기
}
// 인터페이스를 메서드 매개변수로 사용하는 서비스
public class ProductService {
// 특정 수량을 구매할 수 있는지 확인
public boolean canPurchase(ProductInfo productInfo, int quantity) {
return productInfo.isAvailable() &&
productInfo.getStockQuantity() >= quantity;
}
// 수량에 대한 총 가격 계산
public BigDecimal calculateTotal(ProductInfo productInfo, int quantity) {
return productInfo.getPrice().multiply(new BigDecimal(quantity));
}
// 상품 요약 정보 생성
public String getProductSummary(ProductInfo productInfo) {
return String.format("%s (%s) - $%s, 재고: %d",
productInfo.getName(),
productInfo.getCategory(),
productInfo.getPrice(),
productInfo.getStockQuantity());
}
}
이 예제에서 ProductService
는 ProductInfo
의 어떤 구현체라도 함께 작동할 수 있습니다 - 상품 데이터가 어디서 오는지 알 필요가 없습니다.
ProductInfo가 인터페이스여야 하는 이유
실제 애플리케이션에서 상품 정보는 다양한 출처에서 올 수 있습니다:
- 온라인 스토어 상품 (OnlineProductInfo)
- 오프라인 매장 상품 (StoreProductInfo)
- 재고 관리 시스템 상품 (InventoryProductInfo)
- 프로모션 상품 (PromotionProductInfo)
각 출처는 동일한 기본 정보(이름, 가격, 카테고리)를 제공하지만, 추가 정보를 가지거나 동작이 다를 수 있습니다. 예를 들면:
- 온라인 상품은 배송 날짜 정보가 필요합니다
- 매장 상품은 매장 위치 정보가 필요합니다
- 재고 상품은 창고 위치 정보가 필요합니다
ProductService는 상품 정보가 어디서 오든 기본적인 상품 정보만 알면 작동해야 합니다. 인터페이스를 사용하면 다음과 같은 이점이 있습니다:
- 특정 구현체에 의존하지 않아 서비스를 더 유연하게 만듭니다
- 서비스 코드를 변경하지 않고도 새로운 유형의 상품 정보(예: 해외 상품)를 추가할 수 있습니다
- 실제 상품 데이터베이스 없이도 테스트할 수 있어 테스트가 더 쉬워집니다
인터페이스 테스팅 시 도전 과제
이점에도 불구하고, 인터페이스는 고유한 테스팅 도전 과제를 제시합니다:
- 테스트하려면 구체적인 구현체가 필요하지만, 이를 만드는 것은 시간이 많이 걸릴 수 있습니다
- 각 구현체는 서로 다른 설정과 구성이 필요할 수 있습니다
- 테스트 데이터는 인터페이스 계약의 예상 동작과 일치해야 합니다
- 구현체별 속성은 커스터마이징이 필요할 수 있습니다
Fixture Monkey가 어떻게 도움이 되는가
Fixture Monkey는 다음과 같은 방식으로 이러한 과제를 해결합니다:
- 테스트를 위한 구현체를 자동으로 생성합니다
- 인터페이스 동작을 커스터마이징하는 유연한 방법을 제공합니다
- 다양한 테스트 시나리오에 맞는 여러 접근법을 지원합니다
- 테스트 구현에 필요한 상용구 코드를 줄입니다
기본 인터페이스 테스팅 개념
이 섹션에서는 간단한 예제부터 시작하여 Fixture Monkey와 함께하는 인터페이스 테스팅의 기본 개념을 소개합니다.
인터페이스 속성 커스터마이징하기
간단한 예제로 시작해보겠습니다. Fixture Monkey를 사용할 때, 일반 클래스와 마찬가지로 인터페이스 속성을 커스터마이징할 수 있습니다:
// 하나의 메서드가 있는 간단한 인터페이스
public interface StringSupplier {
String getValue(); // 이 메서드는 문자열을 반환합니다
}
// InterfacePlugin을 추가한 Fixture Monkey 인스턴스 생성
// 참고: 모든 인터페이스 작업에는 InterfacePlugin이 필요합니다
FixtureMonkey fixture = FixtureMonkey.builder()
.plugin(new InterfacePlugin())
.build();
// StringSupplier 생성 및 커스터마이징
String result = fixture.giveMeBuilder(StringSupplier.class)
.set("value", "Hello World") // value 속성 설정
.sample() // 인스턴스 생성
.getValue(); // 메서드 호출
// result는 "Hello World"가 됩니다
이 예제에서 Fixture Monkey는 자동으로 StringSupplier
인터페이스의 구현체를 생성하고 value
속성을 설정합니다.
인터페이스 구현체의 속성 설정하기
Fixture Monkey에서 인터페이스로 작업할 때, 구현된 메서드가 반환할 속성을 설정할 수 있습니다:
// 여러 메서드가 있는 예제 인터페이스
public interface StringProvider {
String getValue(); // 문자열 값을 가져오는 메서드
int getNumber(); // 정수 값을 가져오는 메서드
}
// InterfacePlugin을 추가한 Fixture Monkey 인스턴스 생성
FixtureMonkey fixture = FixtureMonkey.builder()
.plugin(new InterfacePlugin())
.build();
// 지정된 속성 값으로 인터페이스 구현체 생성
StringProvider provider = fixture.giveMeBuilder(StringProvider.class)
.set("value", "Hello World") // getValue()가 반환할 값 설정
.set("number", 42) // getNumber()가 반환할 값 설정
.sample();
// 구현체 사용
String value = provider.getValue(); // "Hello World" 반환
int number = provider.getNumber(); // 42 반환
인터페이스 구현 접근법
이 섹션에서는 가장 간단한 것부터 가장 고급 방법까지, 인터페이스 구현체를 생성하고 작업하는 세 가지 접근법을 배웁니다.
여러 구현체를 가진 인터페이스로 작업할 때, 가장 간단한 것부터 시작하여, 세 가지 주요 접근법이 있습니다:
접근법 1: 익명 구현체 사용하기 (가장 간단한 방법)
간단한 테스트 시나리오의 경우, Fixture Monkey는 인터페이스의 익명 구현체를 자동으로 생성할 수 있습니다. 이 접근법은 다음과 같은 경우에 유용합니다:
- 실제 클래스를 만들지 않고도 테스트를 위한 빠른 구현체가 필요할 때
- 구현 세부사항보다는 테스트 값에 집중하고 싶을 때
- 각 테스트마다 다른 값이 필요할 때
다음은 Fixture Monkey로 자동 익명 구현체를 사용하는 방법입니다:
// 상품 정보 인터페이스
public interface ProductInfo {
String getName(); // 상품 이름을 가져오는 메서드
BigDecimal getPrice(); // 상품 가격을 가져오는 메서드
String getCategory(); // 상품 카테고리를 가져오는 메서드
boolean isAvailable(); // 상품 재고 여부를 확인하는 메서드
int getStockQuantity(); // 재고 수량을 가져오는 메서드
}
// InterfacePlugin을 추가한 Fixture Monkey 인스턴스 생성
// 참고: 인터페이스로 작업할 때는 항상 InterfacePlugin이 필요합니다
FixtureMonkey fixture = FixtureMonkey.builder()
.plugin(new InterfacePlugin())
.build();
// Fixture Monkey가 익명 구현체를 생성하도록 함
ProductInfo productInfo = fixture.giveMeBuilder(ProductInfo.class)
.set("name", "Smartphone") // 상품 이름 설정
.set("price", new BigDecimal("999.99")) // 상품 가격 설정
.set("category", "Electronics") // 상품 카테고리 설정
.set("available", true) // 재고 있음으로 설정
.set("stockQuantity", 10) // 재고 수량을 10으로 설정
.sample();
// 테스트에서 사용
ProductService service = new ProductService();
boolean canPurchase = service.canPurchase(productInfo, 5); // true 반환
String summary = service.getProductSummary(productInfo);
// "Smartphone (Electronics) - $999.99, 재고: 10" 반환
이 접근법의 주요 장점은 Fixture Monkey가 모든 구현 세부사항을 처리한다는 것입니다. 여러분은 인터페이스 메서드가 반환해야 할 값만 정의하면, Fixture Monkey가 내부적으로 적절한 익명 구현체를 생성합니다.
접근법 2: Values.just 사용하기
이미 사용하고자 하는 구현체 인스턴스가 있다면, 간단히 Values.just
를 사용할 수 있습니다. 이는 바로 사용할 수 있는 특정 구현체가 있을 때 유용합니다.
// 온라인 스토어 구현체
public class OnlineProductInfo implements ProductInfo {
private String name; // 상품 이름
private BigDecimal price; // 상품 가격
private String category; // 상품 카테고리
private boolean available; // 상품 가용성
private int stockQuantity; // 재고 수량
// 생성자
public OnlineProductInfo(String name, BigDecimal price, String category, boolean available, int stockQuantity) {
this.name = name;
this.price = price;
this.category = category;
this.available = available;
this.stockQuantity = stockQuantity;
}
// 게터
@Override
public String getName() { return name; }
@Override
public BigDecimal getPrice() { return price; }
@Override
public String getCategory() { return category; }
@Override
public boolean isAvailable() { return available; }
@Override
public int getStockQuantity() { return stockQuantity; }
}
// InterfacePlugin을 추가한 Fixture Monkey 인스턴스 생성
FixtureMonkey fixture = FixtureMonkey.builder()
.plugin(new InterfacePlugin())
.build();
// 상품 인스턴스 생성
OnlineProductInfo originalProduct = new OnlineProductInfo(
"Laptop",
new BigDecimal("1999.99"),
"Electronics",
true,
5
);
// Values.just를 사용하여 테스트에서 이 인스턴스 사용
ProductInfo productInfo = fixture.giveMeBuilder(ProductInfo.class)
.set("$", Values.just(originalProduct)) // 기존 인스턴스 사용
.sample();
// 테스트에서 사용
ProductService service = new ProductService();
BigDecimal total = service.calculateTotal(productInfo, 2); // 3999.98 반환
이 접근법의 주요 이점은 단순함입니다 - 추가 구성이 필요하지 않습니다. 그러나 제한점도 있습니다 - 구현체 속성을 추가로 커스터마이징할 수 없습니다:
// 이것은 작동하지 않습니다 - Values.just 접근법에서는 속성을 수정할 수 없습니다
OnlineProductInfo product = (OnlineProductInfo)fixture.giveMeBuilder(ProductInfo.class)
.set("$", Values.just(originalProduct))
.set("price", new BigDecimal("1499.99")) // 이것은 효과가 없을 것입니다
.sample();
접근법 3: interfaceImplements 옵션 사용하기 (가장 유연한 방법)
속성 커스터마이징이 필요한 더 복잡한 시나리오의 경우, interfaceImplements
옵션을 사용할 수 있습니다. 이 접근법은 Fixture Monkey에게 인터페이스의 모든 가능한 구현체를 알려주어, 올바른 구현체를 선택하고 커스터마이징할 수 있게 합니다.
1단계: 구현체로 Fixture Monkey 구성하기
// 상품 정보 인터페이스
public interface ProductInfo {
String getName();
BigDecimal getPrice();
String getCategory();
boolean isAvailable();
int getStockQuantity();
}
// 온라인 스토어 구현체
public class OnlineProductInfo implements ProductInfo {
private String name;
private BigDecimal price;
private String category;
private boolean available;
private int stockQuantity;
// 생성자, 게터, 세터
}
// 추가 속성이 있는 물리적 스토어 구현체
public class StoreProductInfo implements ProductInfo {
private String name;
private BigDecimal price;
private String category;
private boolean available;
private int stockQuantity;
private String storeLocation; // 추가 속성
// 생성자, 게터, 세터
public String getStoreLocation() {
return storeLocation;
}
public void setStoreLocation(String storeLocation) {
this.storeLocation = storeLocation;
}
}
// 구현체로 Fixture Monkey 구성
FixtureMonkey fixture = FixtureMonkey.builder()
.plugin(
new InterfacePlugin()
.interfaceImplements(
ProductInfo.class, // 인터페이스
List.of(OnlineProductInfo.class, StoreProductInfo.class) // 구현체들
)
)
.build();
2단계: 특정 구현체 생성하기
// StoreProductInfo 인스턴스 생성
StoreProductInfo storeProduct = (StoreProductInfo)fixture.giveMeBuilder(ProductInfo.class)
.set("$", new StoreProductInfo()) // 어떤 구현체를 사용할지 지정
.sample();
// 이제 구현체별 속성에 접근할 수 있습니다
storeProduct.setStoreLocation("Downtown"); // 구현체별 속성 설정
String location = storeProduct.getStoreLocation(); // "Downtown"
3단계: 구현체 속성 커스터마이징하기
interfaceImplements 옵션을 사용하면 구현체의 속성도 수정할 수 있습니다:
// StoreProductInfo 생성 및 커스터마이징
StoreProductInfo product = (StoreProductInfo)fixture.giveMeBuilder(ProductInfo.class)
.set("$", new StoreProductInfo()) // StoreProductInfo 구현체 사용
.set("name", "Coffee Maker") // 상품 이름 설정
.set("price", new BigDecimal("89.99")) // 상품 가격 설정
.set("category", "Kitchen Appliances") // 상품 카테고리 설정
.set("available", true) // 재고 있음으로 설정
.set("stockQuantity", 15) // 재고 수량 설정
.set("storeLocation", "America Mall") // 구현체별 속성 설정
.sample();
// 테스트에서 사용
ProductService service = new ProductService();
String summary = service.getProductSummary(product);
// "Coffee Maker (Kitchen Appliances) - $89.99, 재고: 15" 반환
// 구현체별 속성이 설정됨
assertEquals("America Mall", product.getStoreLocation());
올바른 접근법 선택하기
이 가이드는 테스트 요구사항에 따라 어떤 접근법을 선택할지 결정하는 데 도움이 됩니다.
올바른 접근법을 선택하는 데 도움이 되는 간단한 가이드는 다음과 같습니다:
익명 구현체를 사용해야 할 때:
- 테스트를 위한 빠르고 일회성 구현체가 필요할 때
- 테스트만을 위해 전체 클래스를 만들고 싶지 않을 때
- 단일 테스트에 특화된 맞춤 동작이 필요할 때
- 인터페이스 테스팅을 막 시작했을 때
Values.just를 사용해야 할 때:
- 추가 구성 없이 빠른 솔루션이 필요할 때
- 구현체를 한두 번만 사용할 때
- 이미 구현체의 인스턴스가 있을 때
- 생성 후 속성을 수정할 필요가 없을 때
interfaceImplements를 사용해야 할 때:
- 구현체 속성을 커스터마이징해야 할 때
- 여러 테스트에 걸쳐 동일한 구현체 세트를 사용할 때
- Fixture Monkey가 구현체들 중에서 무작위로 선택하기를 원할 때
- 더 복잡한 테스트 시나리오를 구축할 때
실제 예제: 상품 서비스 테스팅
이 섹션은 인터페이스를 사용하는 서비스 테스트의 완전한 예제를 제공합니다.
실제 테스트 시나리오에서 인터페이스를 사용하는 방법은 다음과 같습니다:
@Test
void testProductService() {
// Fixture Monkey 구성
FixtureMonkey fixture = FixtureMonkey.builder()
.plugin(
new InterfacePlugin()
.interfaceImplements(
ProductInfo.class,
List.of(OnlineProductInfo.class, StoreProductInfo.class)
)
)
.build();
// 특정 속성을 가진 상품 생성
ProductInfo product = fixture.giveMeBuilder(ProductInfo.class)
.set("name", "Bluetooth Speaker") // 상품 이름 설정
.set("price", new BigDecimal("79.99")) // 상품 가격 설정
.set("category", "Audio") // 상품 카테고리 설정
.set("available", true) // 재고 있음으로 설정
.set("stockQuantity", 8) // 재고 수량 설정
.sample();
// 상품 서비스 테스트
ProductService service = new ProductService();
// 다양한 메서드 테스트
boolean canPurchase = service.canPurchase(product, 3);
BigDecimal total = service.calculateTotal(product, 3);
String summary = service.getProductSummary(product);
// 결과 검증
assertTrue(canPurchase);
assertEquals(new BigDecimal("239.97"), total);
assertEquals("Bluetooth Speaker (Audio) - $79.99, 재고: 8", summary);
}
일반적인 문제와 해결책
이 섹션은 인터페이스 테스팅 시 자주 발생하는 문제를 다룹니다.
문제: 구현체 속성 접근 시 ClassCastException 발생
// 잘못된 타입이 사용되면 ClassCastException이 발생합니다
StoreProductInfo product = (StoreProductInfo)fixture.giveMeBuilder(ProductInfo.class)
.sample(); // 기본 구현체가 StoreProductInfo가 아닐 수 있습니다
해결책: 캐스팅이 필요할 때는 항상 구현체를 지정하세요:
// 안전한 접근법 - 명시적으로 구현체 타입 지정
StoreProductInfo product = (StoreProductInfo)fixture.giveMeBuilder(ProductInfo.class)
.set("$", new StoreProductInfo()) // 명시적으로 구현체 타입 설정
.sample();
문제: 속성이 설정되지 않음
해결책: 속성을 커스터마이징해야 한다면 interfaceImplements 옵션을 사용하고 있는지 확인하세요:
// 먼저 interfaceImplements로 구성
FixtureMonkey fixture = FixtureMonkey.builder()
.plugin(
new InterfacePlugin()
.interfaceImplements(
ProductInfo.class,
List.of(OnlineProductInfo.class, StoreProductInfo.class)
)
)
.build();
// 그런 다음 속성을 커스터마이징할 수 있습니다
StoreProductInfo product = (StoreProductInfo)fixture.giveMeBuilder(ProductInfo.class)
.set("$", new StoreProductInfo())
.set("name", "Wireless Earbuds") // 이제 작동합니다
.set("storeLocation", "City Center") // 이제 작동합니다
.sample();
요약
- 인터페이스 테스팅은 의존성 주입과 인터페이스를 사용하는 실제 애플리케이션에서 흔합니다
- 빠른 테스트를 위해 간단한 익명 구현체부터 시작하세요
- 특정 구현체를 이미 가지고 있다면 Values.just를 사용하세요
- 구현체 속성을 커스터마이징해야 한다면 interfaceImplements를 사용하세요
- 구현체별 속성에 접근해야 한다면 항상 구현체를 지정하세요