DrillLab
第 09 / 17 节LESSON 09 / 17约 13 分钟~13 min

TODO 1 · User.ordersTODO 1 · User.orders

Federation 链路的终点。三行代码,但每一行都有理由。The last step of the Federation path. Three lines of code, and every line has a reason.

2 个练习2 exercisesFederation · 第 3 部分Federation · Part 3
这一页有什么On this page10
学完这节你会After this lesson you can
  • 独立写出 User.ordersWrite User.orders without help
  • 解释 user.id 是从哪来的Explain where user.id comes from
  • 说清为什么必须 ?? [] 兜底Explain why ?? [] is required as a fallback
  • 写出符合 TODO 要求的错误处理和 correlation id 日志Write the error handling and correlation id logging the TODO asks for
这在考试里考什么What the exam does with this

这是 Federation 那部分唯一一个要你写的 entity 字段。它的正确性直接决定「Router 能不能把用户和订单缝起来」。两个测试查它。This is the only entity field the Federation part asks you to write. Whether it is correct decides whether the Router can join users to their orders. Two tests check it.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.jsUser.orders

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

JavaScriptorderResolvers.js源项目From source
1import DataLoader from 'dataloader';
2import { GraphQLError } from 'graphql';
3
4// Custom error codes
5const ErrorCodes = {
6 ORDER_NOT_FOUND: 'ORDER_NOT_FOUND',
7 INVALID_INPUT: 'INVALID_INPUT',
8 INVENTORY_ERROR: 'INVENTORY_ERROR',
9 SERVICE_ERROR: 'SERVICE_ERROR'
10};
11
12// DataLoader for batching shipping info requests
13function createShippingInfoLoader(shippingDataSource) {
14 return new DataLoader(async orderIds => {
15 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
16
17 const shippingInfos = await Promise.all(
18 orderIds.map(id => shippingDataSource.getShippingInfo(id))
19 );
20
21 return shippingInfos;
22 });
23}
24
25// DataLoader for batching order requests
26function createOrderLoader(orderDataSource) {
27 return new DataLoader(async orderIds => {
28 console.log(`[DataLoader] Batching ${orderIds.length} order requests`);
29
30 const orders = await Promise.all(
31 orderIds.map(id => orderDataSource.getOrderById(id))
32 );
33
34 return orders;
35 });
36}
37
38export const resolvers = {
39 User: {
40 // Reference Resolver - extend User entity from Accounts subgraph
41 __resolveReference(user, { dataSources, loaders }) {
42 return { id: user.id };
43 },
44
45 // Field resolver with caching
46 async orders(user, _, { dataSources, loaders, correlationId }) {
47 // TODO: Implement orders resolver with proper error handling and correlation ID tracing
48 return [];
49 }
50 },
51
52 Order: {
53 // Field resolver for shipping info with DataLoader
54 async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
55 // TODO: Implement shipping info resolver using DataLoader to prevent N+1 queries
56 return null;
57 }
58 },
59
60 Query: {
61 async order(_, { id }, { dataSources, loaders, correlationId }) {
62 // TODO: Implement order query using DataLoader with structured error handling
63 return null;
64 },
65
66 async orders(_, { userId }, { dataSources, correlationId }) {
67 // TODO: Implement orders query with error handling and correlation ID logging
68 return [];
69 }
70 },
71
72 Mutation: {
73 // Mutation provided for reference - candidates focus on Query resolvers
74 async createOrder(_, { userId, items }, { dataSources, correlationId }) {
75 try {
76 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
77
78 if (!userId || !items || items.length === 0) {
79 throw new GraphQLError('Invalid order input', {
80 extensions: {
81 code: ErrorCodes.INVALID_INPUT,
82 correlationId
83 }
84 });
85 }
86
87 const order = await dataSources.orderAPI.createOrder({ userId, items });
88 console.log(`[${correlationId}] Order created: ${order.id}`);
89
90 return order;
91 } catch (error) {
92 console.error(`[${correlationId}] Error creating order:`, error.message);
93 throw new GraphQLError('Failed to create order', {
94 extensions: {
95 code: ErrorCodes.SERVICE_ERROR,
96 correlationId,
97 originalError: error.message
98 }
99 });
100 }
101 }
102 }
103};
104
105export { createShippingInfoLoader, createOrderLoader };
Source: graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js
§01

