使用 React Relay 测试组件

Posted

技术标签:

【中文标题】使用 React Relay 测试组件【英文标题】:Testing components using React Relay 【发布时间】:2021-06-29 20:45:18 【问题描述】:

我正在使用新的 Relay Hooks 并发现很难通过测试。我遇到了他们docs 中提到的问题。

如果在usePreloadedQuery之前和之后添加console.log,只会命中“before”调用

//sample test

jest.useFakeTimers()

test("a list of entries is displayed when the component mounts", async () => 
  const environment = createMockEnvironment()

  environment.mock.queueOperationResolver(operation => 
    return MockPayloadGenerator.generate(operation, 
      Entry() 
        return 
          id: "123",
          title: "hello",
          urlKey: "abc"
        
      
    )
  )

  relay.mock.queuePendingOperation(EntryListQuery, )

  render(<RelayEnvironmentProvider environment=environment>
           <Entries />
         </RelayEnvironmentProvider>
  )

  jest.runAllImmediates()

  expect(await screen.getByText(/hello/i)).toBeInTheDocument()
)
//core component I am wanting to test
import  Suspense, useEffect  from "react"
import  useQueryLoader  from "react-relay/hooks"
import  Loading  from "./Loading"
import  EntryList, EntryListQuery  from "./EntryList"


const Entries = () => 
  const [queryReference, loadQuery, disposeQuery] = useQueryLoader(EntryListQuery)

  useEffect(() => 
    if (!queryReference) loadQuery()
  , [disposeQuery, loadQuery, queryReference])

  if (!queryReference) return <Loading />

  return (
    <Suspense fallback=<Loading />>
      <EntryList queryReference=queryReference />
    </Suspense>
  )


export  Entries 
//the core component's child component
import  usePreloadedQuery  from "react-relay/hooks"
import graphql from "babel-plugin-relay/macro"
import  Link  from "react-router-dom"
import  Entry  from "./Entry"

const EntryListQuery = graphql`
  query EntryListQuery 
    queryEntry 
      id
      title
      urlKey
    
  
`

const EntryList = ( queryReference ) => 
  const  queryEntry  = usePreloadedQuery(EntryListQuery, queryReference)

  return (
    <section>
      <div className="flex justify-between items-center">
        <p>search</p>
        <Link to="?action=new">New Entry</Link>
      </div>
      <ul>
        queryEntry.map(entry => 
          if (entry) return <Entry key=entry.id entry=entry />
          return null
        )
      </ul>
    </section>
  )

export  EntryList, EntryListQuery 

我发现loadQuery 正在被调用,但是我在queueOperationResolver 中的console.log 的任何内容都没有出现。如果我在usePreloadedQuery 之前添加一个console.log,它会输出,但之后不会。因此,EntryList 似乎被挂起,查询永远无法解决。

我发现如果我将测试更改为以下也不会触发任何错误,看起来queueOperationResolver 永远不会被调用。

environment.mock.queueOperationResolver(() => new Error("Uh-oh"))

