TDD(테스트 주도 개발)에 대하여..

365.48km·2023년 5월 25일
0

요즘 취준을 하면서 매일같이 보는 자격요건에 TDD라는 단어가 자주 보인다. 도대체 TDD가 뭐길래? 심심찮게 보이고 있어 결국 공부를 해야겠다고 마음을 먹게 되면서 정리를 하게 되었다.

TDD(Test Driven Development)란?

TDD는 테스트 주도 개발은 개발방식 중 하나로 테스트 코드를 먼저 작성하고, 그 테스트를 통과시킬 수 있는 최소한의 코드로 개발을 진행하는 것을 말한다.
즉, TDD의 주요 아이디어는 개발자가 코드를 작성하기 전에 테스트를 먼저 작성한다는 것이다.

TDD의 목표는?

TDD의 궁극적인 목표는 Clean Code that works 즉, 작동하는 클린코드를 말한다. TDD의 다른말로는, Test First Programming 으로, 테스트를 먼저 작동하는 방식을 의미한다.

테스트란?
프로그램을 실행하여 오류와 결함을 검증하고 애플리케이션이 요구사항에 맞게 동작하는지 검증하는 절차를 의미한다.

왜 TDD가 필요한가? (feat 기술부채)

TDD를 도입하는 이유는 대표적으로 기술부채를 막기 위함이다.

1) 기술부채란?

기술부채란?
기술 부채(Technical debt)는 소프트웨어 개발에서 발생하는 개념으로, 개발자가 의도적으로 혹은 무의식적으로 소프트웨어의 품질을 희생하고 생산성을 높이기 위해 채택한 결정 또는 절차이다.

이는 소프트웨어 개발 과정에서 더 빠르게 결과물을 제공하거나, 기능을 추가하기 위해 일시적인 해결책을 선택하는 것을 의미합니다.

즉, 다시말하면 품질보다 생산성에 초점을 두면 결국, 문제가 터질 수 있다는 말이다.

그러면 어떤 상황에서 기술부채가 생겨날까?

2) 기술부채가 발생하는 상황

기술 부채는 주로 코드의 임시적 해결, 설계의 절충 , 기술적인 빚 등의 세 가지 경우에서 발생할 수 있다.

1. 코드의 임시적인 해결
개발자가 기능을 빠르게 추가하기 위해 효율적인 해결책 대신에 임시로 작동하는 코드를 작성하는 경우입니다. 이는 나중에 코드를 개선하거나 리팩토링해야 하는 부담을 야기할 수 있습니다.

2. 설계의 절충
제한된 시간이나 자원으로 인해 효율적인 설계를 생략하거나 간단한 설계를 선택하는 경우입니다. 이는 기능 추가 또는 변경이 발생할 때 코드를 수정하기 어렵게 만들 수 있습니다.

3. 기술적인 빚
새로운 기술이나 도구를 적용하기 위해 초기에 일부 품질을 희생하는 경우입니다. 이는 추후에 기술적인 빚을 상환하거나 코드를 수정해야 할 필요성을 야기할 수 있습니다.

결국, 기술부채는 처음에는 빠른 개발과 생산성 향상을 제공하지만, 시간이 지나면서 문제 발생과 비용을 더욱 증가시킬 수 있다. (코드의 가독성, 유지 보수성 저하, 버그의 발생 증가, 성능 저하)

기술 부채를 관리하기 위해서는 개발자와 관리자들은 부채의 존재를 인식하고, 적절한 시기에 리팩토링이나 코드 개선 작업을 수행해야 한다.

TDD Cycle

TDD는 다음과 같이 실패 -> 통과 -> 개선 사이클을 갖게 되는데,
처음에 실패한 뒤, 통과를 시킨다. 그다음 통과된 코드에서 중복을 제거하고 개선된 코드의 사이클을 둔다.
하지만, 문제는 테스트가 가능한 코드를 구현하는 것인데, 규모가 커지고 로직이 복잡할 수록 이러한 TDD를 구현하는 것이 어려울 수 있다.

How to make Testable Code

테스트해야할 코드나 기능단위의 덩어리가 커지거나 상태관리에 대해서 복잡해질 경우 Testable Code 즉, 테스트가 가능한 코드를 구현하는 것은 어려움이 따른다.

따라서, 관심사의 분리(Separation of Concerns)가 중요하다.

*관심사 분리란?

개별 요소들이 자신이 관심받고 있는 곳에만 집중하여 관심을 기울이는 SW 엔지니어링 기법이다.

Count 증감 버튼에 따라는 숫자 출력 테스트 예시

📦components
 ┣ 📜AwesomeCounter.test.tsx
 ┗ 📜AwesomeCounter.tsx

증감 버튼과 관련 컴포넌트에 대해서 공통의 관심사에 맞게 네이밍과 배치를 하였다.

interface Props {
  initialValue?: number;
}

