๐Ÿ“ธ Kotlin์„ ์‚ฌ์šฉํ•œ ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ

17679 ๋‹จ์–ด kotlinshowdevtest
์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์ฒ˜์Œ ์‹คํ–‰๋  ๋•Œ ํ…Œ์ŠคํŠธ ์ค‘์ธ ํ•จ์ˆ˜์˜ ์ถœ๋ ฅ์ด ํŒŒ์ผ์— ์ €์žฅ๋˜๊ณ  ์Šค๋ƒ…์ƒท์ด ์ €์žฅ๋˜๋ฉฐ ์ดํ›„์˜ ํ…Œ์ŠคํŠธ ์‹คํ–‰์€ ํ•จ์ˆ˜๊ฐ€ ๋™์ผํ•œ ์ถœ๋ ฅ์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ†ต๊ณผํ•˜๋Š” ํ…Œ์ŠคํŠธ ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ 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
  • Use other serializers
  • Use parameterized test

  • Tests should be deterministic

  • ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ ํ…Œ์ŠคํŠธ



    ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„์ด ์žˆ๋‹ค๊ณ  ์ƒ์ƒํ•ด ๋ณด์‹ญ์‹œ์˜ค.

    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()}")
    }
    


    ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ฆ๊ฑฐ์šด ์ฝ”๋”ฉํ•˜์„ธ์š”! ๐Ÿ’™

    ์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