> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/contributor/contributor-zh/gong-xian-zhe-zhi-nan/testing-standards/writing-tests.md).

# 编写测试

测试可以用多种方式编写。本节试图给出一种标准，以及我们编写测试方式背后的理由。

测试必须使用 **describe** 以及 **它** jest 提供的方法

## 描述和构建上下文

该 **describe** describe 方法在可能的情况下应被用来描述待测试代码将被执行的上下文，并且应在其作用域内构建该上下文。

```tsx
describe('当标志为红色时', () => {
	...
})

describe('当标志为蓝色时', () => {
	...
})
```

为了描述上下文，我们应使用以下词语之一： **当……时** 用于顶层 describe，以及 **具有** 或 **和** 用于表示存在一个依赖于之前定义的其他上下文的上下文。这样做的理由是，通过组合 describe 语句来获得自描述的上下文，使其能够被轻松理解和检查。由于我们的想法是将上下文的描述连接起来， **describe 语句** 必须使用小写。

```tsx
describe('当标志为红色时', () => {
	...
	describe('并且风很强时', () => {
     // 上下文这里通过这些 describe 的合取来描述：
     // “当红旗为红色并且风很强时”
     ...
  })
})
```

多个 describe 可以嵌套在同一层级和更低层级。同一层级的 describe 表示不同的上下文，而更低层级中的 describe（嵌套 describe）表示更具体的上下文。

```tsx
describe('当标志为红色时', () => {
	...
	describe('并且风很强时', () => {
     ...
  })

	describe('并且风很弱时', () => {
     ...
  })
})
```

如前所述，每个 describe 都应配有一个构建上下文的方法，或一个销毁/更改上下文的方法。必须使用的方法是 **beforeEach** 以及 **afterEach**.

