> 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/testing-services-wkc.md).

# 测试服务（WKC）

知名组件服务是使用 [知名组件](https://well-known-components.github.io/documentation/) 架构，其中服务的每一部分都被模块化并封装在一个组件中。这些组件除了易于互换之外，还易于进行单元测试，因为它们的所有依赖项都会在创建时注入。

对这些服务进行测试应使用两种不同类型的测试， **单元** 和 **集成**。包含逻辑的组件必须使用单元测试进行测试，而主要与外部服务交互的组件应使用集成测试进行测试。也就是说，一个不包含任何逻辑且主要包含数据库查询的数据库适配器组件必须通过集成方式测试。

## 知名组件组件的单元测试

所有组件都必须进行单元测试，并且必须按照此处定义的测试编写部分来编写。

为了展示这些组件如何被测试，这里有一个只有一个方法和两个依赖项的简单组件， **设置** 和 **好友**:

```tsx
// 语音组件

export function createVoiceComponent(dependencies: Pick<AppComponents, 'settings' | 'friends'>) {
	const { settings, friends } = dependencies

  async function canCallEachOther(caller: string, callee: string): Promise<boolean> {
	  const [callerSettings, calleeSetting] = Promise.all([settings.getPrivacySetting(caller), settings.getPrivacySetting(callee)])
	  const areFriends = await friends.areUsersFriends(caller, callee)
		return areFriends || (callerSettings !== Settings.ONLY_FRIENDS && calleeSettings !== Settings.ONLY_FRIENDS)
  }
  
  return {
	  createVoiceChat
  }
}
```

为了测试这些组件，必须构建合适的 mock，也就是那些具有正确类型、能够与被测组件交互并且足够灵活以便我们创建各种测试的 mock。这些 mock 在不同测试之间也应该保持不可变，从而保留每个测试都在其自身上下文中执行、彼此隔离的理念。

这些 mock 应使用组件 mock 创建函数创建，并且应命名为 create**ComponentName**MockedComponent。以上面创建的组件为例，下面是它们应该如何呈现的示例：

```tsx
// 放在 /test/mocks/settings.ts 文件中
export function createSettingsMockedComponent(overrides?: Partial<jest.Mocked<ISettingsComponent>>): jest.Mocked<ISettingsComponent> {
  return {
    getPrivacySettings: overrides?.getPrivacySettings ?? jest.fn()
  }
}

// 放在 test/mocks/friends.ts 文件中
export function createFriendsMockedComponent(overrides?: Partial<jest.Mocked<IFriendsComponent>>): jest.Mocked<IFriendsComponent> {
	return {
	  // 我们可以灵活地定义该组件导出的任意方法的行为。
	  areUsersFriends: overrides?.areUsersFriends ?? jest.fn(),
	  // 未使用的方法可以在 overrides 参数中省略，从而使它们成为简单的函数 mock。
	  hasBlockedUser: overrides?.hasBlockedUser ?? jest.fn()
	}
}
```

通过将这些 mock 小心地放在一个 `beforeEach`中，我们可以确保这些 mock 在不同执行之间被正确隔离。下面是这些 mock 的使用示例：

```tsx
// 定义上下文中的 mock 函数，这样我们就可以修改它的 mock。
let getPrivacySettingsMock: jest.MockedFn<ISettingsComponent['getPrivacySettings']>
let areUsersFriendsMock: jest.MockedFn<IFriendsComponent['areUsersFriends']>
let voice: IVoiceComponent

beforeEach({
  getPrivacySettingsMock = jest.fn()
  areUsersFriendsMock = jest.fn()
  // 构建 mock
  const settings = createSettingsMockedComponent({ getPrivacySettings: getPrivacySettingsMock })
  // 仅使用测试中将会用到的方法来初始化组件 mock
  const friends = createFriendsMockedComponent({ areUsersFriends: areUsersFriendsMock })
  // 使用这些 mock 创建被测试的组件
  voice = createVoiceComponent({ settings, friends })
})
```

这样定义 mock 的方式，不仅允许我们在不同执行之间正确隔离测试上下文，还为正确初始化上下文奠定了基础，因为 mock 函数已经定义好了，因此很容易构建每个上下文：

```tsx
// 定义在前一个代码块之后。

describe('当检查两个用户是否可以互相呼叫时', () => {
	const calleeAddress = '0xd7D746d39D142b6bE752efd7626cE28F245a25D1'
	const callerAddress = '0x2e8b4De1230f827082202aa53d489A26163aace0'

  describe('且被叫方只接受来自好友的呼叫时', () => {
    beforeEach(() => {
      // 为该上下文定义 mock，使被叫方 
      getPrivacySettingsMock.mockImplementation((address: string) => {
	      switch(address) {
	        case caleeAddress:
		        return Settings.ONLY_FRIENDS
		      case callerAddress:
			      return Settings.ALL
			    default:
				    throw new Error("错误的 mock")
	      }
      })
    })
    
    describe('且被叫方和主叫方是好友时', () => {
	    beforeEach(() => {
		    areUsersFriendsMock.mockResolvedValueOnce(true)
	    })
	    
	    it('应解析为 true', () => {
		    return expect(voice.canCallEachOther(callerAddress, calleeAddress)).resolves.toBe(true)
	    })
    })
    
    describe('且被叫方和主叫方不是好友时', () => {
		  beforeEach(() => {
		    areUsersFriendsMock.mockResolvedValueOnce(false)
	    })
	    
	    it('应解析为 false', () => {
		    return expect(voice.canCallEachOther(callerAddress, calleeAddress)).resolves.toBe(false)
	    })
    })
  })
  
  //……其他上下文
})
```

## 集成测试

所有端点、WS RPC 调用、作业或任务都必须进行完整集成测试，并且必须按照此处定义的测试编写部分来编写。集成测试必须放在 `test/integration` 目录中，并且必须根据我们要测试的入口点来命名。

集成测试应仅限于测试那些无法通过单元测试验证的特定集成条件（SQL 查询、Redis 操作等），以及我们逻辑组件与协议（HTTP、WS）之间的集成。

为了举例说明集成测试应如何编写，我们将测试一个用于获取用户好友的简单 HTTP 请求。

```tsx
export async function getFriendHandler(
  context: Pick<
    HandlerContextWithPath<'logs' | 'communities', '/v1/users/:user/friends'>,
    'url' | 'components' | 'params' | 'verification'
  >
): Promise<HTTPResponse<AggregatedCommunityWithMemberData>> {
  const {
    components: { friends },
    params: { id },
    verification
  } = context

  try {
    const userAddress = verification!.auth.toLowerCase()

    return {
      status: 200,
      body: {
        data: await friends.getFriends(userAddress)
      }
    }
  } catch (error) {
  
	  if (error instanceof InvalidFriendshipRequest) {
			throw InvalidRequest(isErrorWithMessage(error) ? error.message : '未知错误')
	  }

    return {
      status: 500,
      body: {
        消息
      }
    }
  }
}
```

以及一个具有以下内容的 friends 组件：

```tsx
async function getFriends(addres: string) {
	friendsDb.getFriends()
}
```


---

# 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/testing-services-wkc.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.
