GraphQL 標誌GraphQL

使用 GraphQL 模擬伺服器很容易

2016 年 4 月 19 日 by Jonas Helfer

這篇客座文章由 Jonas Helfer 撰寫,他是 Meteor 的工程師,負責 Apollo。

您是否認為模擬後端始終是一項繁瑣的任務?如果您這麼認為,閱讀這篇文章可能會改變您的想法…

模擬是建立元件假版本的做法,讓您可以獨立開發和測試應用程式的其他部分。模擬後端是快速建立前端原型的好方法,讓您可以在不啟動任何伺服器的情況下測試前端。API 模擬非常有用,以至於 快速 Google 搜尋 會出現數十種昂貴的產品和服務,承諾協助您。

遺憾的是,我認為市面上沒有任何解決方案能讓它變得像它應該的那麼容易。事實證明,那是因為他們一直試圖使用錯誤的技術來執行此操作

編輯註解:這篇文章中的概念是準確的,但有些程式碼範例並未展示新的使用模式。閱讀後,請參閱 graphql-tools 文件,了解如何使用目前的模擬。

為什麼要模擬?#

模擬後端會回傳的資料,基於兩個主要原因,非常有用

  1. 當你還沒有後端時,它讓你開始開發前端功能。這對於前端和後端元件經常平行開發的專案來說,至關重要。
  2. 它讓你可以在不連線到真實後端的情況下,在本地端執行測試,這快得多,而且更安全。隨著你的程式碼庫擴充,你的應用程式變得更複雜,只為了執行一些測試,就啟動所有伺服器基礎架構是不可行的。

如果模擬你的後端 API 有如此明顯的好處,為什麼不是每個人都這麼做?我想這是因為模擬通常看起來麻煩,不值得這麼做。

為什麼模擬後端很困難?#

假設你的後端是透過瀏覽器,以 HTTP 呼叫的 REST API。你有一個人負責後端,另一個人負責前端。後端程式碼實際上決定了每個 REST 端點所回傳的資料形狀,但模擬必須在前端程式碼中進行。這表示模擬程式碼將在後端每次變更時中斷,除非兩者同時變更。更糟的是,如果你對模擬後端進行前端測試,而模擬後端並未更新為你的後端,你的測試可能會通過,但你的實際前端將無法運作。

與其讓更多依賴項保持最新,較簡單的選項就是不要模擬 REST API,或讓後端負責模擬它自己,這樣所有內容都在同一個地方。這可能比較容易,但也會讓你變慢。

我常聽到的另一個原因,是人們在專案中不模擬後端,是因為設定需要時間:首先,你必須在資料擷取層中包含額外的邏輯,讓你能夠開啟和關閉模擬,其次,你必須實際描述模擬資料應有的樣子。對於任何需要大量繁瑣工作的非平凡 API 來說,這都是如此。

後端模擬之所以困難,這兩個原因實際上是出於同一個根本原因:沒有以機器可用的格式提供標準的 REST API 說明,其中包含模擬所需的所有資訊,而且後端和前端都可以使用。有些 API 說明標準,例如 Swagger,但它們不包含您需要的所有資訊,而且撰寫和維護起來可能很麻煩。除非您願意付費購買服務或產品,甚至可能在這種情況下,模擬都是一項繁重的工作。

實際上,我應該說模擬過去是一項繁重的工作,因為一項新技術正在改變我們對 API 的看法:GraphQL。

使用類型系統,模擬很簡單!#

GraphQL 使模擬變得容易,因為每個 GraphQL 後端都附帶一個靜態類型系統。類型可以在您的後端和前端之間共用,它們包含所有必要的資訊,讓模擬變得非常快速且方便。使用 GraphQL,沒有藉口不模擬您的後端以進行開發或測試。

以下是如何輕鬆建立一個模擬後端,它將接受任何有效的 GraphQL 查詢,我們正在建立的 GraphQL 模擬工具作為我們新的 GraphQL 伺服器工具組 的一部分

// > npm install graphql-tools
import { mockServer } from "graphql-tools"
import schema from "./mySchema.graphql"
const myMockServer = mockServer(schema)
myMockServer.query(`{
allUsers: {
id
name
}
}`)
// returns
// {
// data: {
// allUsers:[
// { id: 'ee5ae76d-9b91-4270-a007-fad2054e2e75', name: 'lorem ipsum' },
// { id: 'ca5c182b-99a8-4391-b4b4-4a20bd7cb13a', name: 'quis ut' }
// ]
// }
// }

每個 GraphQL 伺服器都需要一個架構,所以這不是您僅為模擬而需要編寫的額外程式碼。而查詢是您的元件已經用於擷取資料的查詢,所以這也不是您僅為模擬而編寫的程式碼。不計算匯入陳述式,我們只用一行程式碼就能模擬整個後端!

