§使用 ScalaTest 測試應用程式
為應用程式撰寫測試可能會是一個複雜的過程。Play 提供了輔助程式和應用程式 stub,而 ScalaTest 提供了一個整合函式庫,ScalaTest + Play,讓測試應用程式變得盡可能簡單。
§概觀
測試的位置在「test」資料夾中。
您可以從 Play 主控台執行測試。
- 若要執行所有測試,請執行
test
。 - 若要只執行一個測試類別,請執行
test-only
,後接類別名稱,例如test-only my.namespace.MySpec
。 - 若要只執行失敗的測試,請執行
test-quick
。 - 若要持續執行測試,請執行一個前面帶有波浪符號的命令,例如
~test-quick
。 - 若要在主控台中存取測試輔助程式,例如
FakeRequest
,請執行test:console
。
Play 中的測試基於 SBT,而完整的說明可以在 測試 SBT 章節中找到。
§使用 ScalaTest + Play
若要使用 ScalaTest + Play,您需要將它新增到您的建置中,方法是像這樣變更 build.sbt
libraryDependencies ++= Seq(
"org.scalatestplus.play" %% "scalatestplus-play" % "x.x.x" % Test
)
其中 x.x.x
是 scalatestplus-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 IDE和ScalaTest 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
測試 Action
或 Filter
可能需要測試 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"
}
}
}
在此文件檔中找到錯誤嗎?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件檔指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?前往 我們的社群論壇 與社群展開對話。