자바스크립트 TDD

dobby·2024년 11월 2일
0

SOLID

SOLID는 마이클 페더스(Michael Feathers)가 1990년대 후반, 다섯 가지 객체 지향 설계 원칙을 잘 기억하려고 짜낸 머리글자이다.

  • Single Responsibility Principle (단일 책임 원칙)
  • Open/Closed Principle (개방/폐쇄 원칙)
  • Liskov Substitution Principle (리스코프 치환 원칙)
  • Interface Segregation Principle (인터페이스 분리 원칙)
  • Dependency Inversion Principle (의존성 역전 원칙)

TDD란

Test-Driven Development, 우리말로 테스트 주도 개발은 소프트웨어가 완전히 개발되기 전에, 모든 테스트 케이스에 대해 반복적으로 테스트함으로써 소프트웨어 요구사항을 검증하는 소프트웨어 개발 프로세스 중 하나다.

TDD ≠= TESTCODE
TDD의 핵심은 테스트코드가 아니다.

TDD의 핵심은 빠른 피드백에 있다.

고객이 원하는 양질의 소프트웨어를 빠른 시간안에 전달하는 것을 목표로 여기는 XP(extreme programming)에서는, 수시로 발생하는 변경사항을 단기간에 프로토타입으로 제작하고 클라이언트에게 전달하여 최대한 빨리 제품의 피드백을 얻는다.

그렇다면 클라이언트에게 도달하기 전에 프로그래밍 단계에서부터 피드백을 얻을 방법이란?

이 방법이 테스트 주도 개발, 즉 TDD인 것이다.


바르게 유지되는 코드 작성하기

단위 테스트(Unit Test)는 미래에 대비한 투자다

Unit Test는 작은 기능 단위, 혹은 함수 단위 테스트를 말한다.

즉 단위란, 특정 조건에서 어떻게 작동해야 할지 정의한 것이다.

단위 테스트 본체에 작성한 코드는 준비, 실행, 단언의 패턴을 따른다.

  1. 테스트 준비

    단위를 실행할 조건을 확실히 정하고, 의존성 및 함수 입력 데이터를 설정한다.

  2. 단위를 실행하여 테스트한다.

    단위가 함수면 준비 단계에서 미리 설정한 입력값을 함수에 넘겨 실행한다.

  3. 테스트 단언

    미리 정한 조건에 따라 예상대로 단위가 작동하는지 확인한다. 단위가 함수인 테스트라면 예상한 값을 반환하는지 조사하면 된다.

이 Unit Test에 적합한 함수 구조를 만들기 위해서는 DI(Dependency Injection)을 사용해 테스트 하기 적합한 함수를 만들어 주는 것이 일반적이다.


Dependency Injection

이는 의존 주입이라는 말로 번역할 수 있다.

함수가 의존하고(사용하고) 있는 객체들을 함수의 프로퍼티로 주입하는 방법을 이야기 한다.

function getUserName(userId) {
  fetch("user/" + userId)
    .then(res => res.text())
}

함수가 이 fetch 함수를 사용하고 있다는건, fetch 함수에 의존하고 있다는 뜻이다.

Unit Test를 하게 되면, 보통 테스트 용 데이터를 넘겨주어 결과를 확인하게 된다.

하지만 처음의 예처럼 getUserName 함수가 fetch를 의존하고 있으면, 테스트용 UserId 데이터를 넘기더라도 fetch 함수는 그대로 실행되게 된다.

fetch 함수는 서버에 정의된 유저 리소스를 로드하게 되고, getUserName 함수만 테스트 하고자 했던 Unit Test의 의미는 사라지며 결국 서버와의 통합테스트가 되어버리는 결과를 초래한다.

이 경우, Dependency Injection(의존성 주입)으로 문제를 해결할 수 있다.

function getUserName(fetch, userId) {
  return fetch("user/" + userId)
    .then(res => res.text())
}

fetch 함수를 getUserName 함수의 파라미터로써 넘겨주었다.