这一问在要求什么What this task asks for

TODO 原文:Implement orders resolver with proper error handling and correlation ID tracing

三个要求:

  1. 实现 —— 返回这个用户的订单列表。
  2. proper error handling —— 数据源出错时要抛结构化的 GraphQLError, 不能让原始异常裸奔到客户端。
  3. correlation ID tracing —— 日志和错误里都要带上 correlationId

schema 那边的约束:orders: [Order!]!双重非空 —— 绝不能返回 null。

The TODO, word for word: Implement orders resolver with proper error handling and correlation ID tracing.

Three requirements:

  1. Implement it — return this user’s list of orders.
  2. proper error handling — when the data source fails, throw a structured GraphQLError; never let a raw exception run loose to the client.
  3. correlation ID tracing — carry correlationId in both the logs and the errors.

The constraint from the schema: orders: [Order!]!, non-null twice over — never return null.

§02

这一问真正考什么What this task actually tests

  • 你知不知道 parent 是什么。user.id 来自__resolveReference 的返回值, 而它只返回了 { id }
  • 你会不会核对方法名。getOrdersByUserId, 不是 getOrders、不是 findByUser
  • 你读没读 schema 的可空性。[Order!]! 决定了必须 ?? []
  • 你会不会区分「业务错误」和「系统错误」。下面会讲为什么 catch 里要先判断instanceof GraphQLError
  • Whether you know what parent is. user.id comes from what __resolveReference returned, and that was only { id }.
  • Whether you check method names. It is getOrdersByUserId, not getOrders and not findByUser.
  • Whether you read the nullability in the schema. [Order!]! is what forces the ?? [].
  • Whether you tell a business error apart from a system error. The next section explains why a catch has to check instanceof GraphQLError first.
§03

先想再写Think before you write

先别写代码 · 先回答这几个问题Before you write code · answer these first
1.输入是什么?—— parent(user,上面只有 id)。args 是空的。
2.输出是什么?—— Order 数组。schema 说双重非空,所以最少是 []。
3.数据从哪来?—— context.dataSources.orderDataSource。
4.调哪个方法?—— getOrdersByUserId(userId),参数是字符串 id。
5.出错了怎么办?—— 抛 GraphQLError,extensions 里带 code 和 correlationId。
先别写代码 · 先回答这几个问题Before you write code · answer these first
1.What is the input? — parent, the user, which carries nothing but id. args is empty.
2.What is the output? — an array of Order. The schema says non-null twice over, so the floor is [].
3.Where does the data come from? — context.dataSources.orderDataSource.
4.Which method? — getOrdersByUserId(userId), and the argument is a string id.
5.What if it throws? — throw a GraphQLError with code and correlationId in extensions.
§04

分步实现Building it step by step

第一步:最小可用版本。先让那个红的测试变绿。

第二步:加兜底。getOrdersByUserIdfilter 实现, 找不到会返回 [] 而不是 undefined, 所以这里的 ?? [] 严格说是防御性的。但还是要写 —— schema 是双重非空,你不该依赖数据源的实现细节。 真实项目里数据源换个实现(比如换成 HTTP 调用)就可能返回 undefined。

第三步:加 try/catch 和日志。TODO 明确要求这两样。

第四步:处理「已经是 GraphQLError」的情况。这一步是最容易漏的,下一段专门讲。

Step one: the smallest version that works. Get that red test to green.

