내용 복습/Node.js

Jest 공부 2일차

jskim4695 2024. 12. 28. 22:33

1. FIRST principal

  • Fast(빠름): 테스트가 빠르면 피드백을 신속하게 받을 수 있습니다. 특히 한개의 모음이 수천 개의 테스트로 이루어진 경우, 개발 속도를 위해 빠른 테스트는 필수적이다.
  • Independent(독립성): 단위테스트는 다른 테스트에 영향받지 않고 독립적으로 시행되어야 한다.
  • Repeatable(반복 가능): 동일한 입력에 대해 항상 같은 결과를 반환해야 합니다. 랜덤 값이나 날짜 값을 사용하는 테스트는 이 원칙을 지키기 어려울 수 있으며, 이 경우 모킹(mocking)을 사용할 수 있습니다.
  • Self-validating(자기 검증): 개발자가 일일히 sout를 해보면서 값을 확인하는게 아니라 단위 테스트는 스스로 검증할 수 있어야 합니다.
  • Thorough(철저함): 테스트는 가능한 모든 경로와 시나리오를 포괄해야 합니다. 100% 코드 커버리지가 철저한 테스트를 보장하지는 않지만, 코드 품질을 평가하는 좋은 지표가 될 수 있습니다.

2. Jest hook

테스트는 서로 독립성을 유지해야하며, Jest 훅을 사용하면 코드 중복을 줄이는 데에 도움이 된다. 설정 단계가 복수의 테스트에서 동일한 경우에 사용한다.

// Utils.ts
export class StringUtils {
  public toUpperCase(arg: string) {
    return toUpperCase(arg);
  }
}

// Utils.test.ts
describe.only("StringUtils tests", () => {
    let sut: StringUtils;

    beforeEach(() => {
      sut = new StringUtils();
      console.log("Setup");
    });

    afterEach(() => {
      // clear mocks
      console.log("Teardown");
    });

    it("Should return carrect upperCase", () => {
      const actual = sut.toUpperCase("abc");

      expect(actual).toBe("ABC");
      console.log("Actual Test");
    });
  });

 

위 코드를 보면 toUpperCase라는 메소드를 가진 class를 정의하여 매개변수를 주입한 값을 반환하는 코드를 추가하였다. 당연히 아래에 function이 이미 작성되어 있기 때문에 인스턴스를 초기화할 수 있는 것이다. 테스트 파일에는 describe 안에 새로운 describe를 정의했다. 이것이 왜 중요하냐면 이전에 작성된 내용에 it 구문이 있었는데, 실수로 그 내부에 describe를 작성했다가 에러를 겪었기 때문이다. 

beforeEach 훅은 클래스 초기화와 같은 설정 단계를 나타내고 let으로 StringUtils를 선언했는데, 이렇게 하면 각 테스트에서 새 클래스를 초기화하여 테스트가 서로 독립적이게 된다. afterEach 훅은 Mock을 정리하는 단계인데, 여기선 console.log만 찍어줬다.또 다른 훅으로는 beforeAll과 afterAll이 있는데, 통합테스트에 사용되며 beforeAll에서는 데이터베이스 연결을 초기화할 수 있어 한번만 수행되는 것이 바람직하고 afterAll을 호출하여 정리할 수 있다.

마지막으로 Jest 훅의 중요한 점은 이들이 호출되는 컨텍스트이다. describe 블록 안에 있는 훅은 그 블록에만 속한다는 점이다. 최상위 파일에서 사용하면 각 테스트 전에 실행되지만, 좋은 관행은 모든 테스트와 훅을 describe 블록 안에 두는 것이다. 각 describe는 자체 훅과 테스트를 각각 가질 수 있으며 해당 블록에 상대적으로 실행된다.

 

3. Testing for error

    it("Should throw error on invalid argument - function", () => {
      function expectError() {
        const actual = sut.toUpperCase("");
      }
      expect(expectError).toThrow("Invalid argument");
    });

    it("Should throw error on invalid argument - arrow function", () => {
      expect(() => {
        sut.toUpperCase("");
      }).toThrow("Invalid argument");
    });

    it("Should throw error on invalid argument - try catch block", () => {
      try {
        sut.toUpperCase("");
      } catch (error) {
        expect(error).toBeInstanceOf(Error);
        expect(error).toHaveProperty("message",);
      }
    });

 

위의 코드는 오류를 테스트하는 세가지 방법을 제시했다.  expectingError이라는 함수로 actual을 감싸는 방법이 있고, 화살표 함수 구문처럼 toThrow 메소드를 사용하는 방법, try-catch문이 있다. 여기서 toThrowError이라는 메소드도 함께 소개했는데, 이미 deprecated되었다는 것을 알았다. 대신 toThrow에 특정 오류 메시지나 오류 타입 검사 기능을 모두 지원하도록 한 것 같았다. 마지막으로 try-catch문은 단점이 있는데, try에 오류를 발생시키는 코드를, catch에 단언문을 넣는데 드가 오류를 발생시키지 않으면 catch문에 도달하지 않기 때문에 테스트를 통과하게 된다.