이제 테스트용 데이터를 넘겨줄 때 userId 뿐만 아니라, test용 fetch 함수도 넘겨줄 수 있게 된다.

테스트용 가짜 데이터를 넘겨줘서 원하는 결과가 나오는 것을 확인할 수 있다.

// test용 fetch 함수
let testFetch = () => {
  return new Promise(res => {
    res({
      text: () => Promise.resolve("David")
    })
  })
}

getUserName(testFetch, "1234")
  .then(name => console.log(name)) // David

테스트 주도 개발을 실천하라

TDD는 처음부터 프로그램을 제대로 작성했는지 확실히 보장한다.

TDD에서는 애플리케이션 코드를 짜기 전에 이 코드가 통과해야 할 단위 테스트를 먼저 작성한다.

마치 애플리케이션을 개발하듯 전체 단위 테스트 꾸러미를 만들어가는 TDD 방식을 따르면, 단위 정의와 인터페이스 설계에 도움이 많이 된다.

  1. 완벽히 변경하면 성공하나 그렇게 되기 전까지는 반드시 실패하는 단위 테스트를 작성한다.

  2. 테스트가 성공할 수 있을 만큼만 최소한으로 코딩한다.

  3. 애플리케이션 코드를 리팩토링하며 중복을 제거한다.

이 세 단계를 흔히 적색, 녹색, 리팩터라고 줄여 말한다.

적색과 녹색은 각각 새 단위 태스트의 실패와 성공 상태를 가리킨다.

[ 순서 ]

  1. 테스트 작성

    만드려는 대상이 무엇인지 선언하고, 그 대상이 테스트에서 어떻게 작동해야 할지 기술

  2. 테스트를 실행하되, 실패한다.

    우리가 목표로 하는 최소 기능을 정의한다.

  3. 테스트를 통과하기 위해 필요한 최소한의 코드를 작성하여 성공한다.

    테스트 범위를 벗어나는 코드는 추가하지 않을 것

  4. 코드와 테스트를 함께 리팩토링 작업을 한다.

한 번에 모든 테스트를 작성하려 하지 말고, 기능 단위로 테스트 작성 → 구현을 반복한다.


테스트하기 쉬운 코드로 다듬어라

테스트하기 쉬운 코드를 작성하려면 가장 중요한 단계는 관심사를 적절히 분리하는 일이다. (단일 책임 원칙)

let Users = {};
Users.registration = function() {
	return {
		validateAndRegisterUser: async function validateAndDisplayUser(user) {
			if(!user || user.name === "" || user.password === "" || user.password.length < 6) {
				throw new Error("사용자 인증이 실패했습니다.");
			}

			await fetch("http://yourapplication.com/user", user);

			// 화면에 메시지 출력
		}
	}
}

이 함수가 하는 일은 세 가지다.

  • user 객체가 올바르게 채워졌는지 검증한다.
  • 검증을 마친 user 객체를 서버로 전송한다.
  • UI에 메시지를 표시한다.

관심사는 세 가지로 요약된다.

  • 사용자 검증
  • 서버와 통신
  • UI 직접 다루기

validateAndRegisterUser 함수가 작동하는지 테스트할 조건을 모두 나열해보면 다음과 같다.

  • user가 null이면 에러를 낸다.
  • null인 user는 서버로 전송하지 않는다.
  • user가 null이면 UI를 업데이트하지 않는다.
  • user가 undefined이면 에러를 낸다.
  • undefined인 user는 서버로 전송하지 않는다.
  • user가 undefined이면 UI를 업데이트하지 않는다.
  • user의 name 프로퍼티가 빈 상태면 에러를 낸다.
  • name 프로퍼티가 빈 user는 서버로 전송하지 않는다.
  • user의 name 프로퍼티가 비어 있으면 UI를 업데이트하지 않는다.
  • ...

여기서 이름 없는 user가 넘어왔을 때 에러가 나는지 확인하는 테스트가 있다고 이름 없는 user가 서버로 전송되지 않을 것이라고 확신할 수 없다.

