Cypress 및 유닛 테스트에 고정장치를 사용하지 마십시오. 공장 사용

단원 테스트가 좋아요...그들이 믿음직스럽게 일할 때!사실 한 번의 엉터리 테스트가 아예 없는 테스트보다 더 나쁘다는 말이 있다.나는 수주 동안 무작위로 추적하는'가짜음성'테스트가 비효율적이라는 것을 증명할 수 있다.반대로, 당신은 이 시간을 이용하여 사용자를 돕는 업무 코드를 작성할 수 있습니다.
그래서 취약하지 않은 테스트를 작성하는 가장 간단한 기술 중 하나인 테스트 데이터 공장에 대해 이야기해 봅시다.
그러나 공장 함수가 무엇인지, 왜 그것을 사용해야 하는지 토론하기 전에 공장 함수가 제거하는 아삭아삭한 테스트의 유형을 알아보자.

저희가 피하고 싶은 테스트는요.
  • 긴밀 결합
  • 유형 안전성 결여
  • 대형 클램프 폴더
  • 공장 기능은 이 모든 문제를 해결할 것이다.

    그럼 공장 기능은 무엇입니까?
    공장 함수는 창설 대상의 함수다.이렇게 간단해.네,'추상적인 공장'모델이 있는데 몇 십 년 전에 4인방의 디자인 모델서에 의해 보급되었습니다.우리 그것을 간단명료하게 합시다.
    우리가 함수를 만들어서 물건을 만드는 것을 더욱 쉽게 하자. 그러면 우리는 더욱 쉽게 테스트를 진행할 수 있다.
    다음은 세계에서 가장 간단한 예이다.
    interface ISomeObj {
      percentage: string;
    }
    
    export const makeSomeObj = () => {
      return {
        percentage: Math.random()
      };
    }
    
    우리가 위에서 설명한 아삭아삭한 테스트의 각 방면을 복원하기 위해 이런 간단한 모델을 어떻게 사용하는지 봅시다.
    우리는 테스트가 통상적으로 어떻게 작성되었는지 설명하는 것부터 시작해서 모든 문제를 해결할 때 해결 방안을 반복적으로 발전시킬 것이다.

    아삭아삭한 테스트가 어떻게 발생했는지의 실제 예
    모든 것은 무고함에서 시작된다.당신이나 팀의 다른 동력 있는 개발자는 앞당겨 돈을 지불하고 그 중 한 페이지에 단원 테스트를 추가하기를 원합니다.함수를 테스트하려면 일부 테스트 데이터를 JSON 파일에 저장합니다.Cypress(본문을 작성할 때 최고의 UI 테스트 라이브러리)는 테스트 데이터 클러치 JSON 파일을 사용하도록 권장합니다.그런데 문제는...그것은 심지어 원격 유형이 안전하지 않다.그래서 너는 JSON에 오류를 입력한 후에 몇 시간 동안 이 문제를 해결할 수 있다.
    이 점을 설명하기 위해 예시 업무 코드와 테스트 자동화 코드를 살펴보자.이 예들 중 대다수에 대해, 우리는 당신이 보험회사에서 일한다고 가정하고, 이 회사는 이 규칙들이 미국 각 주에 어떻게 적용되는지 설명했다.
    // This file is "src/pages/newYorkInfo.tsx"
    import * as React from 'react';
    
    interface IUser {
        state: string;
        address: string;
        isAdmin: boolean;
        deleted: boolean | undefined;
    }
    
    export const NewYorkUserPage: React.FunctionComponent<{ user: IUser }> = props => {
        if (props.user.state === 'NY' && !props.user.deleted) {
            const welcomeMessage = `Welcome`;
            return <h1 id="ny-dashboard">{welcomeMessage}</h1>;
        } else {
            return <div>ACCESS DENIED</div>;
        }
    };
    
    코드가 괜찮아 보이기 때문에 양성 테스트 용례를 저장하기 위해 JSON을 작성합시다.
    // fixtures/user.json
    {
        state: 'NY',
        isAdmin: true,
        address: '55 Main St',
    }
    
    테스트 코드입니다.이 문제를 설명하기 위해 Cypress 테스트에 사용되는 psuedo 코드를 사용할 것입니다. 그러나 fixture를 불러오고 단언을 실행하는 모든 테스트 코드에서 이런 상황이 발생할 것이라고 상상할 수 있습니다.
    // When the UI calls the user endpoint, return the JSON as the mocked return value
    cy.route('GET', '/user/**', 'fixture:user.json');
    cy.visit('/dashboard');
    cy.get('#ny-dashboard').should('exist')
    
    그것은 다른 사용자와 관련된 또 다른 장면을 테스트하기 전에 아주 잘 작동하는 것처럼 보였다.그럼 어떡해?

    엉터리 해결 방안 - 파일 하나가 유효하면 JSON 파일을 계속 만듭니다
    다른 JSON fixture 파일을 생성해야 합니까?불행하게도, 이런 간단한 해결 방안은 줄곧 발생하고 있다. 왜냐하면 그것은 가장 간단하기 때문이다.하지만 사례 수가 늘어나면서 JSON 파일 수도 늘어나고 있다.미국 사용자의 모든 페이지를 테스트하기 위해서는 52개의 다른 JSON 파일이 필요합니다.사용자가 관리자인지 테스트를 시작할 때 104개의 파일을 만들어야 합니다.서류가 너무 많아요!
    하지만 당신은 여전히 유형 안전에 문제가 있습니다.예를 들어 제품 담당자가 팀에 와서 "우리가 사용자를 환영할 때 그들의 이름을 표시할 수 있도록 친절하게 하고 싶다"고 말했다.
    따라서 name 속성을 인터페이스에 추가하고 이를 처리하기 위해 UI를 업데이트합니다.
    // This file is "src/pages/newYorkInfo.tsx"
    import * as React from 'react';
    
    interface IUser {
        name: string;
        state: string;
        address: string;
        isAdmin: boolean;
        deleted: boolean | undefined;
    }
    
    export const NewYorkUserPage: React.FunctionComponent<{ user: IUser }> = props => {
        if (props.user.state === 'NY' && !props.user.deleted) {
            const welcomeMessage = `Welcome ${props.user.name.toLowerCase()}!`;
            return <h1 id="ny-dashboard">{welcomeMessage}</h1>;
        } else {
            return <div>ACCESS DENIED</div>;
        }
    };
    
    비즈니스 코드를 업데이트하게 되어 기쁩니다. 하지만 fixture JSON은 이미 유행이 지났습니다.fixture JSON에는 name 속성이 없으므로 다음 오류가 발생합니다.
    Uncaught TypeError: Cannot read property 'toLowerCase' of undefined
    
    현재 52명의 사용자 JSON fixture 파일에 name 속성을 추가해야 합니다.우리는 Typescript로 이 문제를 해결할 수 있다.

    좀 더 나은 솔루션 - TypeScript 파일로 이동
    JSON을 fixture 파일에서 .ts 파일로 이동하면 Typescript 컴파일러가 오류를 찾습니다.
    // this file is "testData/users"
    import {IUser} from 'src/pages/newYorkInfo';
    
    // Property 'name' is missing in type '{ state: string; isAdmin: true; address: string; deleted: false; }' but required in type 'IUser'.ts(2741)
    export const generalUser: IUser = {
        state: 'NY',
        isAdmin: true,
        address: '55 Main St',
        deleted: false,
    };
    
    우리는 이 새로운 대상을 사용하기 위해 테스트 코드를 업데이트할 것이다.
    import { generalUser } from 'testData/users';
    
    // When the UI calls the user endpoint, return the JSON as the mocked return value
    cy.route('GET', '/user/**', generalUser);
    cy.visit('/dashboard');
    cy.get('#ny-dashboard').should('exist')
    
    타자 원고 감사합니다!name: 'Bob Smith' 대상에 generalUser를 추가하여 컴파일러 오류를 해결하면 코드는 매우 깨끗하게 컴파일됩니다. 가장 중요한 것은...너의 시험이 또 통과되었구나!
    유형 안전을 실현함으로써 당신은 이미 우리의 세 가지 목표 중의 하나를 실현하였습니다.불행하게도, 긴축 결합 문제는 여전히 존재한다.
    예를 들어 단원 테스트에 익숙하지 않은 개발자가 나타날 때 어떤 일이 일어날지.그들이 생각하는 것은 단지 삭제된 사용자와 관련된 기능을 테스트해야 한다는 것이다.그래서 그들은 deleted: falsegeneralUser 대상에 추가했다.
    카룸!너의 테스트가 실패했어, 그들의 테스트가 통과했어.이것이 바로 긴밀하게 결합된 의미다.
    이 때문에 개발자들은 몇 분(또는 몇 시간) 동안 디버깅을 했고 두 테스트가 같은 설정 데이터를 공유한다는 것을 깨달았다.따라서 개발자들은 테스트마다 하나의 대상을 가지기 위해 이전의 간단한 해결 방안을 사용한다.이것은 곧 통제력을 잃을 수도 있다. 나는 5000행장의 테스트 데이터 파일을 본 적이 있다.
    여기를 클릭하여 이게 얼마나 미친 짓인지 보세요.
    // this file is "testData/users"
    import {IUser} from 'src/pages/newYorkInfo';
    
    export const nonAdminUser: IUser = {
        name: 'Bob',
        state: 'NY',
        isAdmin: false,
        address: '55 Main St',
        deleted: false,
    };
    
    export const adminUser: IUser = {
        name: 'Bob',
        state: 'NY',
        isAdmin: true,
        address: '55 Main St',
        deleted: false,
    };
    
    export const deletedAdminUser: IUser = {
        name: 'Bob',
        state: 'NY',
        isAdmin: true,
        address: '55 Main St',
        deleted: true,
    };
    
    export const deletedNonAdmin: IUser = {
        name: 'Bob',
        state: 'NY',
        isAdmin: false,
        address: '55 Main St',
        deleted: true,
    };
    
    // and on and on and on again...
    
    더 좋은 방법이 있을 거야.

    좋은 솔루션: 공장 기능
    그렇다면 우리는 어떻게 방대한 대상 파일을 재구성합니까?우리는 그것을 하나의 기능으로 만들었다.
    // src/factories/user
    import faker from 'faker';
    import {IUser} from 'src/pages/newYorkInfo';
    
    export const makeFakeUser = (): IUser => {
        return {
            name: faker.name.firstName() + ' ' + faker.name.lastName(),
            state: faker.address.stateAbbr(),
            isAdmin: faker.random.boolean(),
            address: faker.address.streetAddress(),
            deleted: faker.random.boolean(),
        }
    }
    
    현재, 모든 테스트는 사용자를 만들 때 호출할 수 있습니다 deletedUser.
    가장 좋은 것은 공장에서 모든 물건을 무작위로 바꾸는 것이다. 이것은 단독의 테스트가 이 기능을 가지고 있지 않다는 것을 분명히 한다.테스트가 특수한 IUser인 경우 나중에 직접 수정해야 합니다.
    이것은 매우 쉽게 할 수 있다.삭제된 사용자 테스트를 상상해 봅시다. 사용자의 이름이나 어떤 것도 신경 쓰지 않습니다.우리는 그것들이 삭제되는 것만 관심을 갖는다.
    import { makeFakeUser } from 'src/factories/user';
    import {IUser} from 'src/pages/newYorkInfo';
    
    // Arrange
    const randomUser = makeFakeUser();
    const deletedUser: IUser = { ...randomUser, ...{
      deleted: true
    };
    cy.route('GET', '/user/**', deletedUser);
    
    // Act
    cy.visit('/dashboard');
    
    // Assert
    cy.find('ACCESS DENIED').should('exist')
    
    나에게 있어서 이런 방법의 묘미는 그것을 스스로 기록할 수 있다는 데 있다.이 테스트 코드를 보는 모든 사람은 API가 삭제된 사용자를 반환할 때 페이지에서 "접근 거부"를 찾아야 한다는 것을 알아야 한다.
    나는 우리가 그것을 더욱 깨끗하게 할 것이라고 생각한다.

    최적의 솔루션: MergeParty를 사용하여 간편하게 덮어쓰기
    위의 확장 연산자를 사용하면 작은 대상이기 때문에 받아들일 수 있습니다.그러나 이렇게 깊게 내포된 객체일 경우 더 짜증이 날 수 있습니다.
    interface IUser {
        userName: string;
        preferences: {
            lastUpdated?: Date;
            favoriteColor?: string;
            backupContact?: string;
            mailingAddress: {
                street: string;
                city: string;
                state: string;
                zipCode: string;
            }
         }
    }
    
    너는 정말 수백 수천 개의 물체를 사방으로 떠다니게 하고 싶지 않다.
    따라서 만약 우리가 사용자가 원하는 것만 덮어쓸 수 있도록 허락한다면, 우리는 매우 간단하고 무미건조한 설정 코드를 만들 수 있다.상상해 보아라, 매우 구체적인 테스트가 있는데, 반드시 메인 스트리트에 사는 사용자가 있어야 한다
    const userOnMainSt = makeFakeUser({
        preferences: {
            mailingAddress: {
                street: 'Main Street'
            }
        }
    });
    
    와, 그들은 다른 7개의 속성이 아닌 테스트에 필요한 내용을 지정하기만 하면 된다.우리는 일회용 대상을 어떤 거대한 테스트 파일에 저장할 필요가 없다.우리도 자기 평가의 목표에 도달했다.
    우리는 어떻게 이런 부분의 커버를 지원하기 위해 makeFakeUser() 기능을 강화합니까?mergePartially 라이브러리가 얼마나 쉽게 이 점을 할 수 있는지 (완전 공개: 저는 makeFakeUser 관리자입니다).
    const makeFakeUser = (override?: NestedPartial<IDeepObj>): IDeepObj => {
            const seed: IDeepObj = {
              userName: 'Bob Smith',
              preferences: {
                mailingAddress: {
                  street: faker.address.streetAddress(),
                  city: faker.address.city(),
                  state: faker.address.stateAbbr(),
                  zipCode: faker.address.zipCode(),
                },
              },
            };
            return mergePartially.deep(seed, override);
          };
    
    최종 테스트 코드가 얼마나 깨끗해 보이는지 봅시다.여러 줄 코드가 저장되어 있으며 매번 설정된 데이터가 새롭다는 것을 알 수 있습니다.
    import { makeFakeUser } from 'src/factories/user';
    import {IUser} from 'src/pages/newYorkInfo';
    
    // Arrange
    const deletedUser= makeFakeUser({
      deleted: true;
    });
    cy.route('GET', '/user/**', deletedUser);
    
    // Act
    cy.visit('/dashboard');
    
    // Assert
    cy.find('ACCESS DENIED').should('exist')
    

    마무리
    테스트 코드를 취약하고 방대한 테스트 코드에서 미소하고 독립된 테스트 코드로 바꾸는 방법을 읽어 주셔서 감사합니다.
    나는 너의 이런 방법에 대한 견해를 매우 듣고 싶다.

    좋은 웹페이지 즐겨찾기