文件

§測試網路服務用戶端

撰寫網路服務客戶端時,會用到很多程式碼,包括準備要求、序列化和反序列化主體,以及設定正確的標頭。由於很多程式碼都使用字串和弱型別對應,因此測試非常重要。不過,測試也有一些挑戰。一些常見的方法包括

§針對實際網路服務進行測試

當然,這會讓客戶端程式碼達到最高層級的信賴度,不過通常不切實際。如果這是第三方網路服務,可能會有限速措施,讓你的測試無法執行(而且針對第三方服務執行自動化測試並非良好網路公民的行為)。可能無法設定或確保你的測試在該服務上需要必要的資料存在,而且你的測試可能會對服務產生不良的副作用。

§針對網路服務的測試執行個體進行測試

這比前一個方法好一點,不過仍有一些問題。許多第三方網路服務不提供測試執行個體。這也表示你的測試依賴於測試執行個體是否正在執行,這表示測試服務可能會導致你的建置失敗。如果測試執行個體在防火牆之後,也會限制測試可以從何處執行。

§模擬 http 客戶端

這種方法對測試程式碼的信賴度最低,這種測試通常等同於測試程式碼是否執行它所執行的動作,這沒有價值。針對模擬網路服務客戶端進行的測試會顯示程式碼執行並執行某些動作,但無法確認程式碼執行的任何動作是否實際對應到發出的有效 HTTP 要求。

§模擬網路服務

此方法在針對實際網路服務執行測試和模擬 http 伺服器之間取得良好的折衷。您的測試將顯示它所發出的所有要求都是有效的 HTTP 要求,主體的序列化/取消序列化運作正常,等等,但它們將完全獨立,不依賴於任何第三方服務。

Play 提供一些輔助工具程式,用於在測試中模擬網路服務,這使得此測試方法成為一個非常可行且有吸引力的選項。

§測試 GitHub 伺服器

舉例來說,假設您已撰寫一個 GitHub 伺服器,而且您想要測試它。此伺服器非常簡單,它只允許您查詢公開儲存庫的名稱

import javax.inject.Inject

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import play.api.libs.ws.WSClient

class GitHubClient(ws: WSClient, baseUrl: String)(implicit ec: ExecutionContext) {
  @Inject def this(ws: WSClient, ec: ExecutionContext) = this(ws, "https://api.github.com")(ec)

  def repositories(): Future[Seq[String]] = {
    ws.url(baseUrl + "/repositories").get().map { response => (response.json \\ "full_name").map(_.as[String]).toSeq }
  }
}

請注意,它將 GitHub API 基本 URL 作為參數 - 我們將在測試中覆寫此參數,以便我們可以將它指向我們的模擬伺服器。

為了測試此伺服器,我們需要一個嵌入式 Play 伺服器來實作此端點。我們可以使用 Server withRouter 輔助工具程式搭配 字串內插路由 DSL 來執行此操作

import play.api.libs.json._
import play.api.mvc._
import play.api.routing.sird._
import play.core.server.Server

