요즘 취준을 하면서 매일같이 보는 자격요건
에 TDD라는 단어가 자주 보인다. 도대체 TDD가 뭐길래? 심심찮게 보이고 있어 결국 공부를 해야겠다고 마음을 먹게 되면서 정리를 하게 되었다.
TDD
는 테스트 주도 개발은 개발방식 중 하나로 테스트 코드를 먼저 작성하고, 그 테스트를 통과시킬 수 있는 최소한의 코드로 개발을 진행하는 것을 말한다.
즉, TDD의 주요 아이디어는 개발자가 코드를 작성하기 전에 테스트를 먼저 작성한다는 것이다.
TDD
의 궁극적인 목표는 Clean Code that works 즉, 작동하는 클린코드를 말한다. TDD
의 다른말로는, Test First Programming 으로, 테스트를 먼저 작동하는 방식을 의미한다.
테스트란?
프로그램을 실행하여 오류와 결함을 검증하고 애플리케이션이 요구사항에 맞게 동작하는지 검증하는 절차를 의미한다.
TDD
를 도입하는 이유는 대표적으로 기술부채
를 막기 위함이다.
기술부채란?
기술 부채(Technical debt)는 소프트웨어 개발에서 발생하는 개념으로, 개발자가 의도적으로 혹은 무의식적으로 소프트웨어의 품질을 희생하고 생산성을 높이기 위해 채택한 결정 또는 절차이다.
이는 소프트웨어 개발 과정에서 더 빠르게 결과물을 제공하거나, 기능을 추가하기 위해 일시적인 해결책을 선택하는 것을 의미합니다.
즉, 다시말하면 품질보다 생산성에 초점을 두면 결국, 문제가 터질 수 있다는 말이다.
그러면 어떤 상황에서 기술부채가 생겨날까?
기술 부채는 주로 코드의 임시적 해결
, 설계의 절충
, 기술적인 빚
등의 세 가지 경우에서 발생할 수 있다.
1. 코드의 임시적인 해결
개발자가 기능을 빠르게 추가하기 위해 효율적인 해결책 대신에 임시
로 작동하는 코드를 작성하는 경우입니다. 이는 나중에 코드를 개선하거나 리팩토링해야 하는 부담을 야기할 수 있습니다.
2. 설계의 절충
제한된 시간이나 자원으로 인해 효율적인 설계를 생략
하거나 간단한 설계
를 선택하는 경우입니다. 이는 기능 추가 또는 변경이 발생할 때 코드를 수정하기 어렵게 만들 수 있습니다.
3. 기술적인 빚
새로운 기술이나 도구를 적용하기 위해 초기에 일부 품질을 희생하는 경우입니다. 이는 추후에 기술적인 빚을 상환하거나 코드를 수정해야 할 필요성을 야기할 수 있습니다.
결국, 기술부채는 처음에는 빠른 개발과 생산성 향상을 제공하지만, 시간이 지나면서 문제 발생과 비용을 더욱 증가시킬 수 있다. (코드의 가독성, 유지 보수성 저하, 버그의 발생 증가, 성능 저하)
기술 부채를 관리하기 위해서는 개발자와 관리자들은 부채의 존재를 인식하고, 적절한 시기에 리팩토링
이나 코드 개선
작업을 수행해야 한다.
TDD
는 다음과 같이 실패
-> 통과
-> 개선
사이클을 갖게 되는데,
처음에 실패한 뒤, 통과를 시킨다. 그다음 통과된 코드에서 중복을 제거하고 개선된 코드의 사이클을 둔다.
하지만, 문제는 테스트가 가능한 코드를 구현하는 것인데, 규모가 커지고 로직이 복잡할 수록 이러한 TDD를 구현하는 것이 어려울 수 있다.
테스트해야할 코드나 기능단위의 덩어리가 커지거나 상태관리에 대해서 복잡해질 경우 Testable Code
즉, 테스트가 가능한 코드를 구현하는 것은 어려움이 따른다.
따라서, 관심사의 분리(Separation of Concerns)가 중요하다.
*관심사 분리란?
개별 요소들이 자신이 관심받고 있는 곳에만 집중하여 관심을 기울이는 SW 엔지니어링 기법이다.
📦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;
종류 | 값 |
---|---|
초깃값 | 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();
});
종류 | 값 |
---|---|
초깃값 | null |
결과 | 0 |
테스트 | PASS |
test("it should have the correct initial value when of 0", () => {
render(<AwesomeCounter />);
const count = screen.queryByText(0);
expect(count).toBeInTheDocument();
});
종류 | 값 |
---|---|
초깃값 | 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();
});
종류 | 값 |
---|---|
초깃값 | 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();
});
종류 | 값 |
---|---|
초깃값 | 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();
});
종류 | 값 |
---|---|
초깃값 | 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();
});
종류 | 값 |
---|---|
초깃값 | 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();
});
종류 | 값 |
---|---|
초깃값 | 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();
});
종류 | 값 |
---|---|
초깃값 | 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를 구현하였으나, 과연 이게 복잡해지는 기능 단위에 대해서는 어떻게 구현해야할지 어려움이 생길 것 같다고 판단이 된다.
그리고 단순히 구현하기 전에 테스트 작성을 먼저 진행하면서 초기 시간적으로 더 오래 걸리게 되는 것은 어쩔수 없는 것 같다.
하지만 그럼에도 장점이 있다면?
잘 구현하면 오류를 조기에 발견하는 것과 개발 초기 단계부터 관심사분리에 더 집중하면서 리팩토링에 대해 더 많이 생각해볼 수 있을 것 같다.
하지만 이것을 과연 어떻게 잘 적용을 해야할지는 더 해보면서 느껴봐야할 것 같다.