Step two: add the fallback. getOrdersByUserId is written with filter, so a miss gives you [] rather than undefined, which makes the ?? [] here strictly defensive. Write it anyway — the schema is non-null twice over and you should not lean on the data source’s implementation details. In a real project, swap that data source for an HTTP call and undefined becomes possible.

Step three: add try/catch and logging. The TODO asks for both in plain words.

Step four: handle the case where the error is already a GraphQLError. The easiest step to miss, and the next section is all about it.

JavaScript推导 · 第一步Working it out · step one示意Illustrative
1// 第一步
2async orders(user, _, { dataSources }) {
3 return dataSources.orderDataSource.getOrdersByUserId(user.id);
4}
1// Step one
2async orders(user, _, { dataSources }) {
3 return dataSources.orderDataSource.getOrdersByUserId(user.id);
4}
JavaScript推导 · 第二、三步Working it out · steps two and three示意Illustrative
1// 第二步 + 第三步
2async orders(user, _, { dataSources, correlationId }) {
3 try {
4 console.log(`[${correlationId}] Resolving User.orders for userId: ${user.id}`);
5 const orders = await dataSources.orderDataSource.getOrdersByUserId(user.id);
6 return orders ?? [];
7 } catch (error) {
8 console.error(`[${correlationId}] Error resolving User.orders:`, error.message);
9 throw new GraphQLError('Failed to fetch orders for user', {
10 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId, originalError: error.message }
11 });
12 }
13}
1// Step two plus step three
2async orders(user, _, { dataSources, correlationId }) {
3 try {
4 console.log(`[${correlationId}] Resolving User.orders for userId: ${user.id}`);
5 const orders = await dataSources.orderDataSource.getOrdersByUserId(user.id);
6 return orders ?? [];
7 } catch (error) {
8 console.error(`[${correlationId}] Error resolving User.orders:`, error.message);
9 throw new GraphQLError('Failed to fetch orders for user', {
10 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId, originalError: error.message }
11 });
12 }
13}
§05

catch 里为什么要先判断 instanceof GraphQLErrorWhy the catch block must check instanceof GraphQLError first

这是本门考试贯穿三处的一个模式,值得单独理解。This pattern shows up in three places in this exam, so it is worth learning on its own.

catch 会接住 try 块里任何抛出的东西 —— 包括你自己故意抛的那个结构化错误

想一个场景:Query.orders 里你先校验 「userId 不能为空」,不合法就抛GraphQLError(code: INVALID_INPUT)。 然后自己的 catch 接住它,重新包成code: SERVICE_ERROR

结果:客户端收到的是「服务器内部错误」, 而实际上是「你的输入不合法」。这是完全错误的信号 —— 客户端会重试(以为是临时故障), 而重试永远不会成功。

所以模式是:catch 的第一行先问「这个错误已经是 我精心构造过的了吗?」是就原样往上抛。

这个模式在项目里的三处都需要Query.order(抛 ORDER_NOT_FOUND)、Query.orders(抛 INVALID_INPUT)、Mutation.createOrder(抛 INVALID_INPUT)。最后那个就是埋雷 3 —— starter 代码漏了这一行,测试直接失败。

User.orders 里其实没有自己抛业务错误, 所以这一行是防御性的。但统一写法比 「哪里需要哪里写」更可靠,也更容易 review。

A catch catches anything thrown inside the try block — including the structured error you threw on purpose.

Picture this: inside Query.orders you first check that userId is not empty and throw GraphQLError(code: INVALID_INPUT) when it is. Then your own catch grabs that error and rewraps it as code: SERVICE_ERROR.

Result: the client is told “internal server error” when the truth is “your input was invalid”. That is the wrong signal entirely — the client retries, thinking it hit a temporary glitch, and the retry can never succeed.

So the pattern is: the first line of the catch asks “is this error one I carefully built myself?” and rethrows it untouched if it is.

