역방향 공정-테스트 중인 스파이 이해

역방향 공정-테스트 중인 스파이 이해


따라와, 주제나 개선에 대한 당신의 건의를 들으니 기쁩니다/Chris

We use a spy to not only mock a response from a dependency but to ensure that our dependency has been correctly called. With correct we mean we mean the number of times, the correct type and number of arguments. There is a lot we can verify to ensure our code behaves correctly. This exercise is about understanding Spies in Jasmine, what goes on under the hood


이 문서에서는 다음과 같이 설명합니다.

  • 왜, 우리가 왜 스파이를 사용하는지, 그리고 그들의 장점을 이해하기

  • 뭐, 스파이가 우리를 위해 무엇을 할 수 있는지 설명하라

  • 어떻게, 그들이 배후에서 어떻게 일해야 하는지를 밝히지만, 그들의 공공 API를 역공정하려고 시도한다
  • TLDR 구현만 보고 우리가 어떻게 하는지 읽지 않으려면 전체 코드를 포함하는 끝까지 스크롤하십시오.:)

    왜 스파이야?


    장면을 설정합시다.우리는 이 기능에서 주문서를 사용자에게 보내기를 희망하는 업무 관건적인 기능을 가지고 있다.응용 프로그램은 노드에서 작성됩니다.js, 즉 백엔드의 JavaScript입니다.
    우리는 반드시 물건을 발송하기 전에 지불해야 한다.이 코드에 대한 어떠한 변경도 우리가 곧 실현할 스파이에 의해 포착되어야 한다.
    코드는 다음과 같습니다.
    async function makeOrder(
      paymentService, 
      shippingService, 
      address, 
      amount, 
      creditCard
    ) {
      const paymentRef = await paymentService.charge(creditCard, amount)
    
      if (paymentService.isPaid(paymentRef)) {
        shippingService.shipTo(address);
      }
    }
    
    우리는 함수makeOrder()가 있다.makeOrder() 두 개의 서로 다른 의존항 ashippingService와 apaymentService에서 도움을 받았다.중요한 것은 우리가 화물을 발송하기 전에 이미 지불을 받았는지 확인하기 위해 paymentService 를 호출하는 것이다. 그렇지 않으면 업무에 불리하다.
    마찬가지로 중요한 것은 우리가 물품 교부를 확보하기 위해 어느 때 전보shippingService를 쳤다는 것이다.현재, 코드가 이렇게 명확하지 않기 때문에, 당신은 그 작용과 아래의 모든 코드를 삭제하는 결과를 정확하게 볼 수 있습니다.관건은 우리가 아래의 코드를 위해 테스트를 작성해야 한다는 것이다. 우리는 스파이가 우리의 코드가 직접 호출되었는지 검증해야 한다.
    요컨대

    Spies are about asserting behavior over asserting on results


    뭐 공부 해요?


    알겠습니다. 본고의 몇 줄에서 언급한 바와 같이, Spies는 의존항이 몇 번 호출되었는지, 어떤 파라미터를 사용했는지 등을 검사하는 데 도움을 줄 수 있지만, 우리가 Jasmine Spies에서 알고 있는 모든 특성을 열거해 보겠습니다.

  • 호출, 호출 여부 확인

  • Args, 매개변수를 사용하여 호출되었는지 확인

  • 호출 횟수, 호출 검증 횟수

  • 호출 횟수와 파라미터, 호출 횟수와 사용 파라미터 검증

  • 시뮬레이션, 시뮬레이션 값 되돌리기

  • 복구, SPIE가 원래 기능을 대체하기 때문에 의존 관계를 원래 구현으로 복원해야 합니다
  • 이것은 우리가 상술한 행위를 단언하는 데 도움을 줄 수 있는 기능 목록이다makeOrder().

    방법


    이것이 바로 우리가 Jasmine 스파이와 공공 API를 연구하기 시작한 곳이다.그곳에서 우리는 실현이 어떤 모습일지 그려내기 시작할 것이다.
    그래.Jasmine에서는 다음 코드를 호출하여 스파이를 만듭니다.
    const apiService = {
      fetchData() {}
    }
    
    그리고 우리는 이런 테스트에서 그것을 사용한다.
    it('test', () => {
      // arrange
      spyOn(apiService, 'fetchData')
    
      // act
      doSomething(apiService.fetchData)
    
      // assert
      expect(apiService.fetchData).toHaveBeenCalled();
    })
    
    위에서 보듯이 우리는 세 가지 다른 절차에 관심을 가져야 한다.
  • spyOn()를 사용하여 스파이 만들기

  • 스파이 호출

  • 스파이가 소환되었다고 주장
  • 시작하겠습니다.

    스파이 만들기


    그것을 어떻게 사용하는지 관찰하면, 아날로그 함수의 실제 함수를 바꾸고 있다는 것을 깨닫게 될 것이다.이것은 우리가 apiService.fetchData 에게 최종적으로 분배하는 것은 반드시 함수이어야 한다는 것을 의미한다.
    수수께끼의 또 다른 부분은 우리가 그것을 어떻게 단언하는가이다.우리는 다음과 같은 몇 가지를 고려해야 한다.
    expect(apiService.fetchData).toHaveBeenCalled()
    
    이 점에서 우리는 다음과 같이 이 노선을 실시하기 시작해야 한다.
    function expect(spy) {
      return {
        toHaveBeenCalled() {
          spy.calledTimes()
        }
      }
    }
    

    WAIT. You just said that apiService.fetchData is a function. Yet in expect() you send it in and call calledTimes() on it like it was an object. I'm lost :(


    아, 알겠습니다.C#이나 Java 등 대상 언어에 대한 배경이 있을 수 있죠?

    How did you know?


    이 언어들 중에서 너는 대상이든지 함수든지 결코 둘을 겸비해서는 안 된다.하지만 우리는 JavaScript 및 JavaScript 상태입니다.
    함수는 함수 대상이다.JavaScript에서 정의되지 않은, null, 부울, 숫자 또는 문자열의 모든 비기원 형식은 대상입니다.
    이것은 우리의 스파이가 하나의 함수라는 것을 의미하지만, 그것은 하나의 대상과 같은 방법과 속성이 있다.

    Niiice. and weird..


    그래.이런 지식이 있으면 우리는 실시하기 시작할 수 있다.
    // spy.js
    
    function spy(obj, key) {
      times = 0;
      old = obj[key];
    
      function spy() {
        times++;
      }
    
      spy.calledTimes = () => times;
    
      obj[key] = spy;
    }
    
    
    function spyOn(obj, key) {
      spy(obj, key);
    }
    
    module.exports = {
      spyOn
    }
    
    spyOn() 호출spy(), 내부 생성 함수_spy(), 이 함수는 변수times를 이해하고 공공 방법calledTime()을 공개합니다.그리고 우리는 최종적으로 _spy 함수를 바꿀 대상에 분배할 것이다.

    HavebeenCall에 matcher 추가()


    파일util.js을 작성하여 다음과 같이 보십시오.
    // util.js
    
    function it(testName, fn) {
      console.log(testName);
      fn();
    }
    
    function expect(spy) {
      return {
        toHaveBeenCalled() {
          let result = spy.calledTimes() > 0;
          if (result) {
            console.log('spy was called');
          } else {
            console.error('spy was NOT called');
          }
        }
      }
    }
    
    module.exports = {
      it, 
      expect
    }
    
    보시다시피 이것은 expect()it() 방법의 아주 간단한 실현만 포함하고 있습니다.우리는 또한 우리의 실현을 테스트하기 위해 demo.js 파일을 만들어야 한다.
    // demo.js
    
    const { spyOn } = require('./spy');
    const { it, expect } = require('./util');
    
    function impl(obj) {
      obj.calc();
    }
    
    it('test spy', () => {
      // arrange
      const obj = {
        calc() {}
      }
    
      spyOn(obj, 'calc');
    
      // act
      impl(obj);
    
      // assert
      expect(obj.calc).toHaveBeenCalled();
    })
    
    우리는 이미 매우 큰 진보를 얻었지만, 어떻게 개선하는지 봅시다.

    HavebeenCalledTimes에 matcher 추가()


    이 매칭기는 우리가 어떤 물건을 호출하는 횟수를 추적하고 있기 때문에 거의 작성되었다.다음과 같이 it()util.js 함수에 다음 코드를 추가하기만 하면 됩니다.
    toHaveBeenCalledTimes(times) {
      let result = spy.calledTimes();
      if(result == times) {
        console.log(`success, spy was called ${times}`)
      } else {
        console.error(`fail, expected spy to be called: ${times} but was: ${result}`)
      }
    }
    

    HavebeenCalledwith에 matcher 추가()


    이제 이 매칭자는 우리의 스파이가 왜 불리는지, 그리고 이렇게 사용되는지 검증해 보라고 한다.
    expect(obj.someMethod).toHaveBeenCalledWith('param', 'param2');
    
    spy()의 실현을 되돌아봅시다.
    // excerpt from spy.js
    
    function spy(obj, key) {
      times = 0;
      old = obj[key];
    
      function spy() {
        times++;
      }
    
      spy.calledTimes = () => times;
    
      obj[key] = spy;
    }
    
    변수 times 를 통해 호출된 횟수를 포착한 것을 볼 수 있지만, 조금만 바꾸고 싶습니다.숫자를 저장하는 변수를 사용하는 대신 다음 그룹으로 대체합니다.
    // spy-with-args.js
    
    function spy(obj, key) {
      let calls = []
    
      function _spy(...params) {
        calls.push({
          args: params
        });
      }
    
      _spy.calledTimes = () => calls.length;
      _spy._calls = calls;
    
      obj[key] = _spy;
    }
    
    _spy() 방법에서 보듯이 우리는 모든 입력 파라미터를 수집하여 하나의 그룹에 추가합니다 calls.calls 호출 횟수를 기억할 뿐만 아니라 호출할 때마다 사용하는 매개 변수도 기억할 수 있다.
    일치기 만들기
    모든 호출과 파라미터를 저장했는지 테스트하기 위해서, expect() 방법에서 다른 일치기 함수를 만들고 toHaveBeenCalledWith() 호출합니다.현재, 우리의 스파이는 언젠가 이 매개 변수들에 의해 호출되어야 한다는 요구이다.그것은 일치하는 항목을 찾을 때까지 그룹을 순환할 수 있다는 것을 의미하지 않는다.
    매칭기를 calls 에 추가하는 방법 it() 은 다음과 같습니다.
    // excerpt from util.js
    toHaveBeenCalledWith(...params) {
      for(var i =0; i < spy._calls.length; i++) {
        const callArgs = spy._calls[i].args;
        const equal = params.length === callArgs.length && callArgs.every((value, index) => { 
          const res = value === params[index];
          return res;
        });
        if(equal) {
          console.log(`success, spy was called with ${params.join(',')} `)
          return;
        }
      }
      console.error(`fail, spy was NOT called with ${params} spy was invoked with:`);
      console.error(spy.getInvocations());
    
    }
    
    위에서 우리가 utils.js 를 어떻게 비교하는지 볼 수 있습니다. 이것이 바로 우리가 spy를 호출할 때 사용하는 매개 변수입니다.
    이제 params 및 테스트 방법 호출에 코드를 추가하기 위해 새로운 매칭기를 사용해 보겠습니다. 아래와 같습니다.
    
    // excerpt from demo.js
    
    it('test spy', () => {
      // arrange
      const obj = {
        calc() {}
      }
    
      spyOn(obj, 'calc');
    
      // act
      impl(obj);
    
      // assert
      expect(obj.calc).toHaveBeenCalled();
      expect(obj.calc).toHaveBeenCalledWith('one', 'two');
      expect(obj.calc).toHaveBeenCalledWith('three');
      expect(obj.calc).toHaveBeenCalledWith('one', 'two', 'three');
    })
    
    터미널에서 이 기능을 실행하면 다음을 얻을 수 있습니다.

    우리는 그것이 마치 매력과 같다는 것을 볼 수 있다.마땅히 해야 할 것처럼, 그것은 앞의 두 방면에서 성공했고, 마지막 방면에서 실패했다.

    리셋, 마지막 조각


    우리는 또한 실현 능력을 재설정하는 기능을 추가하고 싶다.지금, 이것은 아마도 우리가 한 가장 간단한 일일 것이다.demo.js 파일에 액세스합니다.우리는 다음과 같은 일을 해야 한다.
  • 기존 구현에 대한 참조 추가
  • 하나의 방법spy-with-args.js을 추가하면 이 방법은 우리가 원시적인 실현을 가리킨다
  • 참조 추가reset() 함수에 다음 행을 추가합니다.
    let old = obj[key];
    
    이것은 실현을 변수spy()에 저장할 것이다
    추가old 방법
    다음 줄만 추가하면 됩니다.
    _spy.reset = () => obj[key] = old;
    
    reset() 방법은 지금 이렇게 해야 한다.
    function spy(obj, key) {
      let calls = []
      let old = obj[key];
    
      function _spy(...params) {
        calls.push({
          args: params
        });
      }
    
      _spy.reset = () => obj[key] = old;
      _spy.calledTimes = () => calls.length;
      _spy.getInvocations = () => {
        let str = '';
        calls.forEach((call, index) => {
          str+= `Invocation ${index + 1}, args: ${call.args} \n`;
        });
    
        return str;
      }
    
      _spy._calls = calls;
    
      obj[key] = _spy;
    }
    

    총결산


    우리는 이미 종점에 도착했다.
    우리는 처음부터 간첩을 실시했다.그 밖에 우리는 거의 모든 것이 하나의 대상이기 때문에 우리는 우리의 방식으로 그것을 실현할 수 있다고 설명했다.
    최종 결과는 모든 호출과 호출에 사용되는 파라미터를 저장하는 스파이였다.우리는 스파이가 호출되었는지, 몇 번이나 호출되었는지, 어떤 파라미터를 사용했는지 테스트하는 데 세 개의 다른 매칭기를 성공적으로 만들었다.
    요컨대 이것은 간첩의 본질을 이해하는 성공적인 모험이다.
    우리는 이것이 단지 시작일 뿐이라는 것을 확실히 깨달았다. 그것을 생산에 투입하는 것은 우리가 대상을 사용하여 어떤 물건을 호출했는지, 지원, 시뮬레이션 등을 비교해야 한다는 것을 의미한다.나는 이 일을 너에게 연습하도록 남겨 두겠다.
    또 다른 집으로 가져가는 연습으로 시작할 때 언급한 함수spy()를 작성할 수 있는지 확인하십시오.

    전체 코드


    다음은 내가 도중에 너를 잃지 않도록 완전한 코드이다.
    util.js, 우리 matcher 함수 포함
    함수makeOrder()it() 및 일치기를 포함하는 파일입니다.
    // util.js
    
    function it(testName, fn) {
      console.log(testName);
      fn();
    }
    
    function expect(spy) {
      return {
        toHaveBeenCalled() {
          let result = spy.calledTimes() > 0;
          if (result) {
            console.log('success,spy was called');
          } else {
            console.error('fail, spy was NOT called');
          }
        },
        toHaveBeenCalledTimes(times) {
          let result = spy.calledTimes();
          if(result == times) {
            console.log(`success, spy was called ${times}`)
          } else {
            console.error(`fail, expected spy to be called: ${times} but was: ${result}`)
          }
        },
        toHaveBeenCalledWith(...params) {
          for(var i =0; i < spy._calls.length; i++) {
            const callArgs = spy._calls[i].args;
            const equal = params.length === callArgs.length && callArgs.every((value, index) => { 
              const res = value === params[index];
              return res;
            });
            if(equal) {
              console.log(`success, spy was called with ${params.join(',')} `)
              return;
            }
          }
          console.error(`fail, spy was NOT called with ${params} spy was invoked with:`);
          console.error(spy.getInvocations());
    
        }
      }
    }
    
    module.exports = {
      it, 
      expect
    }
    
    스파이 실시
    우리의 스파이 구현expect():
    function spyOn(obj, key) {
      return spy(obj, key);
    }
    
    function spy(obj, key) {
      let calls = []
      let old = obj[key];
    
      function _spy(...params) {
        calls.push({
          args: params
        });
      }
    
      _spy.reset = () => obj[key] = old;
      _spy.calledTimes = () => calls.length;
      _spy.getInvocations = () => {
        let str = '';
        calls.forEach((call, index) => {
          str+= `Invocation ${index + 1}, args: ${call.args} \n`;
        });
    
        return str;
      }
    
      _spy._calls = calls;
    
      obj[key] = _spy;
    }
    
    module.exports = {
      spyOn
    };
    
    프레젠테이션js, 테스트용
    마지막으로 우리의 spy-with-args.js 파일:
    const { spyOn } = require('./spy-with-args');
    const { it, expect } = require('./util');
    
    function impl(obj) {
      obj.calc('one', 'two');
    
      obj.calc('three');
    }
    
    it('test spy', () => {
      // arrange
      const obj = {
        calc() {}
      }
    
      spyOn(obj, 'calc');
    
      // act
      impl(obj);
    
      // assert
      expect(obj.calc).toHaveBeenCalled();
      expect(obj.calc).toHaveBeenCalledWith('one', 'two');
      expect(obj.calc).toHaveBeenCalledWith('three');
      expect(obj.calc).toHaveBeenCalledWith('one', 'two', 'three');
    })
    

    좋은 웹페이지 즐겨찾기