농담으로 비웃다

오늘 나는 한 동료가 그들의 구성 요소를 위한 단원 테스트를 만드는 것을 도왔다.이 응용 프로그램은react,runningon parcel,jest와 효소의 조합으로 구성되어 테스트에 사용됩니다.우리가 리셋 방법을 사용하여 다른 구성 요소에 전달되는 구성 요소를 찾기 전에 모든 것이 순조롭다.리셋을 호출하면 첫 번째 구성 요소의 결과가 변경됩니다.그때까지 나는 단원 테스트를 어떻게 구축하고 무엇을 찾으며 시뮬레이션을 어떻게 처리하는지에 대한 내 지식을 깊이 이해할 수 있었지만 이런 상황은 그리 간단하지 않았다.인터넷에서 예를 발굴하는 동시에 우리는 답을 찾았습니다. 지금 저는 여러분과 결과를 공유하고 싶습니다.
실제 코드를 시작하기 전에 본고는react,jest,enzyme의 기본 지식을 이해하시기 바랍니다.나는 그것의 일부 기본적인 부분을 설명할 것이지만, 전체 기지는 아니다. 왜냐하면 이것은 상당히 큰 범위이기 때문이다.나는 https://codesandbox.io/에서 시연을 만들기 시작했지만jest와 관련된 문제에 부딪혔다.mock이 지원되지 않았습니다.그래서 이 프레젠테이션을 로컬에서 만들어서github에 놓았습니다. https://github.com/JHotterbeekx/jest-mock-with-callback-demo 에서 찾을 수 있습니다.

답전
우선 리콜이 무엇인지 간단명료하게 설명해 봅시다.나는 네가 이 방법들을 잘 알고 있다고 가정한다면, 우리는 이 기본적인 방법을 예로 들자.
function doSomething(argument) {
  console.log(argument);
}
doSomething("I'm awesome")
이게 무슨 소용이야?그것은 문자열을 콘솔에 잘 쓴다.들어오는 인자가 문자열이라고 가정하면, 이 문자열을 컨트롤러에 기록합니다.사실 너는 무엇이든 그것을 전달할 수 있다. 그것은 그것을 컨트롤러에 쓸 것이다.그런데 만약에 우리가 한 가지 방법을 통과한다면?한번 해보자.
function doSomething(argument) {
  console.log(argument);
}

function doAnotherThing() {
  console.log("I'm awesome");
}

doSomething(doAnotherThing);
컨트롤러가 지금 뭘 알려줄까?
function doAnotherThing() {
  console.log("I'm awesome");
}
그래, 일리가 있는 것 같아.한 방법을 다른 방법에 전달했기 때문에, 이 파라미터를 기록하면 이 방법을 보여 줍니다.그런데 잠깐만, 내가 이 방법을 쓰면?
function doSomething(argument) {
  argument();
}

function doAnotherThing() {
  console.log("I'm awesome");
}

doSomething(doAnotherThing);
컨트롤러가 지금 뭘 알려줄까?
I'm awesome
뭐?이게 얼마나 좋아요?우리는 하나의 방법을 다른 방법에 전달하고, 이 방법은 우리가 전달하는 방법을 반대로 사용한다.저희가 더 잘 만들 수 있을까요?네, 할 수 있어요. 이것 봐요.
function doSomething(argument, whatYouAre) {
  argument(whatYouAre);
}

function doAnotherThing(whatIAm) {
  console.log("I'm " + whatIAm);
}

doSomething(doAnotherThing, "even more awesome");
컨트롤러가 지금 뭘 알려줄까?
I'm even more awesome
우리는 그것을 더욱 훌륭하게 만들었다.너는 우리가 거기에서 무엇을 했는지 보았니?우리는 이 방법을 전달했을 뿐만 아니라, 추가 매개 변수도 전달했는데, 이 매개 변수는 나중에 이 방법으로 전달되었다.당신은 방금 리셋 방법의 기본 개념을 이해했습니다.나는 네가 생각하는 것을 들었다. "그러나 이것은 나에게 아무런 의미가 없다. 너는 왜 이렇게 하느냐?"위의 예는 그것을 쉽게 읽을 수 있도록 설정된 것이지만, 그다지 의미가 없는 것 같다.내가 너에게 더욱 진실한 예를 하나 보여 주겠다.DoSomething에서 API를 호출했다고 가정하면 호출이 완료되면 결과를 해석하고 결과를 사용하여 콜백 방법을 호출합니다.현재, 리셋 방법을 전송하고 그 구성 요소를 정의하면, 이 방법의 내용을 통해 그것을 처리할 것입니다.따라갈 수 있겠어?네가 그것을 볼 수 있을 때, 그것은 항상 더 쉽다. 우리가 그것을 해결하자.

