XState 시스템, 유형 및 테스트의 가독성을 위해 노력하는 방법

나는 최근에 내부 XState 작업팀을 추진했는데 모든 참여자들이 간단한 프로젝트를 실시해야 한다.이 기계 자체는 매우 간단하다. 나는 여기서 나의 설계 결정을 토론하는데 주로 타자 스크립트와 테스트와 관련이 있다.

할 수 있다
할 수 있다

CodeSandbox에서 기계 재생 및 검사 기계적 특징


제안된 제한된 상태기는 사용자 목록의 UI와 엄격하게 결합됩니다.다음과 같은 이점을 제공합니다.
  • 기계 가동 시 인수
  • 필터 교체queryemployment 유형으로 구성)
  • 필터 변경
  • 시 다음 추출을 디더링 제거
  • 마지막 수거가 원활한지 알아보기
  • 방문한 사람
  • 마지막 리콜 실패 시 발생한 오류 대기열 액세스
  • 재시도 전에 실패한 요청
  • 이전 요청을 취소합니다.추출이 진행 중인 경우 검색/필터 입력이 비활성화되지 않습니다.이전 리콜을 취소하면 관리 경쟁을 피할 수 있습니다
  • .
    향후 고려 가능한 미포함 기능:
  • 강제 추출:
  • 반발력이 800+ms
  • 까지 증가합니다.
  • 사용자 목록은 사용자에게 강제 취득 방법이나 즉시 취득해야 할 내용을 터치할 수 있다
  • 분기 GitHub 저장소 설계 결정


    우선, 왜 XState를 선택했는가: 설령 내가 XState 전문가가 아니더라도, 나는 그것이 유한상태기를 관리, 읽기, 기록하는 고급 도구라고 생각하기 때문에'길다'useReducer 코드를 읽거나 React 갈고리를 뛰어넘어 각각useEffect의 기능을 이해하려고 하기 때문이다.나는 이것이 이 주제의 좋은 도서라고 생각한다.저는 본고의 계발을 받았고 XState 경험을 얻고 싶은 의지의 계발을 받았습니다. 저는 저/저희가 XState가 상세하게 소개한 개발 모델을 좋아하는지 알고 싶습니다.최근에 작업에서 사용하기 시작했습니다. 저는 현재 XState 작업팀을 이끌고 있습니다.

    기계.



    이것은 입니다.
    유한 상태기 코드에 사용되는 디자인 모드는 다음과 같습니다.

  • 기계의 초기 상태는idle: 처음에는 목록을 채우기 위해 인원을 가져와야 하고 인원 페이지를 마운트할 때 발생합니다.어쨌든, 기계를 초기의 '빈' 상태에서 캡처 인원으로 이동하는 것은 제어된 사건으로, 반드시 인원 페이지 자체로 보내서, 기계가 응용 프로그램이 기대하지 않을 때 고의로 캡처를 시작하지 않도록 해야 한다.
  • export const machine = createMachine<Context, Events, States>(
      {
        initial: 'idle',
        // ...
      }
    
  • 이벤트는 대문자입니다. 또한 이벤트는 유일하게 대문자 이름을 가진 실체로 다른 실체와 구별됩니다.
  • 전환할 때 지능성보다 가독성을 선호한다. SET_QUERY/SET_EMPLOYMENT 전환은 많은 상태에서 중복된다(5회 미만).이것은 나의 선택이다. 나는 독자들이 불필요한 추상적 때문에 코드에서 도약하는 것이 아니라 모든 상태의 위탁 관리 사건을 즉각 이해하기를 바란다.
  • 내연 리셋과 서비스를 피한다. 리셋과 서비스는 내연 리셋을 하거나 기계의 설정 대상(두 번째createMachine 파라미터)으로 이동할 수 있다.이것은 테스트에 있어서 매우 중요합니다. machine.withConfig 함수를 통해 아날로그 가져온 서비스가 아닌 (이 예에서 fetchPeople 함수를 덮어쓰는 것을 허용합니다.
  • export const machine = createMachine<Context, Events, States>(
      {
        /* ... */
      },
      {
        services: {
          // <-- NOTE: Services goe here
          // ...
        },
      },
    )
    

  • 내연 지연 피하기: 전점도 탈온스 지연에 적용된다. 만약 미래 기계의 지연에 변화가 발생하면 테스트에 아무런 변화가 없다. 이것은 탈온스 지연을 덮어씌울 것이다.
  • export const machine = createMachine<Context, Events, States>(
      {
        /* ... */
      },
      {
        delays: {
          // <-- NOTE: Delays goe here
          // ...
        },
      },
    )
    
    이것은입니다.

    유한 상태기의 전체 코드 타입


    여기는the full code of the Finite State Machine입니다.
    제한된 상태기 유형에 사용되는 디자인 모델은 다음과 같습니다.

  • 모든 상하문은 그 특정한 리메이크만 포함한다. 기계의 상하문은 시종 모든 속성을 가지지만 각종 유형InitialContext, DebounceFetchContext 등은 그 특정한 내용만 지정한다.그것들은 단독으로 사용하는 것이 아니라 Context & InitialContext식의 교차로를 통과한다.
  • export type InitialContext = {
      filter: Filter
      debounceFilter: undefined
      fetching: false
      people: []
      fetchErrors: []
    }
    
    export type DebounceFetchContext = {
      debounceFilter: Filter // <-- NOTE: Specify only the properties to override
    }
    
    export type States =
      | { value: 'idle'; context: Context & InitialContext }
      | { value: 'success'; context: Context & SuccessContext }
      | { value: 'fetch'; context: Context & FetchContext }
      | { value: 'debounceFetch'; context: Context & DebounceFetchContext } // <-- NOTE: the `Context & DebounceFetchContext` type
      | { value: 'failure'; context: Context & FailureContext }
    

  • 이벤트 범주: types 모듈에서 이벤트 기준
  • 내부 이벤트, 특히 데이터를 가져오는 이벤트
  • 사용자가 직접 트리거한 외부 이벤트
  • 사용자가 직접 촉발하지 않은 외부 이벤트(예를 들어 기계를 빈 상태에서 옮기는 경우START 이벤트)
  • 이것은 독자로 하여금 이 사건들의 목적을 즉각 이해할 수 있게 한다
    // ---------------------------------------------------------------
    // EVENTS
    export type Events = InternalEvents | ExternalEvents | UserEvents
    

    테스트


    이것은the full type definitions입니다.
    유한 상태기 테스트에 사용되는 디자인 모델은 다음과 같습니다.

  • 강가독성: 나는 테스트의 가독성에 매우 관심이 있다. 왜냐하면 코드가 어떻게 작동하는지 이해하는 도구이기 때문이다. 그리고 그것들의 복잡도는 테스트 중인 코드보다 10배 낮아야 하기 때문이다.분명히 이 화제는 주관적이지만, 나는 미래의 개발자들이 편안함을 느낄 수 있도록 최선을 다했다
  • 기계를 어떻게 재구성했는지 알고 싶다
  • 예기치 못한 실패를 복구해야 하는 테스트
  • 다음 상황이 가장 문제다.보통 저는 테스트를 최대한 명확하게 하여 테스트 개발자의 업무를 간소화할 것입니다. 그들은 테스트를 빨리 복구하기 위해 테스트가 어떻게 작동하는지 즉시 이해해야 합니다.
  • 로컬 아날로그 데이터: 이 점은'가독성'과 밀접한 관계를 가진다.만약 아날로그 데이터가 매우 간단하다면, 외부 의존을 피하기 위해 같은 파일에 있어야 한다. (((((((defaultFetchDatamachine.test.ts 참조)
  • 선형 테스트 흐름과 내부 사건 이 개발자가 서비스 기반의 기계를 테스트하도록 추진하는 것은 다음과 같다.
  • it('should eventually reach "success"', done => {
      const service = interpret(machine).onTransition(state => {
        if (state.matches('success')) {
          done() // <-- NOTE: asserting/succeeding first
        }
      })
    
      service.start()
      service.send({
        type: 'SUCESSS',
        people: [
          /*...*/
        ],
      }) // <-- NOTE: acting last
    })
    
    하지만
  • 이것은 가독성을 악화시킬 수 있다. 왜냐하면 일반적인 테스트의 마지막 단계는 단언/성공
  • 이기 때문이다.
  • 동시에 { type: 'SUCESSS', people: [/*...*/] })는 내부 사건으로 누구도 알 필요가 없고 심지어 기계의 테스트도 알 필요가 없다. 왜냐하면 이것은 세부 사항을 실현하는 것이기 때문이다
  • 상기 몇 가지를 만족시키기 위해, 나는 이런 테스트를 더욱 좋아한다.
    // ARRANGE
    const { service, resolveFetchMock } = createMockedMachine()
    
    // ACT
    // NOTE: it's an external event, it's fine expliciting it
    service.send({ type: 'START' })
    // NOTE: exxternal services are managed through dedicated utilities, allow concentrating on the `data`
    // the service should return
    resolveFetchMock(defaultFetchData)
    
    // ASSERT
    expect(service.state).toMatchObject({
      value: 'success',
      context: { people: defaultFetchData, fetching: false },
    })
    
  • createMockedMachine utility: 나는 먼저 모든 테스트를 작성한 다음에 어떤 코드가 모든 테스트에서 흔히 볼 수 있는 코드인지 자세하게 분석했는데 이 코드들은 그들의 가독성을 떨어뜨렸다.createMockedMachine 실용 프로그램은 최소 설정의 기계를 만들지만 제어 위조fetchPeople 서비스의 실용 프로그램을 되돌려줍니다.
  • 투명 블록: 테스트마다 길이에 상관없이 투명ARRANGE/ACT/ASSERT 블록
  • 테스트 방법: 이 기계를 테스트하려면 주로 세 가지 방법이 있다
  • 유사한 기능 테스트 방법: 사용자 흐름을 실행하고 테스트가 명확한'끝'이 없으며 같은 테스트에서 여러 입력 조합을 시도
  • 단일 흐름 방법: 모든 테스트는 여러 단계를 포함하지만 단일하고 뚜렷한 최종 결과를 가리킨다
  • 단일 변환 방법: 기계가 특정 상태에서 가동, 이벤트 발송, 다음 상태 서명
  • 두 번째 방법을 선택한 이유는 다음과 같습니다.
  • 첫 번째 방법은 브라우저 기반 기능 테스트 클론을 만드는 작업량이 너무 많다
  • 첫 번째 방법은 매우 긴 테스트를 생성하는데'시각'대응물이 없으면 따르기 어렵다
  • 세 번째 방법은 테스트 기계에 대한 실현 세부 사항에 지나치다
  • 세 번째 방법은 개발자가 XState의 내부 이벤트
  • 를 알아야 한다.
    테스트 예
    첫 번째 방법 예
    it('When the fetch fails, should allow retrying with the same filter and clear the errors', async () =&gt; {
      // ARRANGE
      const debounceDelay = 1
      const query = 'Ann Henry'
      const filteredFetchData = [annHenry]
      const {
        service,
        fetchMock,
        rejectFetchMock,
        resolveFetchMock,
        waitDebouncedFetch,
      } = createMockedMachine(debounceDelay)
    
      // 1. START THE MACHINE
      service.send({ type: 'START' })
      expect(fetchMock).toHaveBeenCalledTimes(1)
    
      // 2. REJECT THE FETCH
      rejectFetchMock(error)
      expect(service.state).toMatchObject({
        value: 'failure',
        context: { fetchErrors: [error] },
      })
    
      // 3. CHANGE THE QUERY
      service.send({ type: 'SET_QUERY', query })
    
      // 4. DEBOUNCED FETCH
      await waitDebouncedFetch()
    
      // 5. REJECT THE FETCH
      rejectFetchMock(error)
      expect(service.state).toMatchObject({ context: { fetchErrors: [error, error] } })
      expect(fetchMock).toHaveBeenCalledTimes(2)
    
      // 6. RETRY FETCHING
      service.send({ type: 'RETRY' })
    
      // 7. RESOLVE THE FETCH
      resolveFetchMock(filteredFetchData)
      expect(service.state).toMatchObject({
        value: 'success',
        context: { fetchErrors: [], people: filteredFetchData },
      })
      expect(fetchMock).toHaveBeenCalledTimes(3)
    
      // 8. CHECK THE THIRD FETCH' QUERY
      expect(fetchMock).toHaveBeenLastCalledWith(
        // machine' context
        expect.objectContaining({ filter: expect.objectContaining({ query }) }),
        // machine' states and invokeMeta, useless for the purpose of the tests
        expect.anything(),
        expect.anything(),
      )
    })
    
    두 번째 방법의 예
    it('When retrying, should retry with the last query', async () =&gt; {
      // ARRANGE
      const query = 'Ann Henry'
      const {
        service,
        fetchMock,
        rejectFetchMock,
        resolveFetchMock,
        waitDebouncedFetch,
      } = createMockedMachine(1)
    
      // ACT
      service.send({ type: 'START' })
      resolveFetchMock(defaultFetchData)
      service.send({ type: 'SET_QUERY', query })
      await waitDebouncedFetch()
      rejectFetchMock(error)
      service.send({ type: 'RETRY' })
      await waitDebouncedFetch()
    
      // ASSERT
      expect(fetchMock).toHaveBeenLastCalledWith(
        // machine' context
        expect.objectContaining({ filter: expect.objectContaining({ query }) }),
        // machine' states and invokeMeta, useless for the purpose of the tests
        expect.anything(),
        expect.anything(),
      )
    })
    
    세 번째 방법의 예
    it('When the "FAILURE" event occurs, should move from the "fetch" state to the "failure" one', () =&gt; {
      // ARRANGE
      const { machineMock } = createMockedMachine()
    
      // ACT
      const actualState = machineMock.transition('fetch', {
        type: 'FAILURE',
        data: error,
      })
    
      // ASSERT
      expect(actualState).toMatchObject({
        value: 'failure',
        context: { fetching: false, fetchErrors: [error] },
      })
    })
    
    이것은the full code of the tests입니다.

    테스트 기계 문서의 테스트 서비스 장절 반응에서 기계를 소모하다


    이것은the full code of the tests입니다.

    포장지


    렌더링 트리는 MachineRoot 구성 요소로 포장되어야 하며, 이 구성 요소는 실행 중인 기계 (서비스) 를 React 상하문에 저장해야 한다.

    React와 통합된 유한 상태기 전체 코드 기계에 접근하다


    운행 중인 기계는 React 상하문을 통해 접근할 수 있는데 이것은 소비자가 반드시 알아야 할 실현 세부 사항이다.기계에 연결해야 하는 모든 구성 요소는 내보낸 useMachine 갈고리를 사용해야 합니다. 이것은 React 상하문의 사용법을 완전히 숨깁니다.이것은 미래의 재구성에 특히 유용하다.

    작업 그룹


    저희가 어떻게 하는지에 대한 노트들입니다.
  • 전단 개발자 4명
  • 으로 구성된 워크그룹
  • 우리는 두 번 만난 적이 있다. 우리는 비동기적으로 일한다
  • 각 구성원은 XState
  • 를 통해 구현되는 소규모 프로젝트를 제안하고 설명합니다.
  • 우리는 서로 프로젝트를 분배한다.아무도 그의 프로젝트에 참여하지 않을 것이다
  • 우리는 Confluence를 통해 모든 것을 기록하여 장래에 참고할 수 있도록 했다
  • CodeSandbox
  • 에서 프로젝트를 구현했습니다.
    나는 서로 다른 사람이 어떻게 비슷한 문제를 해결하는지 보면 항상 좋다고 말할 수 있다😊

    좋은 웹페이지 즐겨찾기