[TDD] 테스트 주도 개발에 대한 고찰과 실습

Tim·2022년 8월 13일
0

ReactJS

목록 보기
5/5
post-thumbnail

테스트 주도 개발(Test-Driven Development, TDD)

들어가기앞서 테스트 주도 개발을 해야하는 이유를 찾아보았다.

"테스트 주도 개발(Test-Driven Development, TDD)은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다. 개발자는 먼저 요구사항을 검증하는 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 테스트 케이스를 통과하기 위한 최소한의 코드를 생성한다. - 위키백과"

[테스트 코드를 먼저 작성해야 하는 이유]

  • 깔끔한 코드를 작성할 수 있다.
  • 장기적으로 개발 비용을 절감할 수 있다.
  • 개발이 끝나면 테스트 코드를 작성하는 것은 매우 귀찮다. 실패 케이스면 더욱 그렇다.

테스트 코드를 작성하면 코드량이 많아진다는 단점이 있지만, 깔끔한 코드와 개발을 진행하며 나오는 오류를 사전에 검출할 수 있기에 테스트 주도 개발을 지향하고 있다고 한다.

Javascript에는 Jasmine , Jest , Tape 및 Mocha 등 많은 테스트 방법들이 있지만 대중적으로 사용하는 Jest를 사용해 볼 것이다.

Jest 시작하기

React + Typescript 환경에서 테스트를 진행하였다.
또한, React 내장 테스트 라이브러리 @testing-library/react도 같이 사용해 볼 것이다.
테스트 내용에 대한 연습 코드는 https://github.com/timcodejs/eslint-prettier-test-app 에도 올려 놓았다.

1. 설치하기

1) 리액트 프로젝트 생성

npx create-react-app test-app --template typescript

2) 라이브러리 설치

npm i -D jest eslint-plugin-jest-dom ts-jest @types/jest
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm i -D eslint-config-airbnb eslint-config-prettier
npm i -D eslint-plugin-import eslint-import-resolver-typescript
npm i -D eslint-plugin-jsx-a11y eslint-plugin-testing-library
npm i -D eslint-plugin-react eslint-plugin-react-hooks
npm i -D prettier eslint-plugin-prettier
npm i -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript babel-jest

2. 기본 설정하기 (root 경로)

1) package.json script 추가.

"scripts": {
  ...
  ...
  "prettier": "prettier --write --config ./.prettierrc \"**/*.{ts,tsx}\"",
  "lint": "eslint './src/**/*.{ts,tsx}'",
  "lint:fix": "eslint --fix './src/**/*.{ts,tsx}'",
  "jest": "jest"
}

2) eslint, prettier 설정.

  • .eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
    jest: true
  },
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'prettier'],
  extends: [
    'eslint:recommended',
    'plugin:import/errors',
    'plugin:import/warnings',
    'plugin:import/typescript',
    'plugin:react/recommended',
    'plugin:prettier/recommended',
    'plugin:@typescript-eslint/recommended',
    'airbnb',
    'prettier',
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatuers: {
      jsx: true
    },
  },
  rules: {
    "indent": "off",
    "camelcase": "off",
    "semi": "off",
    "quotes": "off",
    "no-shadow": "off",
    "no-use-before-define": "off",
    "no-unused-vars": "off",
    "no-console": "off",
    "no-magic-numbers": "off",
    "no-redeclare": "off",
    "no-array-constructor": "off",
    "no-dupe-class-members": "off",
    "no-extra-semi": "off",
    "no-empty-function": "off",
    "linebreak-style": "off",
    "prettier/prettier": "off",
    "arrow-body-style": "off",
    "jsx-a11y/no-noninteractive-element-interactions": "off",
    "import/extensions": "off",
    "import/no-unresolved": "off",
    "import/no-extraneous-dependencies": "off",
    "import/prefer-default-export": "off",
    "import/no-named-as-default": "off",
    /* react options */
    "react/prop-types": "off",
    "react/display-name": "off",
    "react/react-in-jsx-scope": "off",
    "react/destructuring-assignment": "off",
    "react/no-array-index-key": "off",
    "react/jsx-filename-extension": [2, {
      extensions: [".js", ".jsx", ".ts", ".tsx"]
    }],
    "react/function-component-definition": [
      2,
      {
        namedComponents: "arrow-function",
        unnamedComponents: "arrow-function",
      },
    ],
    /* typescript-eslint options */
    "@typescript-eslint/no-empty-interface": "warn",
    "@typescript-eslint/adjacent-overload-signatures": "warn",
    "@typescript-eslint/no-dupe-class-members": "warn",
    "@typescript-eslint/no-misused-new": "warn",
    "@typescript-eslint/no-redeclare": "warn",
    "@typescript-eslint/no-array-constructor": "warn",
    "@typescript-eslint/no-namespace": "warn",
    "@typescript-eslint/no-var-requires": "warn",
    "@typescript-eslint/prefer-namespace-keyword": "warn",
    "@typescript-eslint/no-shadow": "off",
    "@typescript-eslint/no-empty-function": "off",
    "@typescript-eslint/type-annotation-spacing": "off",
    "@typescript-eslint/no-extra-semi": "off",
    "@typescript-eslint/member-ordering": "off",
    "@typescript-eslint/no-inferrable-types": "off",
    "@typescript-eslint/no-use-before-define": "off",
    "@typescript-eslint/no-non-null-asserted-optional-chain": "off",
    "@typescript-eslint/no-non-null-assertion": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/semi": "off",
    "@typescript-eslint/naming-convention": "off",
    "@typescript-eslint/indent": "off",
    "@typescript-eslint/member-delimiter-style": "off",
    "@typescript-eslint/quotes": "off",
    "@typescript-eslint/consistent-type-assertions": "off",
    "@typescript-eslint/triple-slash-reference": "warn",
    "@typescript-eslint/no-magic-numbers": "off"
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
}
  • .eslintignore (eslint 검사를 무시할 파일 또는 폴더를 적으면 된다.)