그러니 테스트에 추가해야 한다.

왜냐하면 코드는 혼자 작성하는 것이 아니라 동료와 함께 작성하게 될 것이며, 동료가 코드의 순서나 로직을 바꾸게 되면 문제가 발생할 것이다.

또한, 동료가 아니더라도 내가 작성한 이 코드를 언젠가 수정할 수도 있기 때문에 테스트에 추가해야 하는 것이다.

하지만, 이렇게 모든 테스트를 작성하고 보완하는건 불가능에 가깝다.

그렇기에, 세 관심사를 하나의 함수에 다 맡기지 말고 별개의 객체로 각 관심사를 추출하여 단일 책임을 부여하도록 하자.

독립적인 객체는 각자 완전한 테스트 꾸러미를 갖게 될 것이고, validateAndRegisterUser 코드는 이런 모습을 갖추게 될 것이다.

let Users = {};
Users.registration = function(userValidator, userRegister, userDisplay) {
	return {
		validateAndRegisterUser: function validateAndDisplayUser(user) {
			if(!userValidator.userIsValid(user)) {
				throw new Error("사용자 인증이 실패했습니다.");	
			}
			userRegister.registerUser(user);
			userDisplay.showRegistrationThankYou(user);
		}
	}
}

validateAndRegisterUser 는 본질적으로 어떤 일을 하는 함수에서 남이 한 일을 조정하는 함수로 탈바꿈했다.

덕분에 이 함수는 테스트하기 쉬워져서 다음 여섯 조건만 확인하면 완벽한 기능 테스트를 할 수 있다.

  • user가 잘못 넘어오면 에러가 난다.
  • 잘못된 user는 등록하지 않는다.
  • 잘못된 user는 표시하지 않는다.
  • 올바른 user를 인자로 userRegister.registerUser 함수를 실행한다.
  • userRegister.registerUser에서 에러가 나면 userDisplay.showReigstrationThankYou 함수는 실행하지 않는다.
  • user가 성공적으로 등록되면 user를 인자로 userDisplay.showRegistrationThankYou 함수를 실행한다.

테스트성을 높이려면, 관심사를 분리하는 일에 집중하고 단일 책임 원칙이나 의존성 주입 같은 소프트웨어 공학 원칙을 잘 써먹는게 중요하다.


좋은 네이밍으로 작성하라

메서드의 네이밍은 메서드가 어떻게 작동하는지가 아니라, 무엇을 하는지를 기준으로 작성해야 한다.

자바스크립트에서 특정 값이 배열에서 위치하는 인덱스를 반환하는 메서드인 indexOf 의 네이밍 과정은 아래와 같다.

  • 의도를 드러내라 - getLinearSearchPosition 배열은 항상 선형으로 존재하며 선형 탐색하며 위치를 찾아 반환한다.
  • 구현 방법을 숨겨라 - getSearchPosition 프로그래머는 작성된 메서드가 어떤 방법(Linear)으로 작동 하는지 알 필요가 없다.
  • 반환 타입에 대해 명확한 힌트를 제공해라 - indexOf 최종적으로 반환 타입(index)을 사용자가 예상할 수 있도록 문장화 시켜 가장 적절한 메서드명을 완성시킨다.

TDD의 장점

  1. 새로운 기능을 추가하거나 기존 기능을 수정할 때 발생할 수 있는 버그를 줄인다.

  2. 코드의 특정 부분에 영향을 줄 수 있는 다른 프로그래머의 변경에 대한 안정망 구축

  3. 코드가 새 변경 사항과 계속 작동하도록 하여 변경 비용을 줄인다.

  4. 테스터와 개발자간 수동 검사의 필요성을 줄인다.

  5. 코드에 대한 신뢰도 향상

  6. 리팩터링 중에 변경 사항을 깨는 것에 대한 두려움을 줄인다.



참고문서

profile
성장통을 겪고 있습니다.

0개의 댓글