{% hint style="warning" %}
Jest 框架有两个方法， **beforeEach** 和 **beforeAll**，但二者同时使用在存在嵌套上下文时会带来混淆和执行问题，因为 [它们的执行顺序](https://jestjs.io/docs/setup-teardown#scoping) 并不是我们所期望的。
{% endhint %}

使用 **beforeEach** 方法创建的上下文会随着我们在 describe 树中向更深层推进而变得更加明确。每个 **beforeEach** 都会根据其文本所述更具体地说明上下文。

### 变量作用域

该 **变量** 在上下文中设置的变量应根据它们将被使用的作用域来定义。也就是说，如果某个变量只在嵌套 describe 的特定上下文中使用，那么该变量应只在该特定上下文中定义。这样做的理由是让变量尽可能贴近使用它们的代码，从而使代码更易于理解。

```tsx
let flag = 'blue'
describe('当标志为红色时', () => {
	let wind = 'mild'
	beforeEach(() => {
    flag = 'red'
  })
  ...

	describe('并且风很强时', () => {
		beforeEach(() => {
	    wind = 'strong'
	  })
    ...
  })

	describe('并且风很弱时', () => {
		beforeEach(() => {
	    wind = 'weak'
	  })
    ...
  })
})
```

……类型的变量 **原始类型**，其 **不会被更改** 在其定义的上下文或后续上下文中应定义为 **const** 一次，而不是在 **beforeEach** 作用域内重复定义。

……类型的变量 **原始类型** 在其定义的上下文或后续上下文中会被更改的必须定义为 **let**，因为 TypeScript 的特性要求如此。

……类型的变量 **非原始类型** （对象、数组等）必须定义为 **let** 且它们的值必须在 **beforeEach 中设置。** 这样做的理由是，JS 中的对象是可变的，这意味着任何使用该对象的代码执行都可能发生变化，并以非自愿的方式影响其他测试。

```tsx
let flag
describe('当标志为红色时', () => {
  // 一个会在多个作用域中被修改的原始值。
	let wind
  // 一个将在测试代码中使用的对象值。
  let someObject
	beforeEach(() => {
    flag = 'red'
		someObject = { id: 1 }
  })
  ...

	describe('并且风很强时', () => {
		// 一个只会在此作用域中使用的常量原始值。
		const aConstantPrimitiveValue = 'something'
		beforeEach(() => {
	    wind = 'strong'
	  })
    ...
  })

	describe('并且风很弱时', () => {
		beforeEach(() => {
	    wind = 'weak'
	  })
    ...
  })
})
...
```

变量应尽可能正确地进行类型标注。重复的类型应抽象为一个类型。

```tsx
let flag: string
describe('当标志为红色时', () => {
  // 一个会在多个作用域中被修改的原始值。
	let wind: string
  // 一个将在测试代码中使用的对象值。
  let someObject: { id: string }
	beforeEach(() => {
    flag = 'red'
		someObject = { id: 1 }
  })
  ...

	describe('并且风很强时', () => {
		// 一个只会在此作用域中使用的常量原始值。
		const aConstantPrimitiveValue = 'something'
		beforeEach(() => {
	    wind = 'strong'
	  })
    ...
  })

	describe('并且风很弱时', () => {
		beforeEach(() => {
	    wind = 'weak'
	  })
    ...
  })
})
...
```

## 描述期望与执行代码

该 **它** 方法必须始终放在 **describe** 并且必须用于描述我们对将要测试的代码的期望，而且如果可能，应只包含 **一个** 断言。每个 **它** 中包含多个断言 **它** 如果存在性能问题，则可以这样做，因为测试将针对每个 **beforeEach**（因为

```tsx
let flag: string
describe('当标志为红色时', () => {
	let wind: string
  let someObject: { id: string }
	beforeEach(() => {
    flag = 'red'
		），或者如果期望内容能够在期望描述中清楚地表达。
  })
  ...

  // 一个上下文中测试执行的单个期望（一个 it）
	describe('并且风很强时', () => {
		const aConstantPrimitiveValue = 'something'
		beforeEach(() => {
	    wind = 'strong'
	  })
    
		it('不应该去游泳', () => {
      expect(goSwimming(flag, wind, someObject)).toBe(false)
    })
  })

  // 一个上下文中测试执行的多个期望（两个 it）
	describe('并且风很弱时', () => {
    let result: boolean
		beforeEach(() => {
	    wind = 'weak'
      result = goSwimming(flag, wind, someObject)
	  })

		it('应该去游泳', () => {
      expect(goSwimming(flag, wind, someObject)).toBe(true)
    })

    it('应该已调用 swim 方法', () => {
      expect(someObject.swim).toHaveBeenCalled()
    })
  })
})
...
```

这种结构背后的理由是，为测试审阅者以及将来维护和修改代码的开发者提供清晰性，因为每个上下文和期望都被清楚地列出，从而更容易理解正在测试什么、如何测试，以及还剩下什么要测试。

沿着 **describe** 描述一直到 **它**，开发者就可以通过连接这些句子轻松理解正在测试什么。

```tsx
// 可读作：“当标志为红色且风很强时， 
// 它应该去游泳”
describe('当标志为红色时', () => {
  ...
  describe('并且风很强时', () => {
		...
		it('应该去游泳', () => {
			...
		})
  })
})
```

### 编写清晰的期望

写在 **它**中的描述必须尽可能详细地说明对测试执行的期望。开发者绝不能使用抽象或笼统的措辞来定义期望，因为这会削弱对代码期望内容的清晰性。

{% hint style="danger" %}
**开发者绝不能使用如下短语：**

* “应该按预期工作” ⇒ 什么叫按预期工作？
* “应该返回正确的值” ⇒ 什么是正确的值？
* “应该正确 resolve/返回/工作” ⇒ 何谓正确地工作？
* “应该失败” ⇒ 它应该如何失败？应提供什么消息？
  {% endhint %}

需要考虑到，在指定 **它**中的期望，或者上下文会是什么时 **describe**中，开发者可以在有助于更好理解意图的情况下使用函数名或精确的错误消息，但在可能时应使用文本表示，以便让测试更易维护。

```tsx
// math.ts
export function div(a: number, b: number): number {
	if(b === 0)	{
		throw new Error('除数 b 等于 0，无法执行除法')
	}
}

// test.spec.ts
import { div } from './math.ts'

describe('当除以零时', () => {
  // it 的描述说明了异常消息的意图
	it('应该抛出一个异常，表明无法进行除以 0 的运算', () => {
		expect(() => div(12, 0)).toThrowError('除数 b 等于 0，无法执行除法')
	})
})
```

## 测试什么

选择测试什么会因正在执行的代码而异。不同因素，从性能到大量输入范围，都可能显著改变测试中应该测试或不应该测试什么。这里我们将给出一组单独的用例，开发者应尽可能测试这些用例。

```tsx
function run(kilometers: number): number {
	if(kilometers > 1000) {
    throw new Error('跑者不能跑超过 1000 公里')
  }

  if(kilometers <= 10) {
		return kilometers
  } else if(kilometers > 10) {
		doSomething(kilometers)
    return beLazy(kilometers)
  }
}
```

这里的 run 函数有几个不同的执行流程。函数或代码段不应仅根据不同的可能执行流程来测试，而应根据函数的期望来测试，因为函数可能并没有按照其编写目的在工作，而测试也可能与代码过于耦合，从而无法捕捉其中的不同问题。这意味着开发者应始终测试所有可能的执行路径，并且也应根据函数语义测试其他可能路径。

对于这个特定情况，开发者应测试 **至少** 以下情况：

1. 以大于 1000 公里的距离运行 run 函数
2. 以等于 10 公里的距离运行 run 函数
3. 以大于 10 公里但小于 1000 公里的距离运行 run 函数

对于第一种情况，开发者需要测试 run 函数会抛出异常。 **必须检查异常的错误消息**，因为也可能抛出其他异常，这会使测试目的失效。如果该异常是自定义异常，开发者可以检查该异常的实例。

对于第二种情况，开发者需要测试该函数返回了与传入相同的公里数。

对于第三种情况，开发者需要测试该函数返回了外部函数 **beLazy** 返回的内容，并且由于 **doSomething** （也在该方法中执行）无法通过函数的返回值来检查，开发者应 **测试它是否以正确的参数被调用**.

```tsx
import { doSomething, beLazy } from '../runningUtils'
jest.mock('../runningUtils')

// 注意，这里没有主 describe
// 唯一的全局上下文是我们想用 jest 进行 mock 的函数

const mockDoSomething = doSomething as jest.MockedFunction<typeof doSomething>
const mockBeLazy = beLazy as jest.MockedFunction<typeof beLazy>

describe('当运行超过 1000 公里时', () => {
	it('应该抛出一个错误，表明跑者不能跑超过 1000 公里', () => {
		expect(() => run(1001)).toThrowError("跑者不能跑超过 1000 公里")
	})
})

describe('当运行距离小于或等于 10 公里时', () => {
	it('应该返回与输入相同的公里数', () => {
		expect(run(2)).toEqual(2)
	})
})

describe('当运行距离大于 10 公里但小于 1000 公里时', () => {
	const kilometers = 50
	let result: number
	beforeEach(() => {
		mockDoSomething.mockReturnValueOnce(undefined)
		mockBeLazy.mockImplementationOnce(value => value)
		result = run(kilometers)
	})
	
	it('应该返回懒处理后的公里数', () => {
		expect(result).toEqual(kilometers)
	})

	it('应该以给定的公里数调用 doSomething 函数', () => {
		expect(mockDoSomething).toHaveBeenCalledWith(kilometers)
	})
})
```

## 何时进行 mock

是否进行 mock 主要取决于开发者正在编写的测试类型。

* 单元测试应将所有外部函数都 mock，简单功能函数除外，例如字符串格式化等。
* API 测试应只对与外部服务通信的部分进行 mock，也就是数据库或其他 API。如果测试中执行的某个操作影响了测试套件的性能，可以通过实现 mock 来缓解这个问题。

所有 mock 都必须使用 Jest 提供的工具完成，但以下情况除外： `redux-saga-test-plan` mock。

### Mock 和辅助函数

* 任何 Jest mock 都应使用其 **一次性** 方法，如果可能的话，也就是 `mockReturnValueOnce` 或 `mockResolvedValueOnce` 不推荐 `mockReturnValue` 或 `mockResolvedValue`。这样做的理由是防止不希望执行的 mock 被调用，从而改变我们的测试执行。
* Jest 提供了许多可用于不同类型 mock 的类型。例如，在 mock 一个对象时，你应使用 `jest.Mocked<typeof someObject>`；对于类，你应使用 `jest.MockedClass<typeof SomeClass>`；对于函数， `jest.MockedFunction<typeof someFunction>`.
* 一个 `afterEach` 应编写为在每次测试后运行的 `jest.resetAllMocks` ，以清除任何全局 mock 模块的 mock 实现，这些实现可能会泄漏到其他测试中。如果你使用 `mockReturnValueOnce` 或 `mockResolvedValueOnce` ，则可以忽略这个重置，因为它会自动为你完成。
* 名为 `mocks` 的目录可以创建来存放 mock，这些 mock 应放在 `test` 或 `spec` 目录中。大型 mock 必须存放在与测试不同的文件中，以避免测试文件过于冗长。小型 mock，如果数量不多，应保留在测试中。
  * Mock 文件应以其所 mock 的实体命名。例如，如果我们在 mock 一个 profile，那么该 mock 应放在 `/test/mocks/profile.ts` 文件中。
  * 如果需要多个大型 mock，它们应放在一个以待 mock 实体命名的目录中的不同文件里。例如，如果我们有两个 profile mock，它们会存储为： `/test/mocks/profile/profile-with-wearables.ts` 和 `/test/mocks/profile/profile-without-wearables.ts`。为了便于访问它们，你应创建一个 `index.ts` 文件放在 `/test/mocks/profile/` 目录中并将它们导出。
  * 为了避免对被 mock 对象的变更，mock **必须以函数形式导出** ，并由函数返回它们。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.decentraland.org/contributor/contributor-zh/gong-xian-zhe-zhi-nan/testing-standards/writing-tests.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