與市面上大多數 REST API 相比,後者模擬的方式是剖析 URL,並針對每個端點回傳自訂格式的資料。模擬單一端點回傳一些看似真實的資料,需要數十行程式碼。使用 GraphQL,格式會編碼在查詢中,加上架構,我們有足夠的資訊,使用單一行程式碼模擬伺服器。

我是否曾提到,這單一行就足以回傳任何您可傳送的有效 GraphQL 查詢的模擬資料?不只某些有效查詢,而是任何有效查詢!很酷吧?

自訂模擬資料#

在上述範例中,模擬伺服器每次查詢時,都會回傳完全隨機的 ID 和字串。當您剛開始建置應用程式,只想查看 UI 程式碼在不同狀態下的樣貌時,這可能就夠了,但當您開始微調版面配置,或想使用模擬伺服器測試元件邏輯時,您可能需要更逼真的資料。

幸運的是,這只需要多一點點功夫:模擬資料的自訂化,正是 Apollo 模擬工具發揮作用之處,因為它讓您自訂它回傳的模擬資料的幾乎所有面向。

它讓您執行下列所有操作,以及更多操作

// customize mocking per type (i.e. Integer, Float, String)
mockServer(schema, {
Int: () => 6,
Float: () => 22.1,
String: () => 'Hello',
});
// customize mocking per field in the schema (i.e. for Person.name and Person.age)
mockServer(schema, {
Person: () => ({
name: casual.name,
age: () => casual.integer(0,120),
})
});
// mock lists of specific or random length( and lists of lists of lists …)
mockServer(schema, {
Person: () => {
// a list of length between 2 and 6
friends: () => new MockList([2,6]),
// a list of three lists of two items: [[1, 1], [2, 2], [3, 3]]
listOfLists: () => new MockList(3, () => new MockList(2)),
},
});
// customize mocking of a field or type based on the query arguments
mockServer(schema, {
Person: () => {
// the number of friends in the list now depends on numPages
paginatedFriends: (o, { numPages }) => new MockList(numPages * PAGE_SIZE),
},
});
// You can also disable mocking for specific fields, pass through to the backend, etc.

對於每個類型和每個欄位,您可以提供一個函式,呼叫該函式來產生模擬資料。欄位上的模擬函式優先於類型上的模擬函式,但它們可以完美地搭配使用:欄位模擬函式只需描述對它們而言重要的物件屬性,類型模擬函式會填入其餘部分。

模擬函式實際上只是 GraphQL 解析函式,偽裝成另一種形式。這表示您的模擬可以執行任何您在 GraphQL 解析函式中可以執行的操作。如果您願意,您可以使用它撰寫整個後端。我不是說您應該這麼做,但您可以。

我想這個工具真正的力量在於,儘管它允許幾乎任意複雜的客製化,但你可以非常快速地開始,並且在需要時以微小的步驟增加你的模擬的複雜性。每一步都非常簡單,會讓你覺得像微風拂面。

但說得夠多了,以下是完整的範例

import { mockServer, MockList } from "graphql-tools"
import casual from "casual-browserify"
// The GraphQL schema. Described in more detail here:
// https://medium.com/apollo-stack/the-apollo-server-bc68762e93b
const schema = `
type User {
id: ID!
name: String
lists: [List]
}
type List {
id: ID!
name: String
owner: User
incomplete_count: Int
tasks(completed: Boolean): [Task]
}
type Task {
id: ID!
text: String
completed: Boolean
list: List
}
type RootQuery {
user(id: ID): User
}
schema {
query: RootQuery
}
`
// Mock functions are defined per type and return an
// object with some or all of the fields of that type.
// If a field on the object is a function, that function
// will be used to resolve the field if the query requests it.
const server = mockServer(schema, {
RootQuery: () => ({
user: (o, { id }) => ({ id }),
}),
List: () => ({
name: () => casual.word,
tasks: () => new MockList(4, (o, { completed }) => ({ completed })),
}),
Task: () => ({ text: casual.words(10) }),
User: () => ({ name: casual.name }),
})
mockServer.query(`
query tasksForUser{
user(id: 6) {
id
name
lists {
name
completeTasks: tasks(completed: true) {
completed
text
}
incompleteTasks: tasks(completed: false) {
completed
text
}
anyTasks: tasks {
completed
text
}
}
}
}`)

現場示範 + 自己嘗試#

若要看到範例實際執行,以及它產生的輸出,請前往 現場示範 嘗試執行一些查詢

如果你想修改範例,只要按一下 Launchpad UI 中的「下載」按鈕即可。如果你好奇它是如何運作的,或是想看看我們為 GraphQL 建置了哪些其他工具,請前往 apollostack/graphql-tools

很酷吧?所有這些都透過使用類型系統而成為可能。那只是個開始而已,我們正努力縮小模擬與實際之間的差距,以便你的模擬伺服器可以在你加入更多功能時逐漸轉變成你的實際伺服器。


這篇文章最初發布於 Apollo 部落格。我們每週會發布一或兩篇文章,關於我們正在處理和思考的事項。