Server.withRouterFromComponents() { components =>
  import Results._
  import components.{ defaultActionBuilder => Action }
  {
    case GET(p"/repositories") =>
      Action {
        Ok(Json.arr(Json.obj("full_name" -> "octocat/Hello-World")))
      }
  }
} { implicit port =>

withRouter 方法會採用一個程式碼區塊,作為伺服器啟動時輸入的埠號。預設情況下,Play 會在隨機的可用埠上啟動伺服器 - 這表示您不必擔心在建置伺服器上發生資源競爭或將埠指派給測試,但這表示您的程式碼確實需要得知將使用哪個埠。

現在,為了測試 GitHub 伺服器,我們需要一個 WSClient。Play 提供一個 WsTestClient 特質,其中有一些用於建立測試伺服器的工廠方法。withClient 會採用一個隱含埠,這很方便與 Server.withRouter 方法搭配使用。

WsTestClient.withClient 方法在此建立的用戶端是一個特殊用戶端,如果您給它一個相對 URL,它會將主機名稱預設為 localhost,而埠號則預設為隱式傳遞的埠號。使用這個方法,我們可以簡單地將 GitHub 用戶端的基礎 URL 設定為空字串。

將所有內容放在一起,我們有以下內容

import scala.concurrent.duration._
import scala.concurrent.Await

import org.specs2.mutable.Specification
import play.api.libs.json._
import play.api.mvc._
import play.api.routing.sird._
import play.api.test._
import play.core.server.Server

class GitHubClientSpec extends Specification {
  import scala.concurrent.ExecutionContext.Implicits.global

  "GitHubClient" should {
    "get all repositories" in {
      Server.withRouterFromComponents() { components =>
        import Results._
        import components.{ defaultActionBuilder => Action }
        {
          case GET(p"/repositories") =>
            Action {
              Ok(Json.arr(Json.obj("full_name" -> "octocat/Hello-World")))
            }
        }
      } { implicit port =>
        WsTestClient.withClient { client =>
          val result = Await.result(new GitHubClient(client, "").repositories(), 10.seconds)
          result must_== Seq("octocat/Hello-World")
        }
      }
    }
  }
}

§傳回檔案

在之前的範例中,我們手動為模擬服務建立 json。通常最好擷取您測試服務的實際回應,然後傳回。為了協助這項工作,Play 提供了一個 sendResource 方法,它允許輕鬆建立類別路徑上檔案的結果。

因此,在對實際 GitHub API 提出要求後,建立一個檔案來將其儲存在測試資源目錄中。如果您使用 Play 目錄配置,測試資源目錄為 test/resources;如果您使用標準 sbt 目錄配置,則為 src/test/resources。在這個案例中,我們會將其命名為 github/repositories.json,它將包含以下內容

[
  {
    "id": 1296269,
    "owner": {
      "login": "octocat",
      "id": 1,
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "name": "Hello-World",
    "full_name": "octocat/Hello-World",
    "description": "This your first repo!",
    "private": false,
    "fork": false,
    "url": "https://api.github.com/repos/octocat/Hello-World",
    "html_url": "https://github.com/octocat/Hello-World"
  }
]

您可以決定修改它以符合您的測試需求,例如,如果您的 GitHub 用戶端使用上述回應中的 URL 向其他端點提出要求,您可能會移除它們的 https://api.github.com 前綴,讓它們也變成相對的,並且會由測試用戶端自動路由到正確埠號上的 localhost。

現在,修改路由器來提供這個資源

import play.api.mvc._
import play.api.routing.sird._
import play.api.test._
import play.core.server.Server

Server.withApplicationFromContext() { context =>
  new BuiltInComponentsFromContext(context) with HttpFiltersComponents {
    override def router: Router = Router.from {
      case GET(p"/repositories") =>
        Action { req => Results.Ok.sendResource("github/repositories.json")(executionContext, fileMimeTypes) }
    }
  }.application
} { implicit port =>

請注意,由於檔案名稱的副檔名為 .json,Play 會自動設定內容類型為 application/json

§擷取設定程式碼

如果您只有一個要執行的測試,到目前為止實作的測試就很好,但如果您有許多要測試的方法,將模擬用戶端設定程式碼萃取到一個輔助方法中可能會更有意義。例如,我們可以定義一個 withGitHubClient 方法

import play.api.mvc._
import play.api.routing.sird._
import play.core.server.Server
import play.api.test._

def withGitHubClient[T](block: GitHubClient => T): T = {
  Server.withApplicationFromContext() { context =>
    new BuiltInComponentsFromContext(context) with HttpFiltersComponents {
      override def router: Router = Router.from {
        case GET(p"/repositories") =>
          Action { req => Results.Ok.sendResource("github/repositories.json")(executionContext, fileMimeTypes) }
      }
    }.application
  } { implicit port => WsTestClient.withClient { client => block(new GitHubClient(client, "")) } }
}

然後在測試中使用它看起來像

withGitHubClient { client =>
  val result = Await.result(client.repositories(), 10.seconds)
  result must_== Seq("octocat/Hello-World")
}

下一步:記錄


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