fixture monkey 는 테스트 객체 라이브러리다. fixture 를 자동으로 만들어준다. 사용 예제를 하나 보자. 아래 Order 객체가 있다.
import lombok.Data;
@Data // lombok getter, setter
public class Order {
@NotNull
private Long id;
@NotBlank
private String orderNo;
@Size(min = 2, max = 10)
private String productName;
@Min(1)
@Max(100)
private int quantity;
@Min(0)
private long price;
@Size(max = 3)
private List<@NotBlank @Size(max = 10) String> items = new ArrayList<>();
@PastOrPresent
private Instant orderedAt;
@Email
private String sellerEmail;
}
fixutre monkey 로 Order 객체를 만들면 아래와 같이 랜덤한 값들로 채워진다.
@Test
fun sampleTest() {
for (i:Int in 1..5) {
println(fixture.giveMeOne(Order::class.java))
}
}
Order(id=1791, orderNo=Z:::gx2P:s7<"dDXeB, productName=, quantity=74, price=48, items=[?E, V, G`], orderedAt=2020-12-18T14:01:18.092Z, sellerEmail=SZ5{`9aZciQZtLaxJ|3~=9P@k.pJAj5HwNCMlOU2aR0Gelq.Nr1Kr8DtlzT71FEoFJ.fgLIobSoi5HQmRz2rsB4MmphmJNsIX1Y.HootI0TbEhbGFU6tK.ksKE3pmrnOzkAXcwTluhzA-bYYf2a8zsEKUW6QUDUNctEZA2q-ZTKxA.07us2lS.VVt4MEd.r4LbSFKf9T60-V.CQP1nkXBC3JpM0ZCzlrqLqIPJhmMdAfjrctqweDLKSsdg.a.OTlITLSa47l2.vMxRBEua8g)
Order(id=34, orderNo=f, productName=s(ZPS1], quantity=74, price=6796303258064044416, items=[}, 4], orderedAt=2020-12-19T14:21:03.093Z, sellerEmail=V-3C@MEdrEzwr4.ua8g1IgsZyzXeWJr.Gz6jTFh-luzdJ.PHqHGfAorEUBfc2VSkgz0R.kSzDrNPjGR0f0JkhAeK1SLg.XD5fdB0jlbvOT6rfRJvfLNsSS2a2Aq5sHl-aXK5XvTMso.4HCHQ3KnKkOxIWKWZ1ylDScjfHt.FvBr7jbzvl5LrEbNKnOEjuOeFoc.O.cdf3nx3mgLcjG.lzi5tdeocuK.cPLbM2GnSnHOyb.AbCsQ4ultDfn.1T)
Order(id=7, orderNo=BFd,oe)QNBv, productName=), quantity=9, price=4233741, items=[!], orderedAt=2020-12-18T13:57:42.426Z, sellerEmail="af(V*"@z.bSFKf9T6.Q.P1nkXBC.0ZC.LqIPJhmMdAfjrctqweDLKSsdg9.cKuO)
Order(id=17214906761180, orderNo=D`k:~#US, productName=(, quantity=27, price=3820489488235044924, items=[5>Ej, 8by0, (9xl,], orderedAt=2020-12-19T12:31:28.677Z, sellerEmail=0@[95.247.26.1])
Order(id=-12413378131691, orderNo=5J0DK0B"n0I>%\3=, productName=nZJw[vC, quantity=96, price=235133537, items=[KB], orderedAt=2021-06-10T19:36:08.496Z, sellerEmail=}A=tLf/}OovfZ%D*Mx$Ba9.Z3ZDI&~&Rcz%l0aUZ=bAa4ZxFm^^ED*zv@lOhRf9UWrkcKOrb.jjFbtRyUX18U-OjDx.Et5B07BzbU39zr8n6B1WoyvBPrf.wlC2aSc.m1b.jPRA4EcbnIybiWyELV.z3KByINgip8fKpuWVMsQ0Uc4SZgEhP.PSutPECwO5BgIR.lVmhpQScJwqbg2ouPQEZKJMO.wjr4BwE3e.z1STigPwZYQKvNWdrLn5PdBglmj0Xtu.aIBlBUYdyPPlKuydX7B.Mv8JjFs4.91XXW)
테스트하고 싶은 코드는 아래와 같다. check 메서드에서, 파라미터로 전달받은 type 이 서로 일치 하지 않으면 예외를 발생시키도록 작업했다. 그리고 의도한대로 예외가 발생하는지 테스트하고 싶다.
cf) 프로덕션 코드는 자바, 테스트 코드는 코틀린으로 만들었다.
@Service
@RequiredArgsConstructor
public class TypeCheckService {
public void check(TypeCheckSource source, Type type) {
if (source.getType() != type) {
throw new RuntimeException("ybs");
}
}
}
@Value
public class TypeCheckSource {
long id;
Type type;
}
public enum Type {
TYPE_1,
TYPE_2,
TYPE_3,
TYPE_4;
}
다음으로 테스트 코드를 보자. ParameterizedTest 는 하나의 테스트 메서드로 여러개의 파라미터에 대해서 테스트 할 수 있다. 그래서 Type enum 4개의 값을 다 테스트 돌릴 수 있고, type 파라미터로 전달된다.
이제 typeCheckService.check(source, type) 결과를 확인하기 위해선 핵심이 되는 source 객체를 셋팅해야 한다. 자세한 내용은 뒤에서 설명하겠다.
class TypeCheckTest : SpringContextTest() {
@ParameterizedTest
@EnumSource(value = Type::class, names = ["TYPE_1", "TYPE_2", "TYPE_3", "TYPE_4"])
fun typeNotMatchesThrows(
type: Type,
@Autowired typeCheckService: TypeCheckService
) {
val source = fixture.typeCheckDomainFixture(
type = type
)
.apply {
typeCheckSourceBuilder.set(
"type",
Arbitraries.of(Type::class.java)
.filter { it != type }.sample()
)
}
.typeCheckSource
thenThrownBy { typeCheckService.check(source, type) }
.isExactlyInstanceOf(RuntimeException::class.java)
}
}
fixture 객체는 SpringContextTest 추상 클래스에서 생성했다.
@SpringBootTest(
classes = [FixtureToyApplicationTests::class]
)
@ActiveProfiles("test")
abstract class SpringContextTest {
@Autowired
lateinit var objectMapper: ObjectMapper
lateinit var fixtureMonkeyBuilder: FixtureMonkeyBuilder
protected val fixture: FixtureMonkey by lazy {
this.fixtureMonkeyBuilder.build()
}
@BeforeEach
fun setUp() {
this.fixtureMonkeyBuilder = fixtureMonkeyBuilder(this.objectMapper)
}
}
fixtureMonkeyBuilder 는 아래와 같이 만들었는데 JsonMappers.OBJECT_MAPPER 는 일반적인 ObjectMapper 객체다. 그리고 registerGroup(BuilderGroup) 부분은 이번 테스트하고는 상관없어서 다음번에 설명하겠다. 일단은 BuilderGroup 이름의 빈껍데기 클래스를 만들어놓는다.
fun fixtureMonkeyBuilder(
objectMapper: ObjectMapper = JsonMappers.OBJECT_MAPPER
): FixtureMonkeyBuilder = KFixtureMonkeyBuilder()
.defaultGenerator(JacksonArbitraryGenerator(objectMapper))
.registerGroup(BuilderGroup::class.java)
이제 다시 테스트코드로 돌아와서 typeCheckDomainFixture 를 살펴보자.
val source = fixture.typeCheckDomainFixture(
type = type
)
.apply {
typeCheckSourceBuilder.set(
"type",
Arbitraries.of(Type::class.java)
.filter { it != type }.sample()
)
}
.typeCheckSource
typeCheckDomainFixture 는 TypeCheckDomainFixture data class 객체 를 리턴하는데 typeCheckSourceBuilder 를 by lazy 로 만든다. 그리고 테스트코드에서 Arbitrary filter를 이용해, 전달받은 type enum 과 다른 enum 값으로 채워서 항상 예외가 발생하게 만든다.
fun FixtureMonkey.typeCheckDomainFixture(
type: Type
) = TypeCheckDomainFixture(
type = type
)
data class TypeCheckDomainFixture(
val type: Type
) {
val typeCheckSourceBuilder: ArbitraryBuilder<TypeCheckSource> by lazy {
fixtureMonkeyBuilder()
.build()
.giveMeBuilder(TypeCheckSource::class.java)
.set("id", 1)
.set("type", TYPE.TYPE_1)
}
val typeCheckSource: TypeCheckSource
get() = typeCheckSourceBuilder.sample()
}
by lazy 에 대해서 조금만 더 얘기해보자. typeCheckSource 가 호출되어 typeCheckSourceBuilder.sample() 이 실행되야 fixture monkey 라이브러리가 TypeCheckSource 객체에 값을 채워준다. 지금 테스트 코드가 하나만 있어서 필요성이 체감 안되지만 여러 테스트에서 typeCheckSource 를 같이 쓴다고 생각했을 때 공통적인 부분을 by lazy 로 묶어서 일종의 싱글톤처럼 관리하고자 했다.
마지막으로 테스트 하는것에 비해서 만들어진게 많다고 느낄 수 있는데, 다른 테스트에서도 공통적으로 사용하기 위해서 구조를 먼저 만들다보니 그렇게 됐다. 다른 테스트들을 또 만들면서 DomainFixture 를 채워나갈 예정이다.
lombok @Value 애노테이션을 delombok 해보면 아래와 같다.
@Value
public class TypeCheckSource {
long id;
Type type;
}
final 클래스이고 멤버변수들도 private final 이다. 생성자로 주입받고 getter 만 존재한다. 즉 Immutable 이 보장된다.
public final class TypeCheckSource {
private final long id;
private final Type type;
public TypeCheckSource(long id, Type type) {
this.id = id;
this.type = type;
}
public long getId() {
return this.id;
}
public Type getType() {
return this.type;
}
public boolean equals(final Object o) { 생략 }
public int hashCode() { 생략 }
public String toString() { 생략 }
그래서 그냥 테스트를 돌리면 아래와 같은 에러를 만나게 된다.
Cannot construct instance of `com.fixturetoy.TypeCheckSource`
(no Creators, like default constructor, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)
json data 를 TypeCheckSource 객체로 deserialize 해야하는데 적절한 생성자를 찾지 못했다는 에러다. 3가지 해결책이 있고 그중에서 @ConstructorProperties 방식을 사용했다.
@Value
public class TypeCheckSource {
long id;
Type type;
@ConstructorProperties({"id", "type"})
public TypeCheckSource(long id, Type type) {
this.id = id;
this.type = type;
}
}
하지만 위와 같이 직접 코딩하는 방식 대신 lombok 을 이용했다. 프로젝트 루트 lombok.config 파일에 lombok.anyConstructor.addConstructorProperties=true 설정을 추가했다. 이 설정을 true로 하면 생성자에 @ConstructorProperties 을 자동으로 붙여준다.
그런데 한가지 주의할점이 있다. @Value 를 통해 만들어진 생성자를 안쓰고 직접 만든 생성자를 쓸 경우 롬복은 @ConstructorProperties 을 붙여주지 않는다. 그래서 직접 만든 생성자를 사용해야 하는 경우에는 lombok.anyConstructor.addConstructorProperties=true 설정이 있더라도 @ConstructorProperties 를 직접 추가해줘야 한다.
@Value
public class TypeCheckSource {
long id;
Type type;
// 이렇게 직접 생성자 추가하면 롬복이 @ConstructorProperties 추가 안해줌
public TypeCheckSource(long id, Type type) {
this.id = id;
this.type = type;
}
}
'Kotlin' 카테고리의 다른 글
DID 스터디 3회차(kotlin let/run/also/apply/with) (0) | 2023.03.18 |
---|---|
fixture monkey BuilderGroups (0) | 2021.12.27 |
코틀린 재귀호출 최적화(Tail-Call) (0) | 2021.12.16 |
form data 를 string으로 변환하기 (0) | 2021.11.15 |