그래서 이 문제를 해결하기 위해 fail 메소드를 고려할 수 있는데, fail 메소드는 강의가 찍힌 시기부터 아직까지도 해결이 안 된 채 공식 github에 issue로 남아있다. 그래서 fail을 대신해 done을 사용할 수 있다고 하는데, 아래와 같이 매개변수로 넣어주면 된다.

    it.only("Should throw error on invalid argument - try catch block", (done) => {
      try {
        sut.toUpperCase("");
        done("GetStringInfo should throw error for invalid arg!");
      } catch (error) {
        expect(error).toBeInstanceOf(Error);
        expect(error).toHaveProperty("message");
      }
    });

 

나는 세 방식 중 toThrow 방식을 사용할 것 같다. 인수를 넣기에 따라 대부분의 오류를 검출할 수 있는 것으로 보인다.

 

4. Jest alias and watch mode

  • describe.skip : 앞서 사용했던 only 속성과 비슷한데, only는 그 블록만 테스트하는 것이라면 skip은 그 블록을 제외하고 실행한다. 시간이 과도하게 걸리거나 실행하고 싶지 않을 때 건너뛰기 위해 쓴다.
  • concurrent : 이 속성은 해당 테스트가 다른 테스트와 동시에 실행된다는 의미입니다.
  • todo : 이 기능은 새로운 프로젝트를 시작할 때 유용한데, 테스트 애플리케이션의 골격을 만드는 훌륭한 방법입니다. 아직 테스트 코드가 작성 중이지만 어떤 식으로 테스트 메시지를 반환하는지 확인하고 싶다면 todo 속성으로 미리 만들어두면 테스트를 실행할 때마다 todo에 관련된 메시지가 표시되어 후에 작업하기 쉽다.
  • only와 skip 모두 describe 블록에 적용할 수 있으며 몇가지 별칭이 있다. it과 test가 별칭의 예시인데, 둘의 기능은 완전히 동일하다. 예컨데, xit은 it.skip의 별칭이며, fit은 it.only의 별칭이다.
  • watch mode는 package.json에서 실행 script에 --watch 인자를 추가하면 실행할 수 있는데, 이 프로세스는 종료되지 않으며 테스트 파일에서 변경될 때마다 테스트를 실행할 수 있다. Enter 키를 눌러 유지되고 있는 프로세스를 이용해 수동으로 다시 검사할 수도 있다.

5. VScode debuging configuration

1. vscode의 왼쪽 바에서 디버깅 아이콘을 클릭
2. 파란 글씨의 creating debuging file을 클릭(launch.json) 자동 생성
3. launch.json 파일 안에 confuguration 키의 밸류값을 아래 코드로 대체
      "type": "node",
      "request": "launch",
      "name": "Jest Current File",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": [
        "--runTestsByPath",
        "${relativeFile}",
        "--config",
        "jest.config.ts" // js면 js로 바꿔줘야
      ],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "disableOptimisticBPs": true,
      "windows": {
        "program": "${workspaceFolder}/node_modules/jest/bin/jest"
      }​

 

위의 단계를 다르면 debuging 준비는 끝났다. 주의할 점은 jest.config.ts의 testEnvironment 옵션이 node로 되어있는지 체크해야 한다. 그렇지 않으면 디버깅이 올바르게 동작하지 않을 것이다. 다음 디버깅 탭에서 재생버튼을 눌러주면 디버깅이 동작하고 찍어놓은 브레이트 포인트를 검사하게 된다.

 

6. Coverage

jest.config.ts 파일의 옵션에 아래의 코드를 추가해준다. coverage 결과를 출력해주는 옵션이다. from은 루트 디렉터리를 기준으로 검사할 파일 형식을 지정하는 것이다. 그리고 npm test하면 커버리지 표를 볼수 있다.

  collectCoverage: true,
  collectCoverageFrom: ["<rootDir>/src/app/**/*.ts"],

 

커버리지 보고서는 터미널에서 확인할 수도 있지만 커버리지가 포함된 테스트 실행시 생성되는 coverage 폴더 내에서도 확인할 수 있다. coverage > lcov-report > index.html에서 확인할 수 있다.

html 파일을 liveserver로 열어보면 아래와 같은 정보를 볼 수 있었다.

 

우리는 Utils 하나만 test 했기에 한개만 있지만 더 큰 프로젝트라면 각 파일별로 커버리지를 확인할 수 있다. Branches는 if문을 의미하고 Functions는 파일내의 함수, Lines는 코드가 포함된 줄을 의미한다. 또한 특정 함수가 테스트에서 몇 번 호출되었는지 확인할 수 있다. 

 

다음으로 특정 부분을 coverage에서 제외하려면 istanbul ignore라는 것을 사용할 수 있는데, 아래와 같은 방법으로 사용한다. 주석 코드는 여러 종류가 있는데, 공식 깃허브 페이지에서 확인할 수 있다.

/* istanbul ignore next */
export function getStringInfo(arg: string): stringInfo {
  return {
    lowerCase: arg.toLowerCase(),
    upperCase: arg.toUpperCase(),
    characters: Array.from(arg),
    length: arg.length,
    extraInfo: {},
  };
}