4 minute read

’테스트’라고 하면 ’유닛 테스트(Unit Testing)’를 떠올린다. 좀 더 해상도를 높여서 테스트 코드를 상상하면 ’예제 기반 테스트(Example-based Testing)’을 떠올린다.

# Money in the cash register
register = [{20.0, 1}, {10.0, 2}, {5.0, 4}, {1.0, 10}, {0.25, 10}, {0.01, 100}]

# cash 함수는 금전등록기에 있는 금액, 가격, 지불한 금액을 인자로 받아서 잔돈을 리턴한다
assert [{10.0, 1}] = cash(register, 10.0, 20.0)
assert [{10.0, 1}, {0.25, 1}] = cash(register, 9.75, 20.0)
assert [{0.01, 18}] = cash(register, 0.82, 1.0)
assert [{10.0, 1}, {5.0, 1}, {1.0, 3}, {0.25, 2}, {0.01, 13}] = cash(register, 1.37, 20.0)

이렇게 테스트할 입력을 프로그래머가 직접 만들어서 테스트하는 방식이다. 입력이 edge case를 포함하는지 신경 써야 한다. 리뷰할 때도 edge case 포함 여부를 보게 된다.

반면 ’속성 기반 테스트(Property-Based Testing)’는 예제를 직접 만드는 대신 입력을 만드는 제네레이터(generator)를 정의한다.

forall {register_money, price_to_pay, money_paid} <- generate() do
  change = cash(register_money, price_to_pay, money_paid)
  sum(change) == money_paid - price_to_pay && fewest_pieces_possible(register_money, change)
end

메타 테스트를 작성하는 것 같다. 어떤 입력이 들어오든 간에 항상 동일해야 하는 규칙을 찾는다. 이걸 속성(property)이라고 부른다. 이렇게 코드를 테스트하는 방법을 ’속성 기반 테스트’라고 부른다.

상태가 있는 속성 기반 테스트(Stateful Property-based Testing)이 정말 재미있었다. 상태가 없는 속성 기반 테스트는 쉽게 상상할 수 있다. 하지만 이게 그렇게 유용할까? 이건 또 의문이다. 익숙해지기 전까지는 적용 범위도 되게 한정적일 것 같다. 하지만 상태가 있는 속성 기반 테스트라면? ’NDC22 달빛조각사에서 서버 테스트 코드를 작성하는 방법 발표 후기’에서 통합 테스트(Integration testing)에 관한 얘기를 했다. 테스트할 게임 서버 모듈을 모델링해서 상태가 있는 속성 기반 테스트를 할 수도 있겠단 생각이 들었다. 영감을 주는 챕터였다.

속성 기반 테스트를 하기 위한 지식을 이 책에 꾹꾹 눌러 담았다. 테스트에 대한 총괄적인 지식은 ’Unit Testing (블라디미르 코리코프, 2021)’ 만한 책이 없다. 최고의 책이다. ’속성 기반 테스트’에 관심이 있다면 이 책 ’Property-Based Testing with PropEr, Erlang, and Elixir (Fred Hebert, 2019)’을 추천하고 싶다. Erlang이나 Elixir 지식이 많이 필요하지 않으니 이 기회에 Elixir 맛도 살짝 보면서 속성 기반 테스트를 공부하는 것도 나쁘지 않은 선택이다. 사실 ’속성 기반 테스트’를 주제로 쓴 책이 이 책 말고는 찾기 힘들다.

읽으면서 배우다

밑줄

  • Property-based testing offers a lot of automation to keep the boring stuff away, but at the cost of a steeper learning curve and a lot of thinking to get it right, particularly when getting started.
  • Overall, you’ll see that property-based testing is not just using a bunch of tools to automate boring tasks, but a whole different way to approach testing, and to some extent, software design itself.
  • An example can be found in Thomas Arts’ slide set and presentation from the Erlang Factory 2016. In that talk, he mentioned using QuickCheck (the canonical property-based testing tool) to run tests on Project FIFO, an open source cloud project. With a mere 460 lines of property tests, they covered 60,000 lines of production code and uncovered twenty-five important bugs
  • property-based tests have nothing to do with listing examples by hand. Instead, you’ll want to write some kind of meta test: you find a rule that dictates the behavior that should always be the same no matter what sample input you give to your program and encode that into some executable code a property.
  • By listing many examples, we try to cover the full set of rules and edge cases that describe what the code should do. In property-based testing, we have to flip that around and come up with the rules first.
  • In fact, when we test with properties, the design and growth of tests requires an equal part of growth and design of the program itself. A common pattern when a property fails will be to figure out if it’s the system that is wrong or if it’s our idea of what it should do that needs to change. We’ll fix bugs, but we’ll also fix our understanding of the problem space. We’ll be surprised by how things we thought we knew are far more complex and tricky than we thought, and how often it happens.
  • Our tests are positive, happy-path tests, validating that everything is right and good things happen. Negative tests, by comparison, try to specifically exercise underspecified scenarios to find what happens in less happy paths.
  • Stateful property tests are particularly useful when ’what the code should do’? what the user perceives? is simple, but ’how the code does it’? how it is implemented? is complex.

링크