Cypress 및 페이지 개체 패턴 - EndToEnd 테스트를 위한 모범 사례

이 기사에서는 명명 규칙과 함께 페이지 개체 패턴을 조정하여 테스트를 작성하는 동안 많은 시간을 절약할 수 있는 방법을 보여줍니다. 먼저 이름 지정 규칙으로 시작한 다음 페이지 개체 패턴을 유리하게 사용할 수 있는 방법을 알아봅니다. Have a look at the end result of the tests .

속성 이름과 일관성 유지

This is pretty much a no-brainer, but becomes very important the bigger the project is. If you utilize your naming properly, you will be able to abstract your tests in a very neat way. For instance, you can build selectors based on your component names and actions. What I often do is:

<button class="registration-button"
data-test="registration-page__button-start">Register</button>

or

<button class="open-settings"
data-test="settings-page__button-open">Settings</button>

so I follow these conventions all the time, depending on the project:

// it is just important that you are consistent with it and have a system in place
<page-name>__<element-type>-<semantic-action>
or even
<page-name>-<component-name>__<element-type>-<semantic-action>
or just
<component-name>__<element-type>-<semantic-action>

This helps me to unify selectors and I do not even need to look 👀 at my markup anymore. The first example button is starting the registration process, hence start , the second button is opening a widget/components, hence open . I would recommend that you take the time and define your semantic action and properly communicate them with your team. Good testing starts with a solid naming convention.

If you do this you can save so much time, have a look at the next section where I show how you can utilize this even more.

테스트를 재사용 가능한 구조로 분할



따라서 코딩의 모든 것에서 일단 코드를 복사하기 시작하면 함수를 작성하는 것이 더 나을 수 있습니다. 그.) 코드를 의미론적 논리적 청크로 분할하는 것은 유지 관리를 위해 많은 의미가 있습니다. 그리고 종단 간 테스트를 작성하는 데 시간을 할애하려는 경우 논리를 공유하지 않는 대규모 프로젝트가 있는 경우 10배 더 많은 시간을 소비하게 됩니다.

내가 본 두 가지 주요 패턴이 있으며 특정 앱에 대해 어느 정도 의미가 있습니다. 하나는 페이지 개체 패턴이고 다른 하나는 함수 개체 패턴입니다(적어도 저는 이렇게 부릅니다 😅). 그러나 이 기사에서는 페이지 개체 패턴만 보여줍니다. 더 많은 기능 기반 접근 방식에 관심이 있는 경우 알려주십시오. 😎

페이지 개체 패턴



페이지 개체 패턴은 의미론적으로 페이지 요소를 클래스로 그룹화하는 방법입니다. 소규모 프로젝트에 복잡성 계층을 추가할 수 있기 때문에 눈살을 찌푸리는 경우가 많습니다. 그러나 클래스는 특히 프로젝트가 커지면 공간이 있습니다. 많은 시간을 절약하고 가독성을 향상시키는 방법으로 수업을 활용하는 방법을 보여 드리겠습니다.