실제 용례
좋아, 만약 우리가 응용 프로그램이 하나 있다면, 그것은 두 개의 구성 요소가 있을 것이다.먼저 DataDisplayer가 DataRetriever에서 검색한 결과를 보여줍니다.그러나 이 검색기는 비동기적으로 작동하기 때문에 결과만 돌려보낼 수 없다.이 점을 해낼 수 있는 몇 가지 방법이 있지만, 본 예에서 우리는 리셋 방법을 사용할 것이다.코드에 주석을 추가해서 DataDisplayer를 설명하려고 했습니다.
import React from "react";
import DataRetriever from "./DataRetriever";

export default class DataDisplayer extends React.Component {
  constructor(props) {
    super(props);

    // We initialize the state with a property that contains a boolean telling us if data is
    // available, which will be set to 'true' once the callback method is called. And a data
    // property which will be filled on callback containing a string with a title.
    this.state = {
      dataAvailable: false,
      data: null
    };
  }

  // We use the componentDidMount to trigger the retrieval of the data once the component is
  // mounted. Which means the component first mounts with its default state and than triggers
  // this method so data is retrieved.
  componentDidMount() {
    // We create a new instance of data retriever and call the retrieve method. In this
    // retrieve method we pass a so-called callback method as a parameter. This method will
    // be called inside the retrieve method. As you can see the method expects a title parameter
    // which it will set on the data property in the state and also setting the dataAvailable
    // property to true;
    new DataRetriever().Retrieve(title => {
      this.setState({
        dataAvailable: true,
        data: title
      });
    });
  }

  // This render method will initially render the text 'Data not available', because in the 
  // initial state the property dataAvailable is false. Once data is retrieved and the callback
  // method has been called the state will update, which triggers a re-render, so the render
  // is executed again. Now the dataAvailable will be true and the content in data will be shown.
  render() {
    if (!this.state.dataAvailable) return <div>Data not available</div>;
    return (
      <div>
        Data value: <strong>{this.state.data}</strong>
      </div>
    );
  }
}

네, 페이지의 기본 기능을 보여 드리겠습니다.그것은 '데이터 사용 불가' 로 페이지를 보여 줍니다.구성 요소를 불러올 때, 검색기에 대한 호출을 터치하고, 리셋 방법을 전달합니다.이 리셋 방법을 사용하면 검색기의 결과를 가져오고 상태를 설정하며 검색한 제목을 표시하는 구성 요소를 다시 보여 줍니다.
이제 DataRetriever를 살펴보겠습니다. 리셋 방법은 바로 이곳에서 전달됩니다.
export default class DataRetriever {

  // This demo method calls an open API, then translates the response to JSON. Once that is done
  // it calls the passed in callbackMethod with the title property as parameter. So when the API
  // gives us { title: 'myTitle' }, the code will perform callbackMethod('myTitle')
  Retrieve(callbackMethod) {
    fetch("https://jsonplaceholder.typicode.com/todos/1")
      .then(response => {
        return response.json();
      })
      .then(responseJson => callbackMethod(responseJson.title));
  }
}

이것은 웹 사이트에서 jsonplaceholder를 호출하는 API 호출입니다.이 결과는 JSON 객체로 해석된 다음 객체의 제목을 매개 변수로 콜백 방법을 호출합니다.이제 의미 있죠?좋아요. 그런데 이걸 어떻게 테스트할 거예요?우리 깊이 토론합시다.

