2016 年 4 月 19 日 由 Jonas Helfer 撰写
这篇文章由 Meteor 工程师 Jonas Helfer 撰写,他正在开发 Apollo。
你是否认为模拟后端总是很繁琐的任务?如果你这样认为,阅读这篇文章可能会改变你的想法……
模拟是指创建组件的假版本,以便你可以独立地开发和测试应用程序的其他部分。模拟后端是快速构建前端原型的好方法,它允许你在不启动任何服务器的情况下测试前端。API 模拟非常有用,以至于 快速搜索 Google 会出现数十种昂贵的产品和服务,它们承诺帮助你。
遗憾的是,我认为现有的解决方案都没有像应该的那样简单。事实证明,这是因为他们一直在使用错误的技术!
编辑注:这篇文章中的概念是准确的,但一些代码示例没有展示新的使用模式。阅读完后,请参阅 graphql-tools 文档,了解如何使用当今的模拟技术。
模拟后端将返回的数据对于以下两个主要原因非常有用
如果模拟您的后端 API 有如此明显的优势,为什么不是每个人都这样做呢?我认为这是因为模拟通常看起来太麻烦了,不值得。
假设您的后端是某些通过 HTTP 从浏览器调用的 REST API。您有一位负责后端的人,还有一位负责前端的人。后端代码实际上决定了每个 REST 端点返回的数据的形状,但模拟必须在前端代码中完成。这意味着每次后端更改时,模拟代码都会中断,除非两者同时更改。更糟糕的是,如果您针对与后端不一致的模拟后端进行前端测试,您的测试可能会通过,但实际的前端将无法正常工作。
与其保持更多依赖项的最新状态,不如选择不模拟 REST API,或者让后端负责模拟自身,这样所有内容都集中在一个地方。这可能更容易,但也会减慢您的速度。
我经常听到的另一个原因是人们在项目中不模拟后端,因为设置需要时间:首先,您必须在数据获取层中包含额外的逻辑,让您能够打开和关闭模拟;其次,您必须实际描述模拟数据应该是什么样子。对于任何需要大量繁琐工作的非平凡 API 来说,这都是如此。
模拟后端很困难的这两个原因实际上都源于同一个根本原因:没有标准的 REST API 描述以机器可读的格式存在,并且包含模拟所需的所有信息,可以被后端和前端使用。有一些 API 描述标准,例如 Swagger,但它们不包含您需要的所有信息,并且编写和维护起来可能很麻烦。除非您想为服务或产品付费——也许即使那样——模拟也是一项繁重的工作。
实际上,我应该说模拟过去是一项繁重的工作,因为一项新技术正在改变我们对 API 的看法:GraphQL。
GraphQL 使模拟变得容易,因为每个 GraphQL 后端都带有静态类型系统。这些类型可以在您的后端和前端之间共享,并且包含所有必要的信息,使模拟变得非常快速和便捷。使用 GraphQL,没有理由不为开发或测试模拟您的后端。
以下是使用我们作为新 GraphQL 服务器工具包的一部分构建的 GraphQL 模拟工具创建模拟后端的简便方法。
// > npm install graphql-toolsimport { 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 相比,在 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 argumentsmockServer(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-bc68762e93bconst 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 博客上。我们每周发布一到两篇文章,内容是我们正在进行的工作和思考。