๐ธ Kotlin์ ์ฌ์ฉํ ์ค๋ ์ท ํ ์คํธ
์ด๊ฒ์ the fronted community์์ ๋งค์ฐ ์ธ๊ธฐ ์๋ ๊ฒ ๊ฐ์ง๋ง ์ฐ๋ฆฌ ๋ฐฑ์๋์์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค!
์ด PoC์์๋ origin-energy/java-snapshot-testing์ ์ฌ์ฉํ๊ณ "the testing framework loved by lฬถaฬถzฬถyฬถ productive devs"์ ๋ช ์๋ ๋๋ก ์๋์ผ๋ก ํ ์คํธ ๊ธฐ๋์น๋ฅผ ํ ์คํธ ํ์ผ๋ก ์ ์ฅํ ๋๋ง๋ค ์ฌ์ฉํฉ๋๋ค ๐
๋ก์ ๋น๋์ค / ์ค๋ ์ท ํ ์คํธ
๐ธ Origin-energy/java-snapshot-testing ๋ฐ Kotlin์ ์ฌ์ฉํ ์ค๋ ์ท ํ ์คํธ
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ตฌ์ฑํ๋ ค๋ฉด Junit5 + Gradle quickstart์ ๋ฐ๋ฅด์ญ์์ค.
src/test/resources/snapshot.properties
ํ์ผ์ ์ถ๊ฐํฉ๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก output-dir=src/test/java
๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ์์ค ์ฝ๋ ๋ด์์ ์ค๋
์ท์ด ์์ฑ๋์ง๋ง(git์ ์ปค๋ฐํ๋ ๊ฒ์ ์์ง ์๋๋ก ๊ฐ์ ํฉ๋๋ค) ๊ฐ์ธ์ ์ผ๋ก output-dir=src/test/snapshots
๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ์ค๋
์ท์ด ์์ฒด ๋๋ ํ ๋ฆฌ์ ์์ฑ๋ฉ๋๋ค. ์์ํ์!
Test a simple implementation
๊ฐ๋จํ ๊ตฌํ ํ ์คํธ
๋ค์๊ณผ ๊ฐ์ ๊ฐ๋จํ ๊ตฌํ์ด ์๋ค๊ณ ์์ํด ๋ณด์ญ์์ค.
class MyImpl {
fun doSomething(input: Int) = MyResult(
oneInteger = input,
oneDouble = 3.7 * input,
oneString = "a".repeat(input),
oneDateTime = LocalDateTime.of(
LocalDate.of(2022, 5, 3),
LocalTime.of(13, 46, 18)
)
)
}
๋ค์๊ณผ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ์ค๋ ์ท ํ ์คํธ๋ฅผ ํ ์ ์์ต๋๋ค.
@ExtendWith(SnapshotExtension::class)
internal class SnapshotTesting {
private val myImpl = MyImpl()
@Test
fun `should do something`(expect: Expect) {
val myResult = myImpl.doSomething(7)
expect.toMatchSnapshot(myResult)
}
}
๋ค์ ๋ด์ฉ์ผ๋ก ์ค๋ ์ท ํ์ผ
MyImplTest.snap
์ ์์ฑํฉ๋๋ค.org.rogervinas.MyImplTest.should do something=[
MyResult(oneInteger=7, oneDouble=25.900000000000002, oneString=aaaaaaa, oneDateTime=2022-05-03T13:46:18)
]
ํ ์คํธ๋ฅผ ๋ค์ ์คํํ๋ฉด ์ค๋ ์ท๊ณผ ์ผ์นํฉ๋๋ค.
๋ค๋ฅธ ์ง๋ ฌ ๋ณํ๊ธฐ ์ฌ์ฉ
์ด์ ์์ ์์ ๋ณผ ์ ์๋ฏ์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ "ToString"์ง๋ ฌ ๋ณํ๊ธฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ค๋ ์ท์ ์์ฑํฉ๋๋ค. ๋์ JSON ์ง๋ ฌ ๋ณํ๊ธฐ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
@Test
fun `should do something`(expect: Expect) {
val myResult = myImpl.doSomething(7)
expect.serializer("json").toMatchSnapshot(myResult)
}
ํ์
com.fasterxml.jackson.core
์ข
์์ฑ์ ์ถ๊ฐํ๊ณ ์ด์ ์ค๋
์ท์ ์ญ์ ํ๋ ๊ฒ์ ์์ง ๋ง์ญ์์ค.๊ทธ๋ฌ๋ฉด ์ ์ค๋ ์ท ํ์ผ์ด ๋ค์๊ณผ ๊ฐ์ด ํ์๋ฉ๋๋ค.
org.rogervinas.MyImplTest.should do something=[
{
"oneInteger": 7,
"oneDouble": 25.900000000000002,
"oneString": "aaaaaaa",
"oneDateTime": "2022-05-03T13:46:18"
}
]
๋ํ
serializer
๋ฉ์๋์์ ์ง๋ ฌ ๋ณํ๊ธฐ ํด๋์ค, ์ง๋ ฌ ๋ณํ๊ธฐ ์ธ์คํด์ค ๋๋ snapshot.properties
์ ๊ตฌ์ฑ๋ ์ง๋ ฌ ๋ณํ๊ธฐ ์ด๋ฆ ์ค ํ๋๋ฅผ ์ ๊ณตํ๋ ์์ฒด ์ฌ์ฉ์ ์ ์ ์ง๋ ฌ ๋ณํ๊ธฐ๋ฅผ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.๋งค๊ฐ๋ณ์ํ๋ ํ ์คํธ ์ฌ์ฉ
์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋งค๊ฐ๋ณ์ํ๋ ํ ์คํธ์ ํจ๊ป ์๋ํ๋๋ก ํ๋ ค๋ฉด
scenario
๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.@ParameterizedTest
@ValueSource(ints = [1, 2, 3, 4, 5, 6, 7, 8, 9])
fun `should do something`(input: Int, expect: Expect) {
val myResult = myImpl.doSomething(input)
expect.serializer("json")
.scenario("$input")
.toMatchSnapshot(myResult)
}
์ด๋ ๊ฒ ํ๋ฉด ๊ฐ ์คํ๋ง๋ค ๊ณ ์ ํ ์ค๋ ์ท ๊ธฐ๋์น๊ฐ ์์ต๋๋ค.
org.rogervinas.MyImplTest.should do something[1]=[
{
"oneInteger": 1,
"oneDouble": 3.7,
"oneString": "a",
"oneDateTime": "2022-05-03T13:46:18"
}
]
...
org.rogervinas.MyImplTest.should do something[9]=[
{
"oneInteger": 9,
"oneDouble": 33.300000000000004,
"oneString": "aaaaaaaaa",
"oneDateTime": "2022-05-03T13:46:18"
}
]
ํ ์คํธ๋ ๊ฒฐ์ ๋ก ์ ์ด์ด์ผ ํฉ๋๋ค.
์ฐ๋ฆฌ๊ฐ ํ ์คํธํด์ผ ํ๋ ๊ตฌํ์ด ์ด๊ฒ์ด๋ฉด ์ด๋ป๊ฒ ๋ ๊น์?
class MyImpl {
private val random = Random.Default
fun doSomethingMore() = MyResult(
oneInteger = random.nextInt(),
oneDouble = random.nextDouble(),
oneString = "a".repeat(random.nextInt(10)),
oneDateTime = LocalDateTime.now()
)
}
์ด ์ค๋ ์ท ํ ์คํธ๊ฐ ์๋ ๊ฒฝ์ฐ:
@Test
fun `should do something more`(expect: Expect) {
val myResult = myImpl.doSomethingMore()
expect.serializer("json").toMatchSnapshot(myResult)
}
์ฒ์์๋ ์ค๋ ์ท ์์ฑ์ ํต๊ณผํ์ง๋ง ํ ์คํธ๊ฐ ๊ฒฐ์ ์ ์ด์ง ์๊ธฐ ๋๋ฌธ์ ๋ค์ ์คํ์ ์คํจํฉ๋๋ค ๐ฑ
์ด ๊ฒฝ์ฐ
MyImpl
๋ฐ Random
๊ตฌํ์ ์ฌ์ฉํ์ฌ Clock
์ ์ ๋ฌํ๋ ์ด์ ๋ฅผ ์ด ํ
์คํธ ๊ฒฐ์ ๋ก ์ ์ผ๋ก ์ฝ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.class MyImpl(
private val random: Random,
private val clock: Clock
) {
fun doSomethingMore() = MyResult(
oneInteger = random.nextInt(),
oneDouble = random.nextDouble(),
oneString = "a".repeat(random.nextInt(10)),
oneDateTime = LocalDateTime.now(clock)
)
}
๊ทธ๋ฐ ๋ค์ ๊ฒฐ์ ๋ก ์ ์ผ๋ก ํ ์คํธํ ์ ์์ต๋๋ค.
@ExtendWith(SnapshotExtension::class)
internal class MyImplTest {
private val myImpl = MyImpl(
Random(seed=1234),
Clock.fixed(
Instant.parse("2022-10-01T10:30:00.000Z"),
ZoneId.of("UTC")
)
)
@Test
fun `should do something more`(expect: Expect) {
val myResult = myImpl.doSomethingMore()
expect.serializer("json").toMatchSnapshot(myResult)
}
}
๋ฐ๋ผ์ ์ค๋ ์ท์ ํญ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
org.rogervinas.MyImplTest.should do something more=[
{
"oneInteger": 345130239,
"oneDouble": 0.6887620080485805,
"oneString": "aaaaaaaaa",
"oneDateTime": "2022-10-01T10:30:00"
}
]
ํ๋ก๋์ ํ๊ฒฝ์์ ๋ค์๊ณผ ๊ฐ์ด
MyImpl
์ธ์คํด์ค๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.fun main() {
val myImpl = MyImpl(Random.Default, Clock.systemDefaultZone())
println("myImpl.doSomething(3) = ${myImpl.doSomething(3)}")
println("myImpl.doSomethingMore = ${myImpl.doSomethingMore()}")
}
๊ฐ์ฌํฉ๋๋ค. ์ฆ๊ฑฐ์ด ์ฝ๋ฉํ์ธ์! ๐
Reference
์ด ๋ฌธ์ ์ ๊ดํ์ฌ(๐ธ Kotlin์ ์ฌ์ฉํ ์ค๋ ์ท ํ ์คํธ), ์ฐ๋ฆฌ๋ ์ด๊ณณ์์ ๋ ๋ง์ ์๋ฃ๋ฅผ ๋ฐ๊ฒฌํ๊ณ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ ๋ณด์๋ค https://dev.to/rogervinas/snapshot-testing-with-kotlin-3nk6ํ ์คํธ๋ฅผ ์์ ๋กญ๊ฒ ๊ณต์ ํ๊ฑฐ๋ ๋ณต์ฌํ ์ ์์ต๋๋ค.ํ์ง๋ง ์ด ๋ฌธ์์ URL์ ์ฐธ์กฐ URL๋ก ๋จ๊ฒจ ๋์ญ์์ค.
์ฐ์ํ ๊ฐ๋ฐ์ ์ฝํ ์ธ ๋ฐ๊ฒฌ์ ์ ๋ (Collection and Share based on the CC Protocol.)
์ข์ ์นํ์ด์ง ์ฆ๊ฒจ์ฐพ๊ธฐ
๊ฐ๋ฐ์ ์ฐ์ ์ฌ์ดํธ ์์ง
๊ฐ๋ฐ์๊ฐ ์์์ผ ํ ํ์ ์ฌ์ดํธ 100์ ์ถ์ฒ ์ฐ๋ฆฌ๋ ๋น์ ์ ์ํด 100๊ฐ์ ์์ฃผ ์ฌ์ฉํ๋ ๊ฐ๋ฐ์ ํ์ต ์ฌ์ดํธ๋ฅผ ์ ๋ฆฌํ์ต๋๋ค