Three places in this project need the pattern: Query.order (throws ORDER_NOT_FOUND), Query.orders (throws INVALID_INPUT) and Mutation.createOrder (throws INVALID_INPUT). That last one is planted bug 3 — the starter code is missing this line, and a test fails because of it.

User.orders never throws a business error of its own, so the line is purely defensive there. But writing it the same way everywhere is more reliable than “add it where it is needed”, and easier to review.

JavaScript一行之差One line apart示意Illustrative
1// ✗ 不判断:自己抛的 INVALID_INPUT 会被自己吞掉
2try {
3 if (!userId) throw new GraphQLError('userId is required', {
4 extensions: { code: 'INVALID_INPUT', correlationId }
5 });
6 ...
7} catch (error) {
8 throw new GraphQLError('Failed to fetch orders', {
9 extensions: { code: 'SERVICE_ERROR', correlationId } // ← 变成 SERVICE_ERROR 了
10 });
11}
12
13// ✓ 先放行已经结构化的错误
14} catch (error) {
15 if (error instanceof GraphQLError) throw error;
16 throw new GraphQLError('Failed to fetch orders', { ... });
17}
1// ✗ no check: the INVALID_INPUT you threw gets swallowed by your own catch
2try {
3 if (!userId) throw new GraphQLError('userId is required', {
4 extensions: { code: 'INVALID_INPUT', correlationId }
5 });
6 ...
7} catch (error) {
8 throw new GraphQLError('Failed to fetch orders', {
9 extensions: { code: 'SERVICE_ERROR', correlationId } // ← now it says SERVICE_ERROR
10 });
11}
12
13// ✓ let an already structured error through first
14} catch (error) {
15 if (error instanceof GraphQLError) throw error;
16 throw new GraphQLError('Failed to fetch orders', { ... });
17}
§06

完整答案The full answer

审计时实测:这样写之后两个相关测试通过。Measured during the audit: with this code the two related tests pass.

注意 _ 那个位置是 args ——User.orders 在 schema 里没有参数,所以用不到。

Note that the _ slot is where args goes — User.orders takes no arguments in the schema, so nothing there is needed.

JavaScriptUser.orders(参考答案,实测通过)User.orders (reference answer, measured to pass)已跑通Verified
1async orders(user, _, { dataSources, loaders, correlationId }) {
2 try {
3 console.log(`[${correlationId}] Resolving User.orders for userId: ${user.id}`);
4
5 const orders = await dataSources.orderDataSource.getOrdersByUserId(user.id);
6
7 // schema 说 [Order!]! -> 绝不返回 null
8 return orders ?? [];
9 } catch (error) {
10 if (error instanceof GraphQLError) throw error;
11
12 console.error(`[${correlationId}] Error resolving User.orders:`, error.message);
13 throw new GraphQLError('Failed to fetch orders for user', {
14 extensions: {
15 code: ErrorCodes.SERVICE_ERROR,
16 correlationId,
17 originalError: error.message
18 }
19 });
20 }
21}
1async orders(user, _, { dataSources, loaders, correlationId }) {
2 try {
3 console.log(`[${correlationId}] Resolving User.orders for userId: ${user.id}`);
4
5 const orders = await dataSources.orderDataSource.getOrdersByUserId(user.id);
6
7 // the schema says [Order!]! -> never return null
8 return orders ?? [];
9 } catch (error) {
10 if (error instanceof GraphQLError) throw error;
11
12 console.error(`[${correlationId}] Error resolving User.orders:`, error.message);
13 throw new GraphQLError('Failed to fetch orders for user', {
14 extensions: {
15 code: ErrorCodes.SERVICE_ERROR,
16 correlationId,
17 originalError: error.message
18 }
19 });
20 }
21}
§07

怎么验证How to check it

单元测试直接调 resolver 函数:

但单元测试绕过了 federation 链路。想验证「Router 那条路也通」,用 _entities 查询 —— 审计时实测输出如下,两个订单都拿到了:

The unit tests call the resolver function directly:

But a unit test bypasses the federation path. To check that the Router’s route works as well, use an _entities query — here is what the audit actually measured, with both orders coming back:

JavaScript__tests__/resolvers.test.js(两个相关测试)__tests__/resolvers.test.js (the two related tests)源项目From source
1describe('User.orders resolver', () => {
2 it('should return orders for a user', async () => {
3 const user = { id: '123' };
4 const orders = await resolvers.User.orders(user, {}, context);
5
6 expect(orders).toBeDefined();
7 expect(Array.isArray(orders)).toBe(true);
8 expect(orders.length).toBeGreaterThan(0);
9 expect(orders[0]).toHaveProperty('id');
10 expect(orders[0]).toHaveProperty('userId', '123');
11 });
12
13 it('should return empty array for user with no orders', async () => {
14 const user = { id: '999' };
15 const orders = await resolvers.User.orders(user, {}, context);
16 expect(orders.length).toBe(0);
17 });
18});
Source: graphql-federation-practice/node-subgraph/__tests__/resolvers.test.js
Textfederation 链路验证Checking the federation path已跑通Verified
1# 用 _entities 走一遍 federation 链路(审计实测输出)
2query($r:[_Any!]!){ _entities(representations:$r){ ... on User { id orders { id status } } } }
3variables: { "r": [{ "__typename": "User", "id": "123" }] }
4
5→ {"_entities":[{"id":"123","orders":[
6 {"id":"order-456","status":"SHIPPED"},
7 {"id":"order-457","status":"DELIVERED"}]}]}
8 errors: []
1# Walking the federation path with _entities (output measured in the audit)
2query($r:[_Any!]!){ _entities(representations:$r){ ... on User { id orders { id status } } } }
3variables: { "r": [{ "__typename": "User", "id": "123" }] }
4
5→ {"_entities":[{"id":"123","orders":[
6 {"id":"order-456","status":"SHIPPED"},
7 {"id":"order-457","status":"DELIVERED"}]}]}
8 errors: []
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L2填空Fill the blanks补全 User.ordersFill in User.orders

四个空。第 2 个是数据源上的真实方法名, 第 4 个是那行最容易漏的防御。

Four blanks. The second is the method name that really exists on the data source; the fourth is the guard people most often forget.

JSsrc/resolvers/orderResolvers.js4 个空4 blanks
1async orders(user, _, { dataSources, loaders, correlationId }) {
2 try {
3 console.log(`[${correlationId}] Resolving User.orders for userId: ${user.}`);
4
5 const orders = await dataSources.orderDataSource.(user.id);
6
7 return orders [];
8 } catch (error) {
9 if (error GraphQLError) throw error;
10
11 console.error(`[${correlationId}] Error:`, error.message);
12 throw new GraphQLError('Failed to fetch orders for user', {
13 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
14 });
15 }
16}
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
L3写整块Write a block不看答案,自己写出 User.ordersWrite User.orders yourself, without looking at the answer

按 TODO 的三条要求写完整实现。检查器会核对方法名、兜底、 错误处理和 correlation id。

Write the full implementation against the three requirements in the TODO. The checker looks at the method name, the fallback, the error handling and the correlation id.

要求Requirements
  • 用 user.id 去取该用户的订单Use user.id to fetch that user's orders
  • 调用数据源上真实存在的方法Call a method that really exists on the data source
  • 绝不返回 null 或 undefined(schema 是 [Order!]!)Never return null or undefined (the schema says [Order!]!)
  • 用 try/catch 包住,失败时抛 GraphQLErrorWrap it in try/catch and throw a GraphQLError on failure
  • 错误的 extensions 里带 code 和 correlationIdPut code and correlationId in the error's extensions
  • 已经是 GraphQLError 的错误要原样往上抛,不要重新包装Rethrow an error that is already a GraphQLError untouched, without rewrapping it
  • 日志里带上 correlationIdInclude correlationId in the log line
