> 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).

# 编写测试

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

测试 MUST 使用 **describe** 和 **it** 方法来编写，jest 提供的方法

## 描述并构建上下文

该 **describe** 方法 SHOULD 用于描述，只要可能，就描述将要测试的代码片段执行时所在的上下文，并 SHOULD 在其作用域内构建该上下文。

```tsx
describe('when the flag is red', () => {
	...
})

describe('when the flag is blue', () => {
	...
})
```

为了描述上下文，我们 SHOULD 使用以下词语之一： **when** 用于顶层 describe，以及 **having** 或 **和** 来表示存在一个依赖于之前已定义上下文的上下文。其背后的理由是创建自描述的上下文，并且可以通过组合 describe 语句轻松理解和检查。由于这个想法是将上下文描述连接起来， **describes** MUST 使用小写。

```tsx
describe('when the flag is red', () => {
	...
	describe('and the wind is strong', () => {
     // 这里的上下文由这些 describes 的合取来描述：
     // "when the red flag is red and the wind is strong"
     ...
  })
})
```

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

```tsx
describe('when the flag is red', () => {
	...
	describe('and the wind is strong', () => {
     ...
  })

	describe('and the wind is weak', () => {
     ...
  })
})
```

如前所述，每个 describe SHOULD 搭配一个构建上下文的方法，或者一个销毁或更改上下文的方法。必须使用的方法是 **beforeEach** 和 **afterEach**.

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

使用 **beforeEach** 方法创建的上下文，会随着我们深入 describe 树而变得越来越具体。每个 **beforeEach** 都会根据其文本所说的内容，更加明确地指定上下文。

### 变量作用域

该 **变量** 在上下文中设置的变量 SHOULD 按照它们将被使用的作用域来定义。也就是说，如果某个变量只在嵌套 describe 的特定层级上下文中使用，那么该变量 SHOULD 只在那个特定上下文中定义。其背后的理由是让变量尽量靠近它们被使用的代码，从而使代码更容易理解。

```tsx
let flag = 'blue'
describe('when the flag is red', () => {
	let wind = 'mild'
	beforeEach(() => {
    flag = 'red'
  })
  ...

	describe('and the wind is strong', () => {
		beforeEach(() => {
	    wind = 'strong'
	  })
    ...
  })

	describe('and the wind is weak', () => {
		beforeEach(() => {
	    wind = 'weak'
	  })
    ...
  })
})
```

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

以下类型的变量 **原始类型** 在其定义的上下文或后续上下文中会被更改的，MUST 定义为 **let**，因为 Typescript 的特性如此规定。

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

```tsx
let flag
describe('when the flag is red', () => {
  // 一个原始值，将在多个作用域中被修改。
	let wind
  // 一个对象值，将在测试代码中使用。
  let someObject
	beforeEach(() => {
    flag = 'red'
		someObject = { id: 1 }
  })
  ...

	describe('and the wind is strong', () => {
		// 一个常量原始值，只会在这个作用域中使用。
		const aConstantPrimitiveValue = 'something'
		beforeEach(() => {
	    wind = 'strong'
	  })
    ...
  })

	describe('and the wind is weak', () => {
		beforeEach(() => {
	    wind = 'weak'
	  })
    ...
  })
})
...
```

变量 SHOULD 在可能的情况下被正确地加上类型。重复的类型 SHOULD 抽象为一个类型。

```tsx
let flag: string
describe('when the flag is red', () => {
  // 一个原始值，将在多个作用域中被修改。
	let wind: string
  // 一个对象值，将在测试代码中使用。
  let someObject: { id: string }
	beforeEach(() => {
    flag = 'red'
		someObject = { id: 1 }
  })
  ...

	describe('and the wind is strong', () => {
		// 一个常量原始值，只会在这个作用域中使用。
		const aConstantPrimitiveValue = 'something'
		beforeEach(() => {
	    wind = 'strong'
	  })
    ...
  })

	describe('and the wind is weak', () => {
		beforeEach(() => {
	    wind = 'weak'
	  })
    ...
  })
})
...
```

## 描述期望并执行代码

该 **it** 方法 MUST 始终放在一个 **describe** 的作用域内，并且 MUST 用于描述我们对将要测试的代码的期望是什么，并 SHOULD 尽可能只包含 **一个** 断言。每个 **it** 中的多个断言是可存在的，如果存在性能问题，因为测试会针对每个 **it** 运行一次（由于 **beforeEach**），或者如果期望内容可以在期望描述中清晰地描述出来。