리셋 시뮬레이션을 사용하여 테스트를 진행하다
우리는 왜 또 비웃어야 합니까?우리는 단원 테스트를 작성하고 있습니다. 단원 테스트의 문제는 그들이 단원을 테스트하기를 바라는 것입니다.이 경우 어셈블리는 하나뿐입니다.DataDisplayer만 호출하면 DataRetriever를 사용하지만 이 구성 요소는 직접 테스트를 수행했을 수도 있습니다.실제로 DataRetriever가 무엇을 할지 예측하고 다른 구성 요소에서 제어하기를 원합니다.이 격리가 필요한 또 다른 이유는 DataRetriever를 중단할 때, 이 구성 요소의 테스트 중단이 어떤 방식으로 모든 구성 요소를 사용할 수 있기만을 원하기 때문입니다.DataRetriever의 논리를 바꿀 때 수십 개의 테스트를 바꿔야 한다고 상상해 보세요. 이것은 당신이 원하지 않는 것입니다.
나는 네가 다른 구성 요소를 예측하고 싶다고 언급했다. 이 예에서 DataRetriever는 무엇을 하는가.우리는 조롱을 통해 이 점을 해냈다.Mocking은 DataRetriever 구성 요소를 가짜 (또는 아날로그) 구성 요소로 교체할 수 있도록 합니다. 이 구성 요소는 우리의 요구를 충족시킵니다.우선 테스트 파일에서 기본 프레임워크를 구축합시다.
import React from "react";
import { mount } from "enzyme";
import DataDisplayer from "./DataDisplayer";
// We want to test DataDisplayer in an isolated state, but DataDisplayer uses DataRetriever.
// To keep the isolation we will need to mock out the DataRetriever. This way we control 
// what this component does and we can predict the outcome. To do this we need to do a manual
// mock, we can do this by importing the component we want to mock, and then defining a mock
// om that import.
import DataRetriever from "./DataRetriever";
jest.mock("./DataRetriever");


describe("DataDisplayer", () => {
  // Before each test we want to reset the state of the mocked component, so each test can
  // mock the component in the way it needs to be mocked. Should you have any default mock
  // needed that is required for every test, this is the place to do this.
  beforeEach(() => {
    DataRetriever.mockClear();
  });
});

이게 의미가 있나요?다시 한 번 봅시다.DataDisplayer의 테스트 파일이며 DataRetriever를 사용합니다.DataRetriever는 DataDisplayer와 같이 테스트를 가져옵니다.그러나 그것을 가져온 후에, 우리는 아날로그 구성 요소로 이 구성 요소를 대체했다.모든 테스트가 독립적으로 실행되는 것을 확보하기 위해서, 테스트가 없으면 다른 테스트가 하는 어떤 시뮬레이션도 곤란합니다. 모든 테스트를 하기 전에 시뮬레이션을 제거합니다.그러나 우리는 시뮬레이션의 기능을 예측하고 제어할 수 있습니까?아니오, 우리는 아직 할 수 없습니다. 그러나 우리는 지금 이미 이렇게 하는 도구를 준비했습니다.첫 번째 테스트를 작성합시다.
// In this test we will mock the DataRetriever in a way that it will call the callback method
// we pass to it, and call it with "fakeTitle" as argument. This simulates that the API has
// given us a result with { title: "fakeTitle" } in it.
it("Should show the data, When retrieved", () => {
  // We are going to set up a mock implementation on the DataRetriever, we tell it when the code
  // uses DataRetiever instead of the original code it will receive a mocked object. This mocked
  // object has one method call "Retrieve".
  DataRetriever.mockImplementation(() => {
    return {
      // The retrieve method is defined as a method with is own logic. It's a method that gets 
      // another method as argument, the so-called callback method. And the only thing it does
      // is call this method with the argument "fakeTitle". This means that when the code will
      // create a new instance of DataRetriever and calls Retrieve(callback) that the method
      // callback is instantly called with the argument "fakeTitle". Simulating the API returning
      // this result.
      Retrieve: (callback) => callback("fakeTitle")
    }
  });

  // We mount the compont through enzyme. This renders the component with a fake DOM making us
  // able to see the result that would be rendered. Usually in unit tests I'd prefer the shallow
  // mount which doesn't execute lifecycle methods, but in this case part of the logic of our
  // component is in the componentDidMount lifecycle method, so we need mount to make sure this
  // lifecycle is triggerd.
  var wrapper = mount(<DataDisplayer />);
  // Since we fake a result coming back from the retriever, we expect the text to actually show
  // the word "fakeTitle" in the component.
  expect(wrapper.text()).toContain("fakeTitle");
});
그렇게 어렵지 않죠?대부분의 테스트가 농담인 것처럼 보이지만 유일하게 이상한 부분은 mockImplementation 부분일 수 있습니다.이것이 바로 이 리셋을 모의하는 관건이다.mock을 실현함으로써 코드가 이 테스트를 실행할 때 DataRetriever의 어떤 실례도 실제 구성 요소가 아니라 정의된 대상을 되돌려줍니다. 이 대상도 Retrieve 방법이 있습니다.그래서 코드는 이 방법을 호출할 수 있다.그러나 이 검색 방법은 우리가 실현한 것이다.'fake Title '을 포함하는 문자열로 리셋을 호출하는 것을 알려 준다.따라서 실제 코드 호출 검색 (콜백) 리셋은 콜백 ("fakeTitle") 과 같이 바로 호출됩니다.적응하는 데 시간이 좀 걸릴 수도 있지만, 한번 시도해 보면 의미가 있다.
이제 우리는 또 다른 장면을 테스트해야 하는데 만약 API가 실패하면 어떻게 합니까?혹은 어떤 이유로든 리콜이 아직 호출되지 않았다.이 때문에 테스트를 작성합시다.
// In this test we will mock the DataRetriever in a way that it will not call the callback
// method we pass to it. This simulates tha API not being finished or returning an error.
it("Should show not available, When data has not been retrieved", () => {
  // We are setting up a new mock implementation on the DataRetriever again.
  DataRetriever.mockImplementation(() => {
    return {
      // This is where we made it a little different. Instead of passing a method which does
      // an instant call to the callback we pass an empty method that doesn't do anything. So
      // when the code will create a new instance of DataRetriever and calls Retrieve(callback)
      // nothing is done with this callback. To make it more clear you could also read this line
      // as: Retriever: (callback) => { /* Do Nothing */ }
      Retrieve: () => {}
    }
  });

  //We mount the component again, since we need to use the lifecycle methods.
  var wrapper = mount(<DataDisplayer />);
  // Since we fake no result coming back from the retriever we don't expect any title appearing
  // on the page, but instead we expect to see the text "not available"
  expect(wrapper.text()).toContain("not available");
});
"Retrieve()의 구현을 교체하는 것이 Dell의 유일한""중대한""변경 사항입니다."우리는 리셋 방법을 직접 사용하지 않고 아무것도 하지 않았다.따라서 코드가 이 테스트에서 Retrieve () 를 호출할 때 실제 리셋은 영원히 터치되지 않습니다.멋있죠?