JavaScriptsrc/resolvers/orderResolvers.js
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

错例Wrong

初学者常见的几种写法错误Mistakes beginners actually make

下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.

JavaScript示意Illustrative
1// ✗ 用了 loader 而不是数据源
2async orders(user, _, { loaders }) {
3 return loaders.orderLoader.load(user.id);
4}
1// ✗ uses the loader instead of the data source
2async orders(user, _, { loaders }) {
3 return loaders.orderLoader.load(user.id);
4}
orderLoader 是按 order id取单个订单的,不是按 user id 取列表。 传 "123" 进去会去找id === "123" 的 order —— 找不到,返回 undefined。
loader 不是万能的,要看它 batch 函数里调的是什么。 这里该用 orderDataSource.getOrdersByUserId
orderLoader fetches one order by order id. It does not fetch a list by user id. Passing "123" makes it look for the order whose id === "123" — there is none, so it returns undefined.
A loader only does what its batch function does. Here you need orderDataSource.getOrdersByUserId.
JavaScript示意Illustrative
1// ✗ 忘了兜底
2async orders(user, _, { dataSources }) {
3 return await dataSources.orderDataSource.getOrdersByUserId(user.id);
4}
1// ✗ forgot the fallback
2async orders(user, _, { dataSources }) {
3 return await dataSources.orderDataSource.getOrdersByUserId(user.id);
4}
这个项目的数据源用 filter 实现, 找不到返回 [],所以恰好不会出问题
但你不该依赖这个实现细节 —— schema 是 [Order!]!, 而数据源随时可能换成 HTTP 调用(那时找不到可能返回undefinednull)。按 schema 的契约写,不按数据源的当前行为写。
The data source in this project uses filter, so a miss returns[]. By luck, nothing breaks.
But you should not rely on that detail. The schema says [Order!]!, and the data source could become an HTTP call at any time — then a miss might return undefined or null. Write to the contract in the schema, not to how the data source behaves today.
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

entity 上的字段 resolverA field resolver on an entity
数据来自 parent 里 @key 声明的那个字段The data comes from the field named in @key, read off parent
TODO 说 proper error handlingA TODO says proper error handling
try/catch + GraphQLError + extensions.codetry/catch plus GraphQLError plus extensions.code
TODO 说 correlation ID tracingA TODO says correlation ID tracing
日志和 error extensions 都带上它Put it in the log line and in the error extensions
字段是 [T!]!The field is [T!]!
?? [] 兜底,按 schema 契约而非数据源行为Fall back with ?? []; follow the schema contract, not the data source behaviour
catch 里要重新包装错误The catch block wraps errors into a new one
先 if (error instanceof GraphQLError) throw errorStart with if (error instanceof GraphQLError) throw error
这节的要点What to take away
  1. user.id 来自 __resolveReference 的返回值,parent 上只有这一个属性。user.id comes from what __resolveReference returned; it is the only property on parent.
  2. 方法名是 getOrdersByUserId —— 去数据源核对,别凭直觉。The method name is getOrdersByUserId. Check it against the data source instead of guessing.
  3. [Order!]! 决定必须 ?? [] 兜底,按 schema 契约写而不是按数据源当前行为。[Order!]! means you must fall back with ?? []. Write to the schema contract, not to how the data source behaves today.
  4. catch 第一行先放行已结构化的 GraphQLError,否则会把业务错误降级成系统错误。The first line of catch must let an existing GraphQLError through, otherwise a business error is turned into a system error.
  5. 单元测试直接调 resolver;想验 federation 链路要用 _entities 查询。Unit tests call the resolver directly. To check the Federation path, use the _entities query.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lessonTODO 2 · Order.shippingInfoTODO 2 · Order.shippingInfo
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 先读题:四个 TODO、三处埋雷、十个测试Read the task first: four TODOs, three planted bugs, ten tests