```tsx
let flag: string
describe('when the flag is red', () => {
	let wind: string
  let someObject: { id: string }
	beforeEach(() => {
    flag = 'red'
		someObject = { id: 1, swim: jest.fn() }
  })
  ...

  // 一个上下文中测试执行的单一期望（一个 it）
	describe('and the wind is strong', () => {
		const aConstantPrimitiveValue = 'something'
		beforeEach(() => {
	    wind = 'strong'
	  })
    
		it('should not go swimming', () => {
      expect(goSwimming(flag, wind, someObject)).toBe(false)
    })
  })

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

		it('should go swimming', () => {
      expect(goSwimming(flag, wind, someObject)).toBe(true)
    })

    it('should have called the swim method', () => {
      expect(someObject.swim).toHaveBeenCalled()
    })
  })
})
...
```

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

遵循 **describe** 描述一路深入到 **it**，开发者可以通过连接这些句子轻松理解正在测试什么。

```tsx
// 这可以理解为："当旗帜是红色且风很强时， 
// 它应该去游泳"
describe('when the flag is red', () => {
  ...
  describe('and the wind is strong', () => {
		...
		it('should go swimming', () => {
			...
		})
  })
})
```

### 编写清晰的期望

在 **it**中编写的期望描述 MUST 尽可能具有描述性，以说明对测试执行的结果有什么期望。开发者 MUST NOT 使用抽象或笼统的措辞来定义期望，因为这会削弱对代码期望内容的清晰度。

{% hint style="danger" %}
**开发者 MUST NOT 使用如下措辞：**

* “should work as expected” ⇒ 哪些内容应该如预期工作？
* “should return the correct value” ⇒ 什么是正确值？
* “should resolve/return/work correctly” ⇒ 某些东西如何才算正确工作？
* “should fail” ⇒ 它应该如何失败？它应该提供什么消息？
  {% endhint %}

必须考虑到，当在 **it**中指定期望内容，或者在 **describe**中指定上下文时，开发者 MAY 使用函数名或精确的错误消息，如果这对于更好地理解意图是必要的，但在可能的情况下，他们 SHOULD 使用文本形式的描述，以便让测试更易于维护。

```tsx
// math.ts
export function div(a: number, b: number): number {
	if(b === 0)	{
		throw new Error('The divisor b equals 0, the division can\'t be performed')
	}
}

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

describe('when dividing by zero', () => {
  // 这个 it 的描述体现的是异常消息的意图
	it('should throw an exception signaling that a division by 0 is not possible', () => {
		expect(() => div(12, 0)).toThrowError('The divisor b equals 0, the division can\'t be performed')
	})
})
```

## 测试内容

要测试什么会因正在执行的代码而异。不同因素，从性能到庞大的输入域，都会显著改变哪些内容应该或不应该在测试中被测试。在这里，我们将展示一组开发者 SHOULD 在可能情况下测试的单独案例。

```tsx
function run(kilometers: number): number {
	if(kilometers > 1000) {
    throw new Error('The runner can\'t run more than 1000 kilometers')
  }

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

这里的 run 函数有几条不同的执行流程。函数，或者代码段，SHOULD NOT 只根据不同的可能执行流程来测试，而应该根据对该函数的期望来测试，因为该函数可能并没有按照它被编写的目的在工作，而测试可能与代码过于紧耦合，无法发现其中不同的问题。这意味着开发者 SHOULD 始终测试所有可能的执行路径，并 SHOULD 按照函数语义去测试其他可能的路径。

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

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

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

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

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

```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('when running more than 1000 kilometers', () => {
	it('should throw an error signaling that the runner can\'t run more than 1000 kilometers', () => {
		expect(() => run(1001)).toThrowError("The runner can't run more than 1000 kilometers")
	})
})

describe('when running less or equal than 10', () => {
	it('should return the same amount of kilometers as the ones given', () => {
		expect(run(2)).toEqual(2)
	})
})

describe('when running more than 10 kilometers but less than 1000', () => {
	const kilometers = 50
	let result: number
	beforeEach(() => {
		mockDoSomething.mockReturnValueOnce(undefined)
		mockBeLazy.mockImplementationOnce(value => value)
		result = run(kilometers)
	})
	
	it('should return the kilometers after being lazy', () => {
		expect(result).toEqual(kilometers)
	})

	it('should call the doSomething function with the given kilometers', () => {
		expect(mockDoSomething).toHaveBeenCalledWith(kilometers)
	})
})
```

## 何时 mock

是否进行 mock 很大程度上取决于开发者正在编写的测试类型。

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

所有 mock MUST 使用 Jest 提供的工具完成，但 `redux-saga-test-plan` mock 除外。

### Mock 和工具函数

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


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
