> 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-ko/contributor-guides/testing-standards/testing-uis.md).

# UI 테스트

이 섹션에서는 개발자들이 UI와 관련된 코드베이스를 어떻게 테스트할지 설명합니다.

## 테스트 스택

모든 UI 테스트는 반드시 다음을 사용해 수행해야 합니다. [Jest](https://jestjs.io/) 을 주 테스트 프레임워크로 사용하고 [redux-saga-test-plan](https://github.com/jfairbank/redux-saga-test-plan) 사가 테스트 도구로 사용하고 [react testing library](https://testing-library.com/docs/react-testing-library/intro/) 를 UI 컴포넌트 테스트 도구로 사용합니다. 테스트 코드는 반드시 다음을 사용해 작성해야 하며 [TypeScript](https://www.typescriptlang.org/) 를 사용해 실행되어야 합니다. [ts-jest](https://github.com/kulshekhar/ts-jest) 를 사용하여 타입 검사 지원을 받아야 합니다.

## 무엇을 테스트할 것인가

* UI 컴포넌트
* 사가
* 리듀서
* 셀렉터
* 액션 크리에이터
* 유틸리티 함수

## 사가 테스트

사는 redux 액션을 처리하는 제너레이터를 생성할 수 있게 해줌으로써 사이드 이펙트를 다룰 수 있게 해줍니다. 사가의 주요 장점 중 하나는 테스트 가능성인데, 사가에서 널리 사용되는 effect들은 테스트하기 매우 쉽기 때문입니다.

이 핸들러들은 반드시 다음을 사용해 테스트해야 합니다. [redux-saga-test-plan](https://github.com/jfairbank/redux-saga-test-plan)그리고 테스트는 모듈의 전체 핸들러(메인 핸들러 또는 사가)부터 우리가 디스패치할 액션에 반응하는 모든 핸들러의 실행까지, 전체 실행 흐름을 검증해야 합니다. 이 접근 방식은 사가 테스트를 쉽게 만들고 유지보수하기 쉽게 해주며, 가능한 한 전체 흐름을 최대한 거치기 때문에 테스트의 가치를 높여줍니다.

핸들러 테스트는 effect와의 상호작용을 통해 테스트해야 하며, 주로 다음을 사용해야 합니다. `put` 입니다. 그 목적은 액션을 처리하고 새로운 액션을 만들어 내는 것이기 때문입니다. 대부분의 테스트에는 분명 다음이 포함될 것입니다. `provide` effect를 mock하는 데 사용되는 call, 하나 또는 여러 개의 `put` calls를 사용하여 핸들러가 예상대로 액션을 디스패치하는지 검증하고, 단일 `dispatch` 뒤에 `run` 호출을 이어서 사가가 완료될 때까지 전체를 실행합니다.

테스트 중인 모듈 외부의 리소스 mocking은 redux-saga-test-plan providers를 사용해 수행해야 하며, redux-sagas가 제공하는 다양한 effect의 이점을 활용해야 합니다. call, apply 또는 그 밖의 effect는 테스트에서 함수가 호출될 때 가능한 경우마다 사용하여 쉽게 mock할 수 있게 해야 합니다. Jest는 모듈이나 함수를 mock하는 데 사용해서는 안 됩니다.

describe 안의 텍스트와 그 다음 내용은 반드시 다음 지침을 따라야 합니다:

* 사가 또는 메인 핸들러의 주요 describe는 다음으로 시작해야 합니다. *when handling*을 사용하여, 테스트 대상이 핸들러임을 나타내야 합니다. 다음의 *when handling* 문구 뒤에는 처리할 액션(또는 그 액션의 의도)에 대한 설명을 작성해야 합니다. 예를 들면 *성공적인 가져오기를 알리는 액션을 처리할 때.*
* 내부 describe는 실행 컨텍스트를 구분하며, 다음의 [컨텍스트 설명 및 구성](/contributor/contributor-ko/contributor-guides/testing-standards/writing-tests.md#describing-and-building-context) 섹션에서 설명한 것과 동일한 규칙을 따릅니다.
* 이 **테스트**describe는 테스트에서 기대되는 내용을 설명해야 하며, 다음으로 시작해야 합니다. *should put* 그리고 핸들러를 실행할 때 put 되어야 하는 액션들의 설명.

### 사가 예시

```tsx
...
export function* tiersSaga(builder: BuilderAPI) {
	function* handleBuyThirdPartyItemTierRequest(action: BuyThirdPartyItemTiersRequestAction) {
    const { thirdParty, tier } = action.payload
    try {
			// 체인 ID를 가져오는 데 실패할 수 있으므로, 이 시나리오가 반드시 실행되도록 테스트해야 합니다.
      const maticChainId: ChainId = yield call(getChainIdByNetwork, Network.MATIC)
      const thirdPartyContract = yield call(getContract, ContractName.ThirdPartyRegistry, maticChainId)
			// 트랜잭션 전송은 성공하거나 실패할 수 있으므로, 두 경우 모두 테스트에서 반드시 다뤄야 합니다.
      const txHash: string = yield call(sendTransaction, thirdPartyContract, instantiatedThirdPartyContractContract =>
        instantiatedThirdPartyContractContract.buyItemSlots(thirdParty.id, tier.id, tier.price)
      )
      yield put(buyThirdPartyItemTiersSuccess(txHash, maticChainId, thirdParty, tier))
    } catch (error) {
      yield put(buyThirdPartyItemTiersFailure(error.message, thirdParty.id, tier))
    }
  }
}
```

### 사가 테스트 예시

```tsx
// 주요 describe는 'when handling...'으로 시작합니다.
describe('제3자 아이템 티어를 구매하려는 요청을 처리할 때', () => {
	// 모든 경우, 부정과 긍정 모두를 다룹니다.
  describe('그리고 체인 ID를 가져올 수 없을 때', () => {
    it('티어와 제3자 ID가 포함된 아이템 슬롯 티어 구매 실패를 알리는 액션을 put해야 한다', () => {
      return expectSaga(tiersSaga, mockedBuilderApi)
				// provide calls는 다양한 effect를 mock하여 서로 다른 시나리오를 구성합니다.
        .provide([[call(getChainIdByNetwork, Network.MATIC), Promise.reject(new Error(defaultError))]])
				// put calls는 예상했던 최종 결과가 실제로 일어났는지 검증합니다.
        .put(buyThirdPartyItemTiersFailure(defaultError, thirdParty.id, thirdPartyItemTier))
				// 테스트당 하나의 dispatch만 수행하여 우리가 테스트 중인 액션 처리를 검증합니다.
        .dispatch(buyThirdPartyItemTiersRequest(thirdParty, thirdPartyItemTier))
        .run({ silenceTimeout: true })
    })
  })

  describe('트랜잭션 전송이 실패할 때', () => {
    it('티어와 제3자 ID가 포함된 아이템 슬롯 티어 구매 실패를 알리는 액션을 put해야 한다', () => {
      return expectSaga(tiersSaga, mockedBuilderApi)
        .provide([
					// call은 정확한 call effect와 매칭되며, 매개변수를 고려합니다.
					// 이렇게 하면 put 이전에 실행된 코드가 우리가 기대한 대로 동작했는지 테스트가 보장됩니다.
          [call(getChainIdByNetwork, Network.MATIC), ChainId.MATIC_MUMBAI],
          [call(getContract, ContractName.ThirdPartyRegistry, ChainId.MATIC_MUMBAI), contract],
					// matchers는 매개변수가 아니라 함수만 매칭하는 데 사용됩니다.
          [matchers.call.fn(sendTransaction), Promise.reject(new Error(defaultError))]
        ])
        .put(buyThirdPartyItemTiersFailure(defaultError, thirdParty.id, thirdPartyItemTier))
        .dispatch(buyThirdPartyItemTiersRequest(thirdParty, thirdPartyItemTier))
        .run({ silenceTimeout: true })
    })
  })

  describe('트랜잭션 전송이 성공할 때', () => {
    let contract: any
    beforeEach(() => {
      contract = { buyItemSlots: jest.fn() }
    })

    it('티어, 제3자, 트랜잭션 세부 정보가 포함된 아이템 슬롯 티어 구매 성공을 알리는 액션을 put해야 한다', () => {
      return expectSaga(tiersSaga, mockedBuilderApi)
        .provide([
          [call(getChainIdByNetwork, Network.MATIC), ChainId.MATIC_MUMBAI],
          [call(getContract, ContractName.ThirdPartyRegistry, ChainId.MATIC_MUMBAI), contract],
          [matchers.call.fn(sendTransaction), Promise.resolve(txHash)]
        ])
        .put(buyThirdPartyItemTiersSuccess(txHash, ChainId.MATIC_MUMBAI, thirdParty, thirdPartyItemTier))
        .dispatch(buyThirdPartyItemTiersRequest(thirdParty, thirdPartyItemTier))
        .run({ silenceTimeout: true })
    })
  })
})
```

## 리듀서 테스트

리듀서는 상태와 액션을 받아 새로운 상태를 반환하는 함수입니다. 이 새로운 상태는 주어진 액션에 대해 수행되도록 프로그래밍된 변경 사항을 포함할 수 있습니다.

테스트는 필요하다면 초기 상태와 테스트할 액션의 다양한 매개변수를 바꿔 가며 수행해야 합니다. 액션이 주어진 초기 상태의 어떤 부분이든 변경할 수 있으므로, 단언은 반환된 전체 상태에 대해 수행해야 합니다.

액션을 리듀스할 때는 테스트를 더 쉽게 유지보수할 수 있도록 액션 크리에이터를 사용해야 합니다.

우리가 다음을 작성하는 방식을 표준화하기 위해 **describe**와 **테스트**를 다음과 같이 작성합니다:

* 주요 describe는 다음 구문을 사용하여 어떤 액션이 리듀스될 것인지 설명하는 것으로 시작해야 합니다. *when reducing the action* 뒤이어 액션에 대한 설명이 와야 합니다. 테스트할 액션을 설명할 때는 액션 타입을 사용해서는 안 됩니다. 타입은 변경될 수 있기 때문입니다.
* 이 **테스트**describe는 반환된 상태가 어떻게 변경되었는지 명확하게 설명해야 합니다. 예: *에러가 null로 되고 fruits가 설정된 상태를 반환해야 한다*.

### 리듀서 예시

```tsx
const INITIAL_STATE: FruitsState = {
  data: {
    fruits: []
  },
  loading: []
}

export function fruitsReducer(state = INITIAL_STATE, action: any): FruitsState {
  switch (action.type) {
    case FETCH_FRUIT_REQUEST: {
      return {
        ...state,
        loading: loadingReducer(state.loading, action),
				error: null
      }
    }
		case FETCH_FUIT_ERROR: {
			const { error } = action
      return {
        ...state,
				error,
        loading: loadingReducer(state.loading, action)
      }
		}
		case FETCH_FRUIT_SUCCESS: {
			const { fruits } = action
      return {
        ...state,
				fruits,
        loading: loadingReducer(state.loading, action)
      }
		}
}
```

### 리듀서 테스트 예시

```tsx
let state: FruitsState

beforeEach(() => {
	state = {
	  data: {
	    fruits: []
	  },
	  loading: [],
		error: null
	}
})

과일 가져오기를 알리는 액션을 리듀스할 때', () => {
	beforeEach(() => {
		// 에러가 설정되어 있는지 확인하여 해당 액션을 리듀스할 때 지워지도록 합니다.
		state.error = 'anError'
	})

	it('에러가 지워지고 loading 상태가 loading으로 설정된 상태를 반환해야 한다', () => {
		// 리듀서를 쉽게 유지보수할 수 있도록 액션 크리에이터를 사용해 테스트합니다.
		const action = fetchFruitsRequest()

		expect(fruitsReducer(state, action)).toEqual({
			...state,
			error: null,
			/* loading 리듀서는 다른 리듀서의 일부이므로(별도로 테스트할 필요가 없으므로),
				 그대로 사용하거나 이를 모방하는 헬퍼를 사용할 수 있습니다.
			*/
			loading: loadingReducer(state.loading, action),
		})
	})
})

과일 가져오기 실패를 알리는 액션을 리듀스할 때', () => {
	const error = 'anError'

	에러가 설정되고 과일 가져오기 액션에서 loading 상태가 지워진 상태를 반환해야 한다', () => {
		const action = fetchFruitsFailure(error)

		expect(fruitsReducer(state, action)).toEqual({
			...state,
			error,
			loading: loadingReducer(state.loading, action),
		})
	})
})

과일 가져오기 성공을 알리는 액션을 리듀스할 때', () => {
	let fruits: string[]
	beforeEach(() => {
		fruits = ['apples', 'pears']
	})

	과일이 설정되고 과일 가져오기 액션에서 loading 리듀서가 지워진 상태를 반환해야 한다', () => {
		const action = fetchFruitsSuccess(fruits)

		expect(fruitsReducer(state, action)).toEqual({
			...state,
			fruits,
			loading: loadingReducer(state.loading, action),
		})
	})
})
```

## 셀렉터 테스트

셀렉터는 상태를 받아 그 상태의 일부 또는 변환된 상태 일부를 반환하는 메서드입니다.

셀렉터에는 메모이즈된 셀렉터와 일반 또는 공통 셀렉터, 두 가지 유형이 있습니다. 일반 또는 공통 셀렉터는 다른 단위 테스트 함수처럼 테스트해야 하며, 메모이즈된 셀렉터는 다음을 사용해 테스트해야 합니다. `resultFunc` 또는 상태의 메모이즈된 부분을 받아 원하는 값을 반환하는 함수로 테스트해야 합니다.

**Describe**와 **테스트**는 다음에 설명된 대로 작성해야 합니다. [컨텍스트 설명 및 구성](/contributor/contributor-ko/contributor-guides/testing-standards/writing-tests.md#describing-and-building-context) 섹션에서 설명한 것과 동일한 규칙을 따릅니다.

### 셀렉터 예시

```tsx
// 일반 셀렉터
export const collectionExists = (state: RootState, id: string): boolean => {
	return state.collections.data.some((collection) => collection.id === id)
}

// 향상된 컬렉션 목록을 반환하는 reselect 기반의 메모이즈된 셀렉터
export const getEnhancedCollections = createSelector(
	getCollections,
	getExtraData,
	(collections, extraData) => {
		return collections.map((collection) =>
			({ ...collection, ...extraData[collection[id]] }))
	}
)
```

### 셀렉터 테스트 예시

```tsx
let state: RootState
let collection: { id: string }
beforeEach(() => {
	collection = {
		id: 'anId'
	}
	state = {
		collections: {
			data: []
		}
	}
})

beforeEach(() => {
	state.collections.data = [collection]
})

// 일반 셀렉터는 일반 함수처럼 단위 테스트합니다.
컬렉션이 있는지 확인할 때', () => {
	해당 id를 가진 컬렉션이 없을 때', () => {
		false를 반환해야 한다', () => {
			expect(collectionExists(state, "anotherId")).toBe(false)
		})
	})

	해당 id를 가진 컬렉션이 있을 때', () => {
		true를 반환해야 한다', () => {
			expect(collectionExists(state, collection.id)).toBe(true)
		})
	})
})

향상된 컬렉션을 가져올 때', () => {
	let enhacements: Record<string, Enhacement>

	이를 향상시킬 데이터가 없을 때', () => {
		beforeEach(() => {
			enhacements = {}
		})

		// 메모이즈된 셀렉터는 resultFunc를 사용해 단위 테스트해야 합니다.
		향상하지 않은 모든 컬렉션을 가져와야 한다', () => {
			expect(getEnhancedCollections.resultFunc(collections, enhacements)).toEqual([
				collection
			])
		})
	})

	이를 향상시킬 데이터가 있을 때', () => {
		beforeEach(() => {
			enhacements = { [collection.id]: { aNewProperty: true } }
		})

		모든 향상된 컬렉션을 가져와야 한다', () => {
			expect(getEnhancedCollections.resultFunc(collections, enhacements)).toEqual([
				{ ...collection, ...enhacements[collection.id] }
			])
		})
	})
})
```

## 액션 크리에이터 테스트

액션 크리에이터는 나중에 리듀서에서 처리될 액션을 생성하는 역할을 합니다. 액션 크리에이터는 보통 단순하지만, 우리가 기대한 대로 동작하는지 확인하기 위해 반드시 테스트해야 합니다.

**Describe**와 **테스트**는 다음에 설명된 대로 작성해야 합니다. [컨텍스트 설명 및 구성](/contributor/contributor-ko/contributor-guides/testing-standards/writing-tests.md#describing-and-building-context) 섹션에서 설명한 것과 동일한 규칙을 따릅니다.

### 액션 크리에이터 예시

```tsx
export const BUY_THIRD_PARTY_ITEM_TIERS_REQUEST = '[Request] Buy a third party item tier'
export const BUY_THIRD_PARTY_ITEM_TIERS_SUCCESS = '[Success] Buy a third party item tier'
export const BUY_THIRD_PARTY_ITEM_TIERS_FAILURE = '[Failure] Buy a third party item tier'

export const buyThirdPartyItemTiersRequest = (thirdParty: ThirdParty, tier: ThirdPartyItemTier) =>
  action(BUY_THIRD_PARTY_ITEM_TIERS_REQUEST, { thirdParty, tier })
export const buyThirdPartyItemTiersSuccess = (txHash: string, chainId: ChainId, thirdParty: ThirdParty, tier: ThirdPartyItemTier) =>
  action(BUY_THIRD_PARTY_ITEM_TIERS_SUCCESS, { thirdParty, tier, ...buildTransactionPayload(chainId, txHash, { tier, thirdParty }) })
export const buyThirdPartyItemTiersFailure = (error: string, thirdPartyId: string, tier: ThirdPartyItemTier) =>
  action(BUY_THIRD_PARTY_ITEM_TIERS_FAILURE, { error, thirdPartyId, tier })
```

### 액션 크리에이터 테스트 예시

```tsx
티어 가져오기 요청의 시작을 알리는 액션을 생성할 때', () => {
  티어 가져오기 시작을 알리는 액션을 반환해야 한다', () => {
    expect(fetchThirdPartyItemTiersRequest()).toEqual({ type: FETCH_THIRD_PARTY_ITEM_TIERS_REQUEST })
  })
})

티어 가져오기 성공을 알리는 액션을 생성할 때', () => {
  티어 가져오기 성공을 알리는 액션을 반환해야 한다', () => {
    expect(fetchThirdPartyItemTiersSuccess([thirdPartyItemTier])).toEqual({
      type: FETCH_THIRD_PARTY_ITEM_TIERS_SUCCESS,
      payload: { tiers: [thirdPartyItemTier], error: undefined, meta: undefined }
    })
  })
})

티어 가져오기 실패를 알리는 액션을 생성할 때', () => {
  티어 가져오기 실패를 알리는 액션을 반환해야 한다', () => {
    expect(fetchThirdPartyItemTiersFailure(defaultError)).toEqual({
      type: FETCH_THIRD_PARTY_ITEM_TIERS_FAILURE,
      payload: { error: defaultError }
    })
  })
})
```

## UI 컴포넌트 테스트

UI 컴포넌트를 테스트할 때는 [React Testing Library](https://testing-library.com/docs)를 사용합니다. 우리는 enzyme 같은 다른 옵션 대신 이 라이브러리를 사용합니다. 사용자와의 상호작용 방식을 고려하여 컴포넌트를 테스트할 수 있게 해주고, 구현 자체가 아니라 사용자 경험에 초점을 맞출 수 있기 때문입니다. 우리는 react 컴포넌트 인스턴스와 상호작용하지 않고, 대신 렌더링된 방식 그대로 DOM 요소와 상호작용합니다.

### 무엇을 테스트할 것인가

이 UI 컴포넌트 테스트의 목적은 컴포넌트의 동작과 사용자가 그것과 상호작용하는 방식을 테스트하는 것입니다. 많은 경우 일부 속성을 얻고 몇몇 액션을 호출하기 위해 redux store에 연결된 컴포넌트가 있을 수 있습니다. 우리는 보통 이를 위해 별도의 `container` 파일을 만들어 store와 상호작용하게 합니다. 이러한 파일은 이 종류의 테스트 범위 밖에 있습니다. 우리는 컴포넌트가 서로 다른 props 값에 따라 어떻게 동작하는지를 테스트하며, 그 값의 출처(redux store, context, parent component)는 중요하지 않고 최종 렌더에 어떤 영향을 주는지만 중요합니다.

### 렌더링

모든 테스트는 render 함수를 호출하는 것으로 시작해야 합니다. 이것은 우리가 매개변수로 전달한 컴포넌트뿐 아니라 모든 자식 컴포넌트도 렌더링합니다. 테스트 설정에서는 이렇게 하지 않아야 합니다.`beforeEach`왜냐하면 DOM 트리에 우리가 원하지 않는 더 많은 컴포넌트가 생기게 하는 문제를 일으킬 수 있기 때문입니다. 결과를 다음 이름의 속성에 저장해야 합니다. **screen**이렇게 하면 구조 분해를 계속 바꾸지 않고도 테스트에 필요한 모든 속성을 얻을 수 있습니다.

```jsx
it('hello world 컴포넌트를 렌더링해야 한다', () => {
	const screen = render(<HelloWorld />)
})
```

문서에 여러 테스트가 있고 컴포넌트에 여러 props가 있다면, 파일 상단에 기본 props 설정과 컴포넌트 렌더링을 담당하는 함수를 만들 수 있습니다.

```jsx
function renderSelectedFilters(props: Partial<Props> = {}) {
  return render(
    <SelectedFilters
      isLandSection={false}
      category={undefined}
      browseOptions={{}}
      onBrowse={jest.fn()}
      {...props}
    />
  )
}
```

### 쿼리

쿼리는 Testing Library가 페이지의 요소를 찾기 위해 제공하는 메서드입니다. 사용할 수 있는 [쿼리의 종류](https://testing-library.com/docs/queries/about/#priority) 가 다릅니다. 가능한 한 항상 `모든 사람이 접근할 수 있는` 쿼리를 사용해야 합니다. 이는 모든 사용자(시각/마우스 사용자와 보조 기술을 사용하는 사용자)의 경험을 반영하기 때문입니다.

해당 쿼리는 다음과 같습니다. `getByRole`, `getByLabelText`, `getByPlaceholderText`, `getByText`, `getByDisplayValue`이 쿼리들을 사용해서 요소에 접근할 수 있다면, 아마도 그 컴포넌트는 접근성이 부족하다는 뜻입니다.

### 사용자 이벤트 테스트

컴포넌트 내부의 함수에는 접근할 수 없으므로, 그 함수가 호출되도록 이끄는 사용자 동작을 모방해야 합니다. 이를 위해 `userEvent` 라이브러리를 사용합니다.

```jsx
const screen = renderSelectedFilters({
      browseOptions: raritiesOptions,
      onBrowse: onBrowseMock
    })
const commonPill = screen.getByTestId('pill-common')
await userEvent.click(within(commonPill).getByRole('button'))
```

### 디버깅

특정 시점의 테스트와 DOM의 현재 상태를 디버그하려면 다음을 사용할 수 있습니다. `debug` 함수인데, render를 호출할 때 반환되는 속성 중 하나입니다.

```jsx
const { debug } = render(<HelloWorld />)

debug()
```

### UI 컴포넌트 테스트 전체 예시

```jsx
function renderSelectedFilters(props: Partial<Props> = {}) {
  return render(
    <SelectedFilters
      isLandSection={false}
      category={undefined}
      browseOptions={{}}
      onBrowse={jest.fn()}
      {...props}
    />
  )
}

describe('희귀도 필터', () => {
  it('희귀도를 렌더링해야 한다', () => {
    const raritiesOptions = { rarities: [Rarity.COMMON, Rarity.EPIC] }
    const screen = renderSelectedFilters({
      browseOptions: raritiesOptions
    })
    expect(screen.getByText(Rarity.COMMON)).toBeInTheDocument()
    expect(screen.getByText(Rarity.EPIC)).toBeInTheDocument()
  })

  it('삭제된 희귀도 없이 onBrowse를 호출해야 한다', async () => {
    const raritiesOptions = { rarities: [Rarity.COMMON, Rarity.EPIC] }
    const onBrowseMock = jest.fn()

    const screen = renderSelectedFilters({
      browseOptions: raritiesOptions,
      onBrowse: onBrowseMock
    })
    const commonPill = screen.getByTestId('pill-common')
    await userEvent.click(within(commonPill).getByRole('button'))
    expect(onBrowseMock).toHaveBeenCalledWith({ rarities: [Rarity.EPIC] })
  })
})
```

## 디렉터리 구조

테스트는 검증할 파일이나 모듈과 같은 위치에, 같은 이름으로, 다음 확장자를 사용해 배치해야 합니다. `spec.ts` 대신 `ts`.

파일 구조는 다음과 같아야 합니다:

```
actions.ts
action.spec.ts
reducer.ts
reducer.spec.ts
sagas.ts
sagas.spec.ts
selectors.ts
selectors.spec.ts
utils.ts
utils.spec.ts
```


---

# 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-ko/contributor-guides/testing-standards/testing-uis.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.
