文件

§使用 ScalaTest 測試應用程式

為應用程式撰寫測試可能會是一個複雜的過程。Play 提供了輔助程式和應用程式 stub,而 ScalaTest 提供了一個整合函式庫,ScalaTest + Play,讓測試應用程式變得盡可能簡單。

§概觀

測試的位置在「test」資料夾中。

您可以從 Play 主控台執行測試。

Play 中的測試基於 SBT,而完整的說明可以在 測試 SBT 章節中找到。

§使用 ScalaTest + Play

若要使用 ScalaTest + Play,您需要將它新增到您的建置中,方法是像這樣變更 build.sbt

libraryDependencies ++= Seq(
  "org.scalatestplus.play" %% "scalatestplus-play" % "x.x.x" % Test
)

其中 x.x.xscalatestplus-play 人工製品的特定版本,例如 5.1.0。請參閱 此處提供的版本

您不需要明確地將 ScalaTest 或 ScalaTest plus mockito 新增到您的建置中。ScalaTest 的正確版本將自動作為 ScalaTest + Play 的傳遞依賴項帶入。但是,您需要選擇與您的 Play 版本相符的 ScalaTest + Play 版本。您可以透過查看 ScalaTest + Play 的版本相容性矩陣來執行此操作。

ScalaTest + Play 中,您可以透過擴充 PlaySpec 特質來定義測試類別。以下是一個範例

import org.scalatestplus.play._

import scala.collection.mutable

class StackSpec extends PlaySpec {

  "A Stack" must {
    "pop values in last-in-first-out order" in {
      val stack = new mutable.Stack[Int]
      stack.push(1)
      stack.push(2)
      stack.pop() mustBe 2
      stack.pop() mustBe 1
    }
    "throw NoSuchElementException if an empty stack is popped" in {
      val emptyStack = new mutable.Stack[Int]
      a[NoSuchElementException] must be thrownBy {
        emptyStack.pop()
      }
    }
  }
}

您也可以定義您自己的基本類別,而不是使用PlaySpec

您可以在 Play 本身、IntelliJ IDEA(使用Scala 外掛程式)或 Eclipse(使用Scala IDEScalaTest Eclipse 外掛程式)中執行測試。請參閱IDE 頁面以取得更多詳細資訊。

§Matchers

PlaySpec會混入 ScalaTest 的MustMatchers,因此您可以使用 ScalaTest 的 matchers DSL 來撰寫斷言

import play.api.test.Helpers._

"Hello world" must endWith ("world")

如需更多資訊,請參閱MustMatchers的說明文件。

§Mockito

您可以使用模擬來隔離單元測試,以防範外部依賴關係。例如,如果您的類別依賴於外部DataService類別,您可以提供適當的資料給您的類別,而不用實例化DataService物件。

ScalaTest 提供與Mockito的整合,透過其MockitoSugar特質。

若要使用 Mockito,請將MockitoSugar混入您的測試類別,然後使用 Mockito 函式庫來模擬依賴關係

case class Data(retrievalDate: java.util.Date)

trait DataService {
  def findData: Data
}
import org.scalatestplus.mockito.MockitoSugar
import org.scalatestplus.play._

import org.mockito.Mockito._

class ExampleMockitoSpec extends PlaySpec with MockitoSugar {

  "MyService#isDailyData" should {
    "return true if the data is from today" in {
      val mockDataService = mock[DataService]
      when(mockDataService.findData).thenReturn(Data(new java.util.Date()))

      val myService = new MyService() {
        override def dataService = mockDataService
      }

      val actual = myService.isDailyData
      actual mustBe true
    }
  }
}

模擬對於測試類別的公開方法特別有用。模擬物件和私有方法是可行的,但難度相當高。

§單元測試模型

Play 不要求模型使用特定的資料庫資料存取層。但是,如果應用程式使用 Anorm 或 Slick,則模型通常會在內部參照資料庫存取。

import anorm._
import anorm.SqlParser._

case class User(id: String, name: String, email: String) {
   def roles = DB.withConnection { implicit connection =>
      ...
    }
}

對於單元測試,此方法可能會讓模擬roles方法變得棘手。

常見的做法是將模型與資料庫隔離,並盡可能抽象資料庫存取至儲存庫層。

case class Role(name: String)

case class User(id: String, name: String, email: String)
trait UserRepository {
  def roles(user: User): Set[Role]
}
class AnormUserRepository extends UserRepository {
  import anorm._
  import anorm.SqlParser._

  def roles(user:User) : Set[Role] = {
    ...
  }
}

然後透過服務存取它們

class UserService(userRepository: UserRepository) {

  def isAdmin(user: User): Boolean = {
    userRepository.roles(user).contains(Role("ADMIN"))
  }
}

如此一來,isAdmin 方法可以透過模擬 UserRepository 參照並將它傳遞至服務中來進行測試

class UserServiceSpec extends PlaySpec with MockitoSugar {

  "UserService#isAdmin" should {
    "be true when the role is admin" in {
      val userRepository = mock[UserRepository]
      when(userRepository.roles(any[User])).thenReturn(Set(Role("ADMIN")))

      val userService = new UserService(userRepository)

      val actual = userService.isAdmin(User("11", "Steve", "[email protected]"))
      actual mustBe true
    }
  }
}

§單元測試控制器

由於控制器只是常規類別,因此您可以輕鬆使用 Play 輔助程式對它們進行單元測試。如果控制器依賴於其他類別,使用 依賴注入 將使您能夠模擬這些依賴項。例如,給定以下控制器

class ExampleController(val controllerComponents: ControllerComponents) extends BaseController {
  def index() = Action {
    Ok("ok")
  }
}

您可以像這樣測試它

import scala.concurrent.Future

import org.scalatestplus.play._

import play.api.mvc._
import play.api.test._
import play.api.test.Helpers._

class ExampleControllerSpec extends PlaySpec with Results {

  "Example Page#index" should {
    "should be valid" in {
      val controller             = new ExampleController(Helpers.stubControllerComponents())
      val result: Future[Result] = controller.index().apply(FakeRequest())
      val bodyText: String       = contentAsString(result)
      bodyText mustBe "ok"
    }
  }
}

§單元測試 EssentialAction

測試 ActionFilter 可能需要測試 EssentialAction有關 EssentialAction 的更多資訊

為此,測試 Helpers.call 可以這樣使用

class ExampleEssentialActionSpec extends PlaySpec with GuiceOneAppPerSuite {

  implicit lazy val materializer: Materializer   = app.materializer
  implicit lazy val Action: DefaultActionBuilder = app.injector.instanceOf(classOf[DefaultActionBuilder])

  "An essential action" should {
    "can parse a JSON body" in {
      val action: EssentialAction = Action { request =>
        val value = (request.body.asJson.get \ "field").as[String]
        Ok(value)
      }

      val request = FakeRequest(POST, "/").withJsonBody(Json.parse("""{ "field": "value" }"""))

      val result = call(action, request)

      status(result) mustEqual OK
      contentAsString(result) mustEqual "value"
    }
  }
}

下一步:使用 ScalaTest 編寫功能測試


在此文件檔中找到錯誤嗎?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件檔指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?前往 我們的社群論壇 與社群展開對話。