node_modules
dist

*.css
*.svg
  • .prettierrc
{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 120,
  "arrowParens": "always"
}
  • babel.config.js
module.exports = {
  presets: [
    "@babel/preset-react",
    ['@babel/preset-env', {
      targets: {
        node: 'current'
      }
    }],
    '@babel/preset-typescript',
  ],
};
  • jest.config.js
const config = {
  preset: "ts-jest",
  testEnvironment: "jsdom",
  moduleFileExtensions: ["js", "jsx", "ts", "tsx"],
  testPathIgnorePatterns: ["/node_modules/"],
  testRegex: ".*.(test|spec).(j|t)s[x]?$",
  transform: {
    "node_modules/(react-dnd|dnd-core|@react-dnd)/.+\\.(j|t)sx?$": "ts-jest",
    "^.+\\.js$": "babel-jest",
  },
  transformIgnorePatterns: [`/node_modules/(?!(somePkg)|react-dnd|dnd-core|@react-dnd)`],
};

module.exports = config;
  • 파일 구조

3. 테스트 코드 작성하기

Props로 전달받은 인자로 덧셈 테스트 코드를 작성해 보았다.
(모든 테스트 파일들은 OOOO.test.jsx 또는 OOOO.test.tsx 로 파일 이름을 설정해야 테스트코드를 읽어들여 테스트가 실행된다.)

  • increment.tsx (실제 코드 파일)
import React from 'react';

export type NumberProps = {
  num1: number,
  num2: number
}

const Increment = ({ num1, num2 }: NumberProps) => {
  return <div>result = {num1 + num2}</div>;
};

export default Increment;
  • increment.test.tsx (테스트 파일)
import { render, screen } from '@testing-library/react';
import Increment from '../components/increment';

// 테스트용 임의 값
const num1: number = 3;
const num2: number = 5;

describe('Increment', () => {
  test('3 더하기 5는 8 인가?', () => {
    render(<Increment num1={num1} num2={num2} />);
    const el = screen.getByText(/8/i);
    expect(el).toBeInTheDocument();
    // screen.debug(); // DOM TREE 출력
  });
});
  • 테스트 함수
    1) react 내장 테스트 라이브러리 @testing-library/react의 render, screen 함수 import
    2) describe() 함수로 테스트 함수 그룹화
    3) test() 또는 it() 함수로 테스트 케이스 정의
    4) render() 함수에 테스트할 컴포넌트를 렌더
    5) screen() 함수의 getByText 쿼리함수를 사용하여 해당 텍스트를 가진 엘리먼트가 있는지 확인 (정규식 사용)
    6) expect(...).toBeInDocument() 로 테스트 확인
    7) screen() 힘수의 debug 쿼리함수를 사용하여 DOM을 확인

  • 테스트 해보기

profile
HTML5, CSS3, JavaScript, Typescript, React 프론트엔드 개발자입니다.

0개의 댓글