当我在 EntryList 之前的 usePreloadedQuery 代码之前 console.log queryReference 时,它会输出如下所示的对象。所以我知道查询被正确传递了。


      kind: 'PreloadedQuery',
      environment: RelayModernEnvironment 
        configName: 'RelayModernMockEnvironment',
        _treatMissingFieldsAsNull: false,
        __log: [Function: emptyFunction],
        requiredFieldLogger: [Function: defaultRequiredFieldLogger],
        _defaultRenderPolicy: 'partial',
        _operationLoader: undefined,
        _operationExecutions: Map(1)  '643ead0ae575426fdd62800c27d6fef3' => 'active' ,
        _network:  execute: [Function: execute] ,
        _getDataID: [Function: defaultGetDataID],
        _publishQueue: RelayPublishQueue 
          _hasStoreSnapshot: false,
          _handlerProvider: [Function: RelayDefaultHandlerProvider],
          _pendingBackupRebase: false,
          _pendingData: Set(0) ,
          _pendingOptimisticUpdates: Set(0) ,
          _store: [RelayModernStore],
          _appliedOptimisticUpdates: Set(0) ,
          _gcHold: null,
          _getDataID: [Function: defaultGetDataID]
        ,
        _scheduler: null,
        _store: RelayModernStore 
          _gcStep: [Function (anonymous)],
          _currentWriteEpoch: 0,
          _gcHoldCounter: 0,
          _gcReleaseBufferSize: 10,
          _gcRun: null,
          _gcScheduler: [Function: resolveImmediate],
          _getDataID: [Function: defaultGetDataID],
          _globalInvalidationEpoch: null,
          _invalidationSubscriptions: Set(0) ,
          _invalidatedRecordIDs: Set(0) ,
          __log: null,
          _queryCacheExpirationTime: undefined,
          _operationLoader: null,
          _optimisticSource: null,
          _recordSource: [RelayMapRecordSourceMapImpl],
          _releaseBuffer: [],
          _roots: [Map],
          _shouldScheduleGC: false,
          _storeSubscriptions: [RelayStoreSubscriptions],
          _updatedRecordIDs: Set(0) ,
          _shouldProcessClientComponents: undefined,
          getSource: [Function],
          lookup: [Function],
          notify: [Function],
          publish: [Function],
          retain: [Function],
          subscribe: [Function]
        ,
        options: undefined,
        _isServer: false,
        __setNet: [Function (anonymous)],
        DEBUG_inspect: [Function (anonymous)],
        _missingFieldHandlers: undefined,
        _operationTracker: RelayOperationTracker 
          _ownersToPendingOperationsIdentifier: Map(0) ,
          _pendingOperationsToOwnersIdentifier: Map(0) ,
          _ownersIdentifierToPromise: Map(0) 
        ,
        _reactFlightPayloadDeserializer: undefined,
        _reactFlightServerErrorHandler: undefined,
        _shouldProcessClientComponents: undefined,
        execute: [Function: mockConstructor] 
          _isMockFunction: true,
          getMockImplementation: [Function (anonymous)],
          mock: [Getter/Setter],
          mockClear: [Function (anonymous)],
          mockReset: [Function (anonymous)],
          mockRestore: [Function (anonymous)],
          mockReturnValueOnce: [Function (anonymous)],
          mockResolvedValueOnce: [Function (anonymous)],
          mockRejectedValueOnce: [Function (anonymous)],
          mockReturnValue: [Function (anonymous)],
          mockResolvedValue: [Function (anonymous)],
          mockRejectedValue: [Function (anonymous)],
          mockImplementationOnce: [Function (anonymous)],
          mockImplementation: [Function (anonymous)],
          mockReturnThis: [Function (anonymous)],
          mockName: [Function (anonymous)],
          getMockName: [Function (anonymous)]
        ,
        executeWithSource: [Function: mockConstructor] 
          _isMockFunction: true,
          getMockImplementation: [Function (anonymous)],
          mock: [Getter/Setter],
          mockClear: [Function (anonymous)],
          mockReset: [Function (anonymous)],
          mockRestore: [Function (anonymous)],
          mockReturnValueOnce: [Function (anonymous)],
          mockResolvedValueOnce: [Function (anonymous)],
          mockRejectedValueOnce: [Function (anonymous)],
          mockReturnValue: [Function (anonymous)],
          mockResolvedValue: [Function (anonymous)],
          mockRejectedValue: [Function (anonymous)],
          mockImplementationOnce: [Function (anonymous)],
          mockImplementation: [Function (anonymous)],
          mockReturnThis: [Function (anonymous)],
          mockName: [Function (anonymous)],
          getMockName: [Function (anonymous)]
        ,
        ...

更新

我发现以下测试有效,因此这意味着在尝试模拟使用 useQueryLoader 的组件中的查询时我做错了。

//sample test
test("a list of entries is displayed when the component mounts", async () => 
  const environment = createMockEnvironment()

  environment.mock.queueOperationResolver(operation => 
    return MockPayloadGenerator.generate(operation, 
      Entry() 
        return 
          id: "123",
          title: "hello",
          urlKey: "abc"
        
      
    )
  )

  relay.mock.queuePendingOperation(EntryListQuery, )
  
  const queryReference = loadQuery(environment, EntryListQuery, , )

  render(<RelayEnvironmentProvider environment=environment>
           <EntryList queryReference=queryReference= />
         </RelayEnvironmentProvider>
  )

  expect(await screen.getByText(/hello/i)).toBeInTheDocument()
)

