Scala 언어에도 다양한 테스트 라이브러리가 존재하는데 그 중 대표적인 테스트 라이브러리로 ScalaTest가 존재합니다.
ScalaTest는 사용하기도 쉽고, 사용법도 scala에 잘 어울리며 다양한 Mock 라이브러리 연동도 지원해주면서 테스트 스타일도 굉장히 다양하게 지원해주어서 개인적으로 사용중인 라이브러리이기도 합니다.
만약 테스트를 작성해야 한다면 ScalaTest를 적용해보기를 추천합니다.
ScalaTest 설치
ScalaTest를 설치하려고 결정했다면 build.sbt에 의존성을 추가해주기만 하면 사용 가능합니다.
Scalatic 라이브러리도 설치할 수 있는데 그냥 유틸리티 라이브러리이므로 사용하지 않아도 됩니다. 저도 사용한 적은 없습니다.
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.13.13"
lazy val root = (project in file("."))
.settings(
name := "scala-project",
)
// 여기서 필요한 라이브러리를 추가해주면 됩니다
libraryDependencies ++= Seq(
// 필요하면 scalatic 라이브러리를 추가합니다
// "org.scalactic" %% "scalactic" % "3.2.18",
// 마지막에 "test"를 명시해서 테스트에만 사용하는 라이브러리 라는 것을 알려줍니다
"org.scalatest" %% "scalatest" % "3.2.18" % "test",
)
ScalaTest의 다양한 테스트 스타일
ScalaTest는 테스트 스타일의 다양성을 인정하고 거의 대부분의 테스트 스타일을 trait로 지원해주는 라이브러리입니다.
예를 들면 xUnit 스타일의 테스트 방식도 지원해주고, BDD 스타일의 테스트 방식도 지원해주고, Specs2 라이브러리를 사용하던 사람들을 위한 스타일도 지원해주고 다양합니다.
테스트 스타일은 팀이나 개인의 선호의 영역이므로 수많은 스타일 중 하나를 선택해서 개발하면 됩니다. 다만 프로젝트 내부에서 다양한 스타일을 사용하면 테스트를 개발할 때마다 스타일을 바꿔야해서 생산성에 좋지 못하므로 하나만 정해서 사용하기를 추천합니다.
참고로 테스트를 개발할 때 ScalaTest의 Suite trait와 이름이 겹치는 것을 방지하고자 XXXTest나 XXXSpec이라고 이름짓는 것을 추천합니다.
FunSuite 스타일
FunSuite 스타일은 xUnit을 참조해서 만들어진 테스트 스타일로 작성이 간단하면서도 BDD의 이점을 어느 정도 누릴 수 있습니다. 간단하게 테스트를 작성하려는 팀에게 추천합니다.
import org.scalatest.funsuite.AnyFunSuite
class AnyFunSuiteSpec extends AnyFunSuite {
test("빈 Set의 크기는 0이다") {
assert(Set.empty.size == 0)
}
}
FlatSpec 스타일
FunSuite 스타일에서 BDD 스타일로 넘어가려고 시도하는 분들에게 추천합니다. FunSuite처럼 테스트를 작성할 수도 있고, BDD 스타일을 일부 따라해서 개발할 수도 있습니다. 테스트를 작성할 때 "X should Y" "A must B" 같이 문장을 작성해야 합니다.
테스트를 작성할 때 FunSuite때처럼 X should Y라고 바로 작성할 수도 있고, behavior of 함수를 사용해서 X should 와 Y, Z라는 행위를 나눌 수도 있습니다.
import org.scalatest.flatspec.AnyFlatSpec
class SetAnyFlatSpec extends AnyFlatSpec {
// X should Y 스타일
"빈 Set" should "크기는 0이다" in {
assert(Set.empty.size == 0)
}
// X should
behavior of "빈 Set"
// Y이다
it should "head를 호출하면 NoSucheElementException을 throw한다" in {
assertThrows[NoSuchElementException] {
Set.empty.head
}
}
// Z이다
it should "headOption을 호출하면 None을 반환한다" in {
assert(Set.empty.headOption == None)
}
}
FunSpec 스타일
루비의 RSpec 스타일에서 채용한 스타일로 BDD를 지향할 때 훌륭한 선택이 될 수 있습니다. describe와 it이라는 함수를 사용해 각 상황과 결과를 구분해 표현할 수 있습니다.
describe는 상황을 설명하고, it은 원하는 결과를 표현합니다.
import org.scalatest.funspec.AnyFunSpec
class SetAnyFunSpec extends AnyFunSpec {
describe("Set은") {
describe("비어있을 때") {
it("크기가 0이어야 한다") {
assert(Set.empty.size == 0)
}
it("head를 호출하면 NoSucheElementException을 throw한다") {
assertThrows[NoSuchElementException] {
Set.empty.head
}
}
}
}
}
WordSpec 스타일
specs나 specs2 스타일에서 채용한 스타일로 테스트 spec을 문자로 표현하기 적합한 테스트 스타일입니다. FunSpec과 비슷한 형태이며 when, should, in 함수로 세부적으로 나뉘는 것이 다릅니다. FunSpec의 describe가 다시 when, should로 나뉘었다고 이해해도 됩니다.
class SetAnyWordSpec extends AnyWordSpec {
"Set은" when {
"비어있을경우" should {
"크기가 0이어야 한다" in {
assert(Set.empty.size == 0)
}
"head를 호출하면 NoSucheElementException을 throw한다" in {
assertThrows[NoSuchElementException] {
Set.empty.head
}
}
}
"비어있지 않을경우" should {
"isEmpty를 호출하면 false여야한다" in {
assert(Set(1).isEmpty == false)
}
}
}
}
FreeSpec 스타일
기존의 스타일들과 다르게 어떤 고정된 형식도 없이 테스트를 작성하는 스타일입니다. - 기호로 depth를 깊게 가져가며, 자유롭게 테스트를 작성하고 싶을 때 적합합니다. 다만 어떠한 고정된 형식도 없어서 테스트가 중구난방 작성될 수도 있어 주의가 필요한 스타일입니다.
import org.scalatest.freespec.AnyFreeSpec
class SetAnyFreeSpec extends AnyFreeSpec {
"Set은" - {
"비어있을 경우" - {
"크기가 0이어야 한다" in {
assert(Set.empty.size == 0)
}
"head를 호출하면 NoSucheElementException을 throw한다" in {
assertThrows[NoSuchElementException] {
Set.empty.head
}
}
}
}
}
PropSpec 스타일
PropertyTest 를 작성할 때 적합한 스타일입니다. PropertyTest는 다른 테스트와 같이 작성하면 데이터 준비 문장들로 인해 테스트 코드의 줄이 매우 길어지게 되는데, 코드를 따로 작성하고 싶을 때 사용할만한 스타일입니다.
이를 직접 사용하지 않고 TableDrivenPropertyChecks trait만 상속해도 Property test를 할 수 있어 사용은 비교적 드문 스타일입니다.
import org.scalatest._
import matchers._
import org.scalatest.propspec.AnyPropSpec
import prop._
import scala.collection.immutable._
class SetAnyPropSpec extends AnyPropSpec with TableDrivenPropertyChecks with should.Matchers {
val examples =
Table(
"set",
BitSet.empty,
HashSet.empty[Int],
TreeSet.empty[Int]
)
property("비어있는 Set은 크기가 0이어야 한다") {
forAll(examples) { set =>
set.size should be (0)
}
}
property("head를 호출하면 NoSucheElementException을 throw한다") {
forAll(examples) { set =>
a [NoSuchElementException] should be thrownBy { set.head }
}
}
}
FeatureSpec 스타일
프로그래머가 아닌 기획자, 도메인 전문가 등의 사람들과 소통하고 싶을 때 적용하기 적합한 스타일입니다.
BDD 테스트에 가장 적합한 스타일이며, Gherkin 테스트를 작성할 수 있는 유일한 스타일입니다.
Feature, Scenario, Info, Given, When, Then으로 작성되며 실무에서 테스트를 작성할 때 가장 추천하는 스타일입니다.
개인적으로는 Cucumber보다도 더욱 사용자 친화적으로 테스트를 작성할 수 있다고 생각합니다.
import org.scalatest._
import org.scalatest.featurespec.AnyFeatureSpec
case class TVSet(on: Boolean) {
def isOn: Boolean = on
def pressPowerButton(): TVSet = {
copy(on = !on)
}
}
class TVSetSpec extends AnyFeatureSpec with GivenWhenThen {
info("TV의 사용자로서")
info("TV를 on off 하고 싶다")
info("on off 기능을 통해 TV를 자유롭게 시청하고")
info("TV를 보지 않을 때 에너지를 절약하고싶다")
Feature("TV 전원 버튼") {
Scenario("TV가 꺼져있을 때 사용자가 전원 버튼을 누른다") {
Given("TV의 전원이 꺼져 있다")
val tv = TVSet(false)
When("전원 버튼을 누른다")
val actual = tv.pressPowerButton()
Then("TV 전원이 켜진다")
assert(actual.isOn)
}
Scenario("TV가 켜져있을 때 사용자가 전원 버튼을 누른다") {
Given("TV의 전원이 켜져있다")
val tv = TVSet(true)
When("전원 버튼을 누른다")
tv.pressPowerButton()
Then("TV 전원이 꺼진다")
assert(!tv.isOn)
}
}
}
RefSpec 스타일
RefSpec 스타일은 JVM 기반에서만 동작하므로 scala.js에서는 사용할 수 없습니다.
RefSpec은 다른 Spec들과 다르게 직접 함수를 작성하므로(def) 테스트를 컴파일할 때 depth를 한 단계 줄여줍니다(테스트를 나타내는 when, should, test 같은 함수들은 내부적으로 함수를 한번 더 호출하므로 공정이 한 단계 더 추가됩니다).
서비스가 매우 커서 컴파일이 오래 걸릴 때 사용할만한 스타일로, 처음부터 적용하기에는 적합하지 않습니다.
import org.scalatest.refspec.RefSpec
class SetRefSpec extends RefSpec {
object `Set은` {
object `비어있을 때` {
def `크기가 0이어야 한다` {
assert(Set.empty.size == 0)
}
def `head를 호출하면 NoSucheElementException을 throw한다` {
assertThrows[NoSuchElementException] {
Set.empty.head
}
}
}
}
}
추천하는 스타일
Controller-Service-Repository 레이어로 구분될 때
Controller, Service에서는 FeatureSpec으로 Gherkin 테스트를 작성하기를 추천드립니다.
테스트를 작성하는 목적은 코드를 잘 짜는 것이 아닌 사용자의 요구사항에 부합하는 것이므로 개발자 이외의 기획자나 도메인 전문가가 정해주는 요구사항을 만족하는 테스트를 작성하는 것이 가장 중요합니다.
이 요구사항에 가장 부합하는 테스트 방법이 FeatureSpec이므로 이 스타일을 사용하기를 추천드립니다.
Repository는 외부의 요구사항이 중요하지 않고 어떤 기능이 동작하는지가 중요하므로 여기서는 FunSuite로 간단히 처리하거나 FunSpec이나 WordSpec으로 작성하면 편합니다.
유틸이나 라이브러리 형태의 코드
유틸이나 라이브러리 형태의 코드는 사용자의 요구사항이 존재하지 않고 개발자가 편의를 위해 작성하는 코드이므로 FunSuite, FunSpec이나 WordSpec으로 작성하는 것을 추천드립니다.
하나의 테스트에 매개변수만 바꿔서 테스트가 필요할 때
이럴때 사용하라고 나온 것이 PropSpec이므로 새롭게 테스트 파일을 만들어서 PropSpec만을 위한 테스트를 작성하기를 추천드립니다.
테스트 스타일은 팀이나 개인의 선호가 가장 중요하지 따로 정해진 정답은 존재하지 않으므로 각자의 상황에 맞춰서 적절히 선택해 사용하면 됩니다.