리소스
인터넷에는 자원이 가득 차 있다. 비록 이 문제에 있어서는 유행이 지난 자원을 사용하지 않도록 주의해야 한다.제스트와 모킹에 있어서 좋은 출발점은 그들이 https://jestjs.io/docs/en/getting-started에 관한 문서, 특히 모킹 ES6류https://jestjs.io/docs/en/es6-class-mocks에 관한 부분이다.그들의 문서는 사람들로 하여금 어찌할 바를 모르게 할 수도 있다. 특히 많은 방법으로 사물을 모의할 수 있지만, 이것은 매우 좋은 문서이기 때문에 한번 시도해 보세요.이외에도 구글로 원하는 것을 검색하면 이런 방식으로 답을 찾을 수 있다.너는 충분한 문장과 산더미처럼 쌓인 문제에 부딪혀 문제를 해결할 수 있을 것이다. 문장의 날짜를 보고 갱신을 시도하기만 하면 된다.

약속 및 비동기식/대기
비록 이 예는 리셋 방법을 사용했지만, 현재 이러한 방법은 대부분promises나 업데이트된 async/await 구조에 의해 대체되었다.이것은 결코 네가 다시 리셋을 사용할 수 없다는 것을 의미하지 않는다.코드에 의미 있는 것만 사용하면

마무리
이때 너의 머리가 회전하고 있을지도 모르니, 그것이 멈출까 봐 걱정하지 마라.아니면 생각할지도 몰라, 이게 다야?그럼 내가 말해줄게, 너한테 좋을 거야!소프트웨어 개발의 대다수 개념과 마찬가지로, 너는 반드시 어딘가에서 그것을 보고 나서 스스로 해야 한다.그리고 한 번 또 한 번 한다.마지막으로, 그들로 하여금 견지하게 하고, 개선하게 하며, 그들을 다른 사람에게 가르치게 하다.그래서 나는 누군가가 어떻게 그나 그녀의 방식으로 이 점을 해낼 수 있는지에 관한 글을 쓰기를 기대하고 있다.너는 독서에서 배우고, 실천에서 배우며, 공유를 통해 그것을 견지해 나가라.

좋은 웹페이지 즐겨찾기