【问题讨论】:

【参考方案1】:

感谢 OP 和官方文档,我得到了它的工作。我的最终代码如下所示:

import React,  ReactNode, Suspense  from "react";
import  act, render, RenderAPI  from "@testing-library/react-native";
import 
  createMockEnvironment,
  MockPayloadGenerator,
  RelayMockEnvironment,
 from "relay-test-utils";
import  loadQuery, RelayEnvironmentProvider  from "react-relay";
import Component from "../../src/components/Component";
import compiledQuery, 
  ComponentQuery,
 from "../../src/components/__generated__/Component.graphql";

type RenderWithProps = 
  environment: RelayMockEnvironment;
;

const renderWith = ( environment : RenderWithProps): RenderAPI => 
  const wrapper = ( children :  children: ReactNode ) => 
    return (
      <RelayEnvironmentProvider environment=environment>
        <Suspense fallback=<View></View>>children</Suspense>
      </RelayEnvironmentProvider>
    );
  ;
  const queryRef = loadQuery<ComponentQuery>(
    environment,
    compiledQuery,
    
      id: "testId",
    
  );
  return render(<Component queryRef=queryRef />,  wrapper );
;

describe("Component", () => 
  it("renders", async () => 
    jest.useFakeTimers();
    const environment = createMockEnvironment();

    environment.mock.queueOperationResolver((operation) => 
      return MockPayloadGenerator.generate(operation, 
        DataType() 
          return 
            edges: [
              
                node: 
                  name: "hello",
                ,
              ,
              
                node: 
                  name: "world",
                ,
              ,
            ],
          ;
        ,
      );
    );

    environment.mock.queuePendingOperation(compiledQuery, 
      // these variables need to be identical to the variables used in loadQuery
      id: "testId",
    );

    const  getAllByTestId, getByText  = renderWith( environment );
    act(() => jest.runAllImmediates());

    expect(getAllByTestId("list-item").length).toBe(2);
    getByText("hello");
    getByText("world");
  );
);

【讨论】:

【参考方案2】:

我能够通过以下测试通过测试,但我不认为使用间谍是最好的方法。


import  screen, render  from "@testing-library/react"
import  loadQuery, RelayEnvironmentProvider  from "react-relay"
import  createMockEnvironment, MockPayloadGenerator  from "relay-test-utils"

//this is only used for the spy
import * as reactRelay from "react-relay/hooks"


test("a list of entries is displayed when the component mounts", async () => 
  const environment = createMockEnvironment()

  environment.mock.queueOperationResolver(operation => 
    return MockPayloadGenerator.generate(operation, 
      Entry() 
        return 
          id: "123",
          title: "hello",
          urlKey: "abc"
        
      
    )
  )

  relay.mock.queuePendingOperation(EntryListQuery, )

  const mockLoadQuery = loadQuery(relay, EntryListQuery, , )

  const useQueryLoaderSpy = jest.spyOn(reactRelay, "useQueryLoader").mockReturnValueOnce([null, mockLoadQuery, jest.fn()])

  render(<RelayEnvironmentProvider environment=environment>
           <Entries />
         </RelayEnvironmentProvider>
  )

  expect(await screen.getByText(/hello/i)).toBeInTheDocument()

  useQueryLoaderSpy.mockRestore()
)

【讨论】:

以上是关于使用 React Relay 测试组件的主要内容,如果未能解决你的问题,请参考以下文章

使用 Jest 对 Relay 容器的集成测试与工作中的 GraphQL 后端不工作

使用 Jest 对 Relay 容器的集成测试与工作中的 GraphQL 后端不工作

使用 Relay.setVariables 保留 React 组件状态

如何将 react-relay 组件添加到故事书中?

[react] 说说你对Relay的理解

制作一个高阶组件以与 TypeScript 互操作 react-relay 和 react-router