양식을 나타내는 두 개의 구성 요소가 있고 둘 다 열 수 있고 제출된 단추와 오류 메시지가 있다고 가정해 보겠습니다. 따라서 구성 요소를 보지 않고도 테스트가 수행해야 할 작업을 이미 상상할 수 있습니다.
  • 구성 요소를 열거나(닫혀 있는 경우) 간단히 스크롤합니다
  • .
  • 잘못된 데이터로 필수 입력을 입력하십시오
  • .
  • 오류 메시지가 표시되는지 제출하고 평가합니다
  • .
  • 올바른 데이터를 입력하세요.
  • 성공 메시지 제출 및 평가

  • 사실 거의 모든 폼 구성 요소가 이 작업을 수행합니다! 따라서 다음과 같이 모든 것을 기본 클래스로 추상화할 수 있습니다.

    /**
     * The Widget Test Interaction Page Object - abstract class!!
     */
    class FormPageObject {
      defaultOpenSelector = '';
      defaultSubmitUrl = '';
      defaultSubmitButtonSelector = '';
      defaultInterceptName = '';
      defaultSelectorForStringInputChange = '';
      defaultCloseSelector = '';
      defaultMessageSelector = '';
    
      /**
       * Opens the widget
       */
      open(
        selector = this.defaultOpenSelector,
        options = {},
        inputSelector = ''
      ) {
        return cy
          .$(selector, options, inputSelector)
          .click({
            force: true,
          })
          .scrollIntoView({ offset: { top: -100, left: 0 } });
      }
    
    
      /**
       * Close the  widget
       */
      close(
        selector = this.defaultCloseSelector,
        options = {},
        inputSelector = ''
      ) {
        return cy.$(selector, options, inputSelector).click({
          force: true,
        });
      }
    
    
      /**
       * The intercept will register with the default name and url and will look for the next request
       * hence you can wait for it. By default it is expecting that the response will work.
       * You can also force a success true
       */
      interceptSubmit(
        forceSuccess = false,
        forceFailure = false,
        shouldSucceed = false,
        inputUrl = '',
        interceptName = this.defaultInterceptName,
      ) {
        const url = inputUrl ?? this.defaultSubmitUrl;
        cy.intercept('POST', url, (req) => {
          req.continue((res) => {
            if (forceSuccess) {
              res.body.success = true;
            } else if (forceFailure) {
              res.body.success = false;
            } else if (!res.body.success && shouldSucceed) {
              expect(
                res.body.success,
                'It was not possible to change the the widget and it was expected that it would be possible'
              ).to.be.true;
            }
            return res;
          });
        }).as(interceptName);
      }
    
    
      updateStringInput(
        input = '',
        selector = this.defaultSelectorForStringInputChange
      ) {
        let newString = '';
        if (input === '') {
          return cy.stringInput(selector, input);
      }
    
    
    
      submit(
        selector = this.defaultSubmitButtonSelector,
        options = {},
        inputSelector = ''
      ) {
        cy.$(selector, options, inputSelector).click({
          force: true,
        });
      }
    
    
    
      submitValidator(req, messageSelector = this.defaultMessageSelector) {
        cy.isRequestValid(req);
        const correctClass = req.response.body.success
          ? 'alert-success'
          : 'alert-danger';
        cy.checkForClass(
          correctClass,
          messageSelector,
          `The submit request returned ${req.response.body.success}, but the message had not this class ${correctClass}`
        );
      }
    }
    


    코드를 추상화하고 cy.$ 또는 cy.stringInput 와 같은 나만의 도우미 메서드를 사용했습니다. (만약 내가 헬퍼 메소드를 어떻게 구축하는지 관심이 있다면 댓글로 알려주십시오.) 이 추상 클래스의 좋은 점은 이제 다음과 같이 사용할 수 있다는 것입니다.

     */
    
    class FormComponentA extends FormPageObject {
      defaultOpenSelector = 'form-componentA__button--open';
      defaultSubmitUrl = /regexForSubmitUrl/;
      defaultSubmitButtonSelector = 'form-componentA__button--submit';
      defaultInterceptName = 'formComponentASubmit';
      defaultSelectorForStringInputChange = 'form-componentA__input-text--type';
      defaultMessageSelector = 'form-componentA__message'
    }
    
    class FormComponentB extends FormPageObject {
      defaultOpenSelector = 'form-componentB__button--open';
      defaultSubmitUrl = /regexForSubmitUrl/;
      defaultSubmitButtonSelector = 'form-componentB__button--submit';
      defaultInterceptName = 'formComponentBSubmit';
      defaultSelectorForStringInputChange = 'form-componentB__input-text--type';
      defaultMessageSelector = 'form-componentB__message'
    
      // this component needs some custom interaction, e.g. a slider
      useSlider(percentage = 0, selector = 'default-selector-for-slider') {
        cy.moveSlider(selector, percentage);
      }
    }
    
    


    JumpToNamingConvention

    이제 :
    
    describe('test-for-component-A', () => {
      it('should fail and display error message', () => {
        const component = new FormComponentA();
        component.open();
       
        // e.g. update name input
        component.updateStringInput('Max');
      
        // set up the interceptor and force it to fail
        component.interceptSubmit(false, true);
      
        // hit the submit button
        component.submit();
       
        // wait for the request and evaluate the response
        cy.wait(component.defaultInterceptName).then((req) => {
          component.submitValidator(req);
        });
      });
    
    
    
      it('should succeed and display success message', () => {
        const component = new FormComponentA();
        component.open();
    
        // set up the interceptor and force it to succeed
        component.interceptSubmit(true);
    
        component.updateStringInput('Max');
        component.submit();
        cy.wait(component.defaultInterceptName).then((req) => {
          component.submitValidator(req);
        });
      });
    });
    

    Our tests are now very good readable 😇. I think anyone can now understand what the test is actually doing. Readable code is the first step of creating maintainable code! The cool thing is, if we want to create a test for FormComponentB we can very quickly do so!

    Coming back to my earlier statement

    You should use a solid naming convention for your tests attributes can really help you utilize the power of this approach.

    Because you can also do stuff like this in your base class(es):

      constructor(componentName, interceptRegex) {
        this.defaultOpenSelector = `.${componentName}__button-open`;
        this.defaultSubmitUrl = interceptRegex;
        this.defaultSubmitButtonSelector = `.${componentName}__button--submit`;
        this.defaultInterceptName = `${componentName}Intercept`;
        this.defaultSelectorForStringInputChange = `.${componentName}__input-text--type`;
        this.defaultMessageSelector = `.${componentName}-message`;
      }
    

    So now you do not even need to write a new class and can use the abstract FormClass directly. 🔥🔥🔥

    const component = new FormPageObject('form-componentB');
    
    JumpToTop 과 같은 실제 테스트 로직을 작성할 수 있습니다.

    알려주세요 🚀🚀🚀



  • 위에 쓰여진 것과 관련하여 도움이 필요하십니까?

  • 첫 번째 이미지는 무엇입니까? 😄
  • 개선할 수 있다고 생각하십니까? - 그러면 알려주세요

  • 기사가 마음에 드셨나요? 🔥
  • 좋은 웹페이지 즐겨찾기