const AwesomeCounter = ({ initialValue }: Props) => {
  const [count, setCount] = useState(initialValue ?? 0);

  const add = () => {
    setCount((prev) => prev + 1);
  };

  const remove = () => {
    setCount((prev) => {
      const result = prev - 1;
      if (result < 0) {
        return 0;
      }
      return result;
    });
  };
  return (
    <div>
      <h1>Awesome Counter</h1>
      <button onClick={remove}>Remove</button>
      <span>{count}</span>
      <button onClick={add}>Add</button>
    </div>
  );
};

export default AwesomeCounter;

1. TEST : Props로 초기값을 전달할 때, 테스트

종류
초깃값7
결과7
테스트PASS
test("it should have the correct initial value when set to 7", () => {
  render(<AwesomeCounter initialValue={7} />);
  const count = screen.queryByText(7); // 문자열로 비교하도록
  expect(count).toBeInTheDocument();
});

2. Props로 초기값을 전달하지 않을 때, 0 출력 테스트

종류
초깃값null
결과0
테스트PASS
test("it should have the correct initial value when of 0", () => {
  render(<AwesomeCounter />);
  const count = screen.queryByText(0);
  expect(count).toBeInTheDocument();
});

3. 초깃값이 0일때, add 버튼 클릭 시 1 출력 테스트

종류
초깃값0
결과1
테스트PASS
test("it should increment the count by 1 when the add button is clicked", () => {
  render(<AwesomeCounter initialValue={0} />);
  const addButton = screen.getByText("Add");
  fireEvent.click(addButton);
  const count = screen.getByText(1);
  expect(count).toBeVisible();
});

4. 초기값이 1일때, add 버튼 클릭시 2 출력 테스트

종류
초깃값1
결과2
테스트PASS
test("it should have the correct initial value add is clicked once", () => {
  render(<AwesomeCounter initialValue={1} />);
  const addButton = screen.getByText("Add");
  fireEvent.click(addButton);
  const count = screen.getByText(2);
  expect(count).toBeVisible();
});

5. 초기값이 1일때, add 버튼 두 번 클릭시 3 출력 테스트

종류
초깃값1
결과3
테스트PASS
test("it should have the correct initial value add is clicked twice", () => {
  render(<AwesomeCounter initialValue={1} />);
  const addButton = screen.getByText("Add");
  fireEvent.click(addButton);
  fireEvent.click(addButton);
  const count = screen.queryByText(3);
  expect(count).toBeVisible();
});

6. 초기값이 5일때, remove 버튼 클릭시 4 출력 테스트

종류
초깃값5
결과4
테스트PASS
test("it should have the correct initial value remove is clicked once", () => {
  render(<AwesomeCounter initialValue={5} />);
  const removeButton = screen.getByText("Remove");
  fireEvent.click(removeButton);
  const count = screen.queryByText(4);
  expect(count).toBeVisible();
});

7. 초기값이 5일때, remove 버튼 두 번 클릭시 3 출력 테스트

종류
초깃값5
결과3
테스트PASS
test("it should have the correct initial value remove is clicked twice", () => {
  render(<AwesomeCounter initialValue={5} />);
  const removeButton = screen.getByText("Remove");
  fireEvent.click(removeButton);
  fireEvent.click(removeButton);
  const count = screen.queryByText(3);
  expect(count).toBeVisible();
});

8. 초기값이 0일때, remove 버튼 두 번 클릭시 0 출력 테스트

종류
초깃값0
결과0
테스트PASS
test("it should not allow a negative number when the initial value is 0 and remove is clicked", () => {
  render(<AwesomeCounter initialValue={0} />);
  const removeButton = screen.getByText("Remove");
  fireEvent.click(removeButton);
  const count = screen.queryByText(0);
  expect(count).toBeVisible();
});

9. 초기값이 2일때, remove 버튼 네 번 클릭시 0 출력 테스트

종류
초깃값2
결과0
테스트PASS
test("it should not allow a negative number when the initial value is 0 and remove is clicked", () => {
  render(<AwesomeCounter initialValue={2} />);
  const removeButton = screen.getByText("Remove");
  fireEvent.click(removeButton);
  fireEvent.click(removeButton);
  fireEvent.click(removeButton);
  fireEvent.click(removeButton);

  const count = screen.queryByText(0);
  expect(count).toBeVisible();
});

TDD를 공부하며 느낀점

단순 겉핥기식으로 기본적인 기능단위의 TDD를 구현하였으나, 과연 이게 복잡해지는 기능 단위에 대해서는 어떻게 구현해야할지 어려움이 생길 것 같다고 판단이 된다.
그리고 단순히 구현하기 전에 테스트 작성을 먼저 진행하면서 초기 시간적으로 더 오래 걸리게 되는 것은 어쩔수 없는 것 같다.

하지만 그럼에도 장점이 있다면?

잘 구현하면 오류를 조기에 발견하는 것과 개발 초기 단계부터 관심사분리에 더 집중하면서 리팩토링에 대해 더 많이 생각해볼 수 있을 것 같다.

하지만 이것을 과연 어떻게 잘 적용을 해야할지는 더 해보면서 느껴봐야할 것 같다.

profile
이게 마즐까?

0개의 댓글