React 구성 요소를 사용하여 Cypress 구성 요소 테스트에 접근하는 몇 가지 방법

목차
  • What is Cypress Component Testing ?
  • Wrapping components to provide their external dependencies
  • Applying this concept to entire pages

  • Cypress 구성 요소 테스트란 무엇입니까?

    A few months ago, Cypress released their component test runner in beta. I have been using it since then, as part of a real world project, why ?

    The main advantage of Cypress Component Testing over the competition is the fact that it runs in the browser, just like their end-to-end product. This is a big deal because your components will run on browsers as-well once you ship them to your users ! This alone makes writing component tests much easier for the developer because they have access to the same APIs they do when building their apps and components compared to others node-based test runners.

    Today we won't talk about how to install, configure or approach the conception process of your tests, they have great documentation on the subject here 대신 Cypress 구성 요소 테스트를 시작할 때 발생할 수 있는 몇 가지 함정에 대해 이야기하십시오.

    외부 종속성을 제공하기 위해 구성 요소 래핑

    Components might need external dependencies, context, props, or anything really to work properly, this is a recurrent problem for all test runners and cypress has its own way of dealing with it. Let's have an easy to understand example:

    This is a simple button which will toogle a darkMode context set somewhere else in your application, it calls the hook useDarkMode which is defined in another folder

    DarkModeSelector.jsx

    import { useDarkMode } from "../utils/useDarkMode";
    
    export const DarkModeSelector = () => {
      const { darkMode, setDarkMode } = useDarkMode();
      return (
        <button onClick={() => setDarkMode(!darkMode)}>
          {darkMode ? "Switch to lightmode" : "Switch to darkmode"}
        </button>
      );
    };
    

    useDarkMode.jsx

    import { createContext, useContext } from "react";
    
    const DarkModeContext = createContext(undefined);
    
    export const DarkModeProvider = ({
      children,
      value: { darkMode, setDarkMode },
    }) => (
      <DarkModeContext.Provider value={{ darkMode, setDarkMode }}>
        {children}
      </DarkModeContext.Provider>
    );
    
    export const useDarkMode = () => {
      const context = useContext(DarkModeContext);
      if (!context) {
        throw new Error("useDarkMode must be used within a DarkModeProvider");
      }
      return context;
    };
    

    If you tried to mount this component as is, you'd get this error: useDarkMode must be used within a DarkModeProvider , because DarkModeSelector tries to use a context that hasn't been defined prior to its mounting.

    The easiest, most simple way to fix that is to define a wrapper and then wrap our component in the mount function

    DarkModeSelector.cy.jsx

    const DarkModeWrapper = ({ children }) => {
      const [darkMode, setDarkMode] = useState(false);
      return (
        <DarkModeProvider
          value={{
            darkMode,
            setDarkMode,
          }}
        >
          {children}
        </DarkModeProvider>
      );
    };
    
    describe("<DarkModeSelector />", () => {
      it("mounts with a wrapper", () => {
        cy.mount(
          <DarkModeWrapper>
            <DarkModeSelector />
          </DarkModeWrapper>
        );
        cy.contains("Switch to darkmode");
      });
    });
    

    Good ! Now your test is passing and your component mounts.

    At the moment you might want to pass an initial value to your context, so you can pass a setting set somewhere else in your application:

    We can add an initialValue to the props of the wrapper

    DarkModeSelector.cy.jsx

    const DarkModeWrapper = ({ children, initialValue }) => {
      const [darkMode, setDarkMode] = useState(initialValue || false);
      return (
        <DarkModeProvider
          value={{
            darkMode,
            setDarkMode,
          }}
        >
          {children}
        </DarkModeProvider>
      );
    };
    
    describe("<DarkModeSelector />", () => {
      it("mounts with a wrapper", () => {
        cy.mount(
          <DarkModeWrapper>
            <DarkModeSelector />
          </DarkModeWrapper>
        );
        cy.contains("Switch to darkmode");
      });
      it("renders the 'Switch to lightmode' label when initiated with darkmode=true value", () => {
        cy.mount(
          <DarkModeWrapper initialValue={true}>
            <DarkModeSelector />
          </DarkModeWrapper>
        );
        cy.contains("Switch to lightmode");
      });
    });
    
    This is a good start, however we're only checking that the component is reacting properly to changes to the dark mode context, but how do you test that the button calls setDarkMode properly ?
    Cypress has an utility called "spy", you can find more info
    herehere 함수를 캡처한 다음 함수가 호출되었음을 어설션할 수 있습니다. 래퍼와 함께 구성 요소 테스트에서 어떻게 사용할 수 있는지 살펴보겠습니다.

    DarkModeSelector.cy.jsx

    const initDarkModeWrapper = () => {
      const setDarkModeSpy = cy.spy();
      const DarkModeWrapper = ({ children, initialValue }) => {
        const [darkMode, setDarkMode] = useState(initialValue);
        return (
          <DarkModeProvider
            value={{
              darkMode,
              setDarkMode: (value) => {
                setDarkMode(value);
                setDarkModeSpy(value);
              },
            }}
          >
            {children}
          </DarkModeProvider>
        );
      };
      return [setDarkModeSpy, DarkModeWrapper];
    };
    
    describe("<DarkModeSelector />", () => {
      it("mounts with a wrapper and a spy", () => {
        const [setDarkModeSpy, DarkModeWrapper] = initDarkModeWrapper();
        cy.mount(
          <DarkModeWrapper initialValue={true}>
            <DarkModeSelector />
          </DarkModeWrapper>
        );
        cy.contains("Switch to lightmode");
      });
    });
    


    여기서 우리는 스파이를 정의하고 스파이를 호출하고 둘 다 반환하는 래퍼를 정의하는 함수를 정의합니다. 지금은 구성 요소가 이 래퍼로 마운트되는지 테스트하지만 스파이가 특정 값으로 호출될 것으로 예상하는 더 많은 테스트를 추가할 수 있습니다.

    it("changes context value to 'true' once clicked when initiated with darkmode=false value", () => {
      const [setDarkModeSpy, DarkModeWrapper] = initDarkModeWrapper();
      cy.mount(
        <DarkModeWrapper initialValue={false}>
          <DarkModeSelector />
        </DarkModeWrapper>
      );
      cy.get("button")
        .click()
        .then(() => expect(setDarkModeSpy).to.have.been.calledWith(true));
    });
    



    it("changes context value to 'false' once clicked when initiated with darkmode=true value", () => {
      const [setDarkModeSpy, DarkModeWrapper] = initDarkModeWrapper();
      cy.mount(
        <DarkModeWrapper initialValue={true}>
          <DarkModeSelector />
        </DarkModeWrapper>
      );
      cy.get("button")
        .click()
        .then(() => expect(setDarkModeSpy).to.have.been.calledWith(false));
    });
    


    그런 다음 버튼을 클릭하면 컨텍스트가 새 값으로 업데이트된다는 것을 상당히 확신할 수 있습니다!

    이 개념을 전체 페이지에 적용

    One of the advantages of Cypress Component Testing compared to E2E testing is its speed, it allows you to mount components and test them very quickly. In React, pages are components as well, so what's stopping you from testing pages just as we tested components in the last section ? It might get a little more complicated, but the same principles apply. Let's have an example:

    Most app have routing, localization, a css framework, and data fetching. So let's use react-i18next, react-router, talwindcss and react-query.

    You can find an example application with all of the files here 구현에 대해 자세히 설명하지는 않겠지만 다크 모드, 번역 및 몇 가지 API 호출을 처리하므로 필요한 사항을 시연할 수 있습니다. 일하다.

    간단한 마운트 테스트부터 시작하여 어떤 일이 발생하는지 살펴보겠습니다.

    ProductsByCategoryPage.cy.jsx

    import { ProductsByCategoryPage } from "../../src/pages/ProductsByCategoryPage";
    
    describe("<ProductsByCategoryPage />", () => {
      it("mounts", () => {
        cy.mount(<ProductsByCategoryPage />);
      });
    });
    


    결과:

    Error: useLoaderData must be used within a data router



    마지막 섹션에서 볼 수 있듯이 컨텍스트 공급자와 함께 사용되지 않는 후크와 관련된 많은 오류가 발생합니다. 이전처럼 래퍼를 만들거나 다른 접근 방식을 시도할 수 있습니다! 사용자 지정 마운트 명령을 빌드해 보겠습니다.
    사용자 정의 마운트 명령에 대한 추가 정보/예제를 찾을 수 있습니다here.

    cypress/support/component.jsx

    Cypress.Commands.add("mountApplication", (component, options = {}) => {
      return mount(<ApplicationStub>{component}</ApplicationStub>, options);
    });
    


    Cypress는 이미 사용자 지정 마운트 기능import { mount } from "cypress/react18";을 가져오므로 자체 마운트 명령에 사용할 것입니다. component.jscomponent.jsx로 이름을 바꿔야 합니다. 그렇지 않으면 파일에 jsx 코드가 있기 때문에 cypress가 실패합니다.

    또한 다음과 같이 테스트에 전달할 수 있는 몇 가지 옵션을 정의할 수 있습니다.

    cypress/support/component.jsx

    const mountApplicationDefaultOptions = {
      viewport: [1920, 1080],
      applicationStubProps: {
        darkMode: false,
        lang: "en",
        loader: () => {},
      },
    };
    
    Cypress.Commands.add("mountApplication", (component, options) => {
      const consolidatedOptions = {
        ...mountApplicationDefaultOptions,
        ...options,
      };
      cy.viewport(...consolidatedOptions.viewport);
      return mount(
        <ApplicationStub {...consolidatedOptions.applicationStubProps}>
          {component}
        </ApplicationStub>
      );
    });
    


    ApplicationStub 구성 요소에서 우리는 앱이 제대로 마운트되고 작동하는지 확인하는 데 필요한 모든 공급자와 CSS 파일을 가져와서 사용합니다. React 구성 요소이므로 가능성은 무한합니다.

    cypress/support/ApplicationStub.jsx

    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    import { useTranslation } from "react-i18next";
    import { createMemoryRouter, RouterProvider } from "react-router-dom";
    import { App } from "../../src/App";
    import "../../src/i18n";
    import "../../src/index.css";
    
    export const ApplicationStub = ({ children, darkMode, lang, loader }) => {
      const router = createMemoryRouter([
        {
          path: "/",
          element: <App darkModeParameter={darkMode} />,
          children: [
            { path: "/", element: children },
            {
              path: "/:category",
              element: children,
              loader,
            },
          ],
        },
        ,
      ]);
      const { i18n } = useTranslation();
      if (lang) {
        i18n.changeLanguage(lang);
      }
      const queryClient = new QueryClient();
      return (
        <QueryClientProvider client={queryClient}>
          <RouterProvider router={router} />
        </QueryClientProvider>
      );
    };
    


    여기에서 전체 페이지를 쉽게 마운트하고 다크 모드 또는 언어와 같은 기본 인수를 전달할 수 있습니다.

    ProductsByCategoryPage.cs.jsx

    import {
      loader,
      ProductsByCategoryPage,
    } from "../../src/pages/ProductsByCategoryPage";
    
    describe("<ProductsByCategoryPage />", () => {
      it("mounts", () => {
        cy.mountApplication(<ProductsByCategoryPage />, {
          applicationStubProps: { loader },
        });
        cy.contains("Switch");
      });
      it("mounts with darkMode", () => {
        cy.mountApplication(<ProductsByCategoryPage />, {
          applicationStubProps: { darkMode: true, loader },
        });
        cy.get("body").should("have.class", "bg-gray-800");
      });
      it("mounts with french language", () => {
        cy.mountApplication(<ProductsByCategoryPage />, {
          applicationStubProps: { lang: "fr", loader },
        });
        cy.contains("Passer au darkmode");
      });
    });
    


    테스트 중에 페이지가 API를 호출한다는 것을 알았을 수도 있습니다. 괜찮을 수도 있지만 API가 실패하고 E2E 테스트 중에 웹 앱과 API를 모두 테스트하면 구성 요소 테스트가 중단될 수 있습니다. 따라서 이러한 테스트가 웹앱에 의해서만 영향을 받도록 API 호출을 스텁하는 방법을 추가할 것입니다. 이를 위해 Cypress'intercept를 사용합니다.

    테스트에서 이러한 가로채기를 직접 사용할 수 있습니다.

    it("intercepts /products/categories calls", () => {
      cy.intercept("GET", "/products/categories", {
        statusCode: 200,
        body: ["electronics", "jewelery", "men's clothing", "women's clothing"],
      });
      cy.mountApplication(<ProductsByCategoryPage />, {
        applicationStubProps: { loader },
      });
    });
    


    그러나 큰 페이지에서는 상당히 반복될 수 있으므로 beforeEach 핸들러에도 추가할 수 있습니다.

    describe("<ProductsByCategoryPage />", () => {
      beforeEach(() => {
        cy.intercept("GET", "/products/categories", {
          statusCode: 200,
          body: ["electronics", "jewelery", "men's clothing", "women's clothing"],
        });
        cy.intercept("GET", "/products", {
          statusCode: 200,
          body: [
            {
              id: 1,
              title: "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
              price: 109.95,
              description:
                "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
              category: "men's clothing",
              image: "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
              rating: { rate: 3.9, count: 120 },
            },
            ... // there might be a lot of products to stub
            {
              id: 20,
              title: "DANVOUY Womens T Shirt Casual Cotton Short",
              price: 12.99,
              description:
                "95%Cotton,5%Spandex, Features: Casual, Short Sleeve, Letter Print,V-Neck,Fashion Tees, The fabric is soft and has some stretch., Occasion: Casual/Office/Beach/School/Home/Street. Season: Spring,Summer,Autumn,Winter.",
              category: "women's clothing",
              image: "https://fakestoreapi.com/img/61pHAEJ4NML._AC_UX679_.jpg",
              rating: { rate: 3.6, count: 145 },
            },
          ],
        });
      });
    


    또는 cypress/fixtures에서 json 고정 파일을 생성하고 이를 사용하여 인터셉트를 선언할 수 있습니다.

    describe("<ProductsByCategoryPage />", () => {
      beforeEach(() => {
        cy.intercept("GET", "/products/categories", {
          statusCode: 200,
          body: ["electronics", "jewelery", "men's clothing", "women's clothing"],
        });
        cy.intercept("GET", "/products", {
          statusCode: 200,
          fixture: "products.json",
        });
      });
      it("intercepts all the calls", () => {
        cy.mountApplication(<ProductsByCategoryPage />, {
          applicationStubProps: { loader },
        });
      });
    });
    


    이 기사가 도움이 되었기를 바라며 Cypress Component Testing을 배우면서 겪었던 몇 가지 장애물을 컴파일하려고 했습니다. 예제 앱은 완벽하지는 않지만 이 도구를 사용하여 구성 요소 및 전체 페이지를 테스트하는 방법에 대한 아이디어를 얻는 데 도움이 됩니다.

    이 문제를 이해하는 데 도움을 주신 Cypress 팀에 감사드립니다. Discord에 대한 토론에 참여하고 모든 문서here를 살펴볼 수 있습니다.

    Github 및 다음에서 나를 찾을 수 있습니다.

    좋은 웹페이지 즐겨찾기