JavaScript로 테스트 가능한 코드 작성: 간략한 개요
게시 됨: 2022-03-11Mocha 또는 Jasmine과 같은 테스트 프레임워크와 페어링된 Node를 사용하든 PhantomJS와 같은 헤드리스 브라우저에서 DOM 종속 테스트를 실행하든, JavaScript 단위 테스트 옵션은 그 어느 때보다 향상되었습니다.
그러나 이것이 우리가 테스트하는 코드가 우리 도구만큼 쉽다는 것을 의미하지는 않습니다! 쉽게 테스트할 수 있는 코드를 구성하고 작성하려면 약간의 노력과 계획이 필요하지만 함수형 프로그래밍 개념에서 영감을 받아 코드를 테스트할 때 어려운 상황에 빠지지 않도록 하는 데 사용할 수 있는 몇 가지 패턴이 있습니다. 이 기사에서는 JavaScript로 테스트 가능한 코드를 작성하기 위한 몇 가지 유용한 팁과 패턴을 살펴보겠습니다.
비즈니스 로직과 디스플레이 로직을 별도로 유지
JavaScript 기반 브라우저 애플리케이션의 주요 작업 중 하나는 최종 사용자가 트리거한 DOM 이벤트를 수신한 다음 일부 비즈니스 로직을 실행하고 페이지에 결과를 표시하여 이벤트에 응답하는 것입니다. DOM 이벤트 리스너를 설정하는 바로 그 곳에서 대부분의 작업을 수행하는 익명의 함수를 작성하고 싶을 것입니다. 이것이 생성하는 문제는 이제 익명 함수를 테스트하기 위해 DOM 이벤트를 시뮬레이션해야 한다는 것입니다. 이것은 코드 라인과 테스트 실행에 걸리는 시간 모두에서 오버헤드를 생성할 수 있습니다.
대신 명명된 함수를 작성하고 이벤트 핸들러에 전달하십시오. 그렇게 하면 가짜 DOM 이벤트를 트리거하기 위해 후프를 건너뛰지 않고 명명된 함수에 대한 테스트를 직접 작성할 수 있습니다.
이것은 DOM 이상에 적용됩니다. 브라우저와 노드 모두에 있는 많은 API는 이벤트를 실행하고 수신하거나 다른 유형의 비동기 작업이 완료되기를 기다리는 것을 중심으로 설계되었습니다. 경험상 많은 익명 콜백 함수를 작성하는 경우 코드를 테스트하기가 쉽지 않을 수 있습니다.
// hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }
비동기 코드로 콜백 또는 프라미스 사용
위의 코드 예제에서 리팩토링된 fetchThings 함수는 대부분의 작업을 비동기식으로 수행하는 AJAX 요청을 실행합니다. 이것은 우리가 함수를 실행할 수 없고 우리가 예상한 모든 것을 수행했는지 테스트할 수 없다는 것을 의미합니다. 왜냐하면 언제 실행이 완료될지 알 수 없기 때문입니다.
이 문제를 해결하는 가장 일반적인 방법은 비동기적으로 실행되는 함수에 매개변수로 콜백 함수를 전달하는 것입니다. 단위 테스트에서 전달한 콜백에서 어설션을 실행할 수 있습니다.
비동기 코드를 구성하는 또 다른 일반적이고 점점 더 인기 있는 방법은 Promise API를 사용하는 것입니다. 다행히 $.ajax 및 대부분의 다른 jQuery 비동기 함수는 이미 Promise 객체를 반환하므로 많은 일반적인 사용 사례가 이미 다루어졌습니다.
// hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }
부작용 피하기
결과를 얻기 위해 수학 방정식에 숫자를 펀칭하는 것처럼 인수를 사용하고 해당 인수만을 기반으로 값을 반환하는 함수를 작성하십시오. 함수가 일부 외부 상태(예: 클래스 인스턴스의 속성 또는 파일 내용)에 의존하고 함수를 테스트하기 전에 해당 상태를 설정해야 하는 경우 테스트에서 더 많은 설정을 수행해야 합니다. 실행 중인 다른 코드가 동일한 상태를 변경하지 않는다는 것을 신뢰해야 합니다.
같은 맥락에서, 실행되는 동안 외부 상태를 변경하는 기능(예: 파일에 쓰기 또는 데이터베이스에 값 저장)을 작성하지 마십시오. 이렇게 하면 다른 코드를 자신 있게 테스트하는 능력에 영향을 줄 수 있는 부작용을 방지할 수 있습니다. 일반적으로 가능한 한 적은 "표면적"으로 부작용을 코드 가장자리에 최대한 가깝게 유지하는 것이 가장 좋습니다. 클래스 및 개체 인스턴스의 경우 클래스 메서드의 부작용은 테스트 중인 클래스 인스턴스의 상태로 제한되어야 합니다.
// hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }
의존성 주입 사용
함수의 외부 상태 사용을 줄이는 일반적인 패턴 중 하나는 종속성 주입입니다. 함수의 모든 외부 요구 사항을 함수 매개변수로 전달합니다.

// depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }
의존성 주입을 사용하는 주요 이점 중 하나는 실제 부작용(이 경우 데이터베이스 행 업데이트)을 일으키지 않는 단위 테스트에서 모의 객체를 전달할 수 있고 모의 객체가 작동했다고 주장할 수 있다는 것입니다. 예상대로.
각 기능에 단일 목적 부여
여러 작업을 수행하는 긴 함수를 단일 목적의 짧은 함수 모음으로 나눕니다. 이렇게 하면 값을 반환하기 전에 큰 함수가 모든 것을 올바르게 수행하기를 바라는 것보다 각 함수가 제대로 수행하는지 테스트하는 것이 훨씬 쉽습니다.
함수형 프로그래밍에서 여러 단일 목적 함수를 함께 묶는 행위를 합성이라고 합니다. Underscore.js에는 _.compose
함수도 있습니다. 이 함수는 함수 목록을 가져와서 함께 연결하여 각 단계의 반환 값을 가져와 다음 함수에 전달합니다.
// hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return 'Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }
매개변수를 변경하지 마십시오.
JavaScript에서 배열과 객체는 값이 아닌 참조로 전달되며 변경 가능합니다. 즉, 객체나 배열을 매개변수로 함수에 전달할 때 코드와 객체 또는 배열을 전달한 함수 모두 메모리에서 해당 배열이나 객체의 동일한 인스턴스를 변경할 수 있는 기능을 갖습니다. 즉, 자신의 코드를 테스트하는 경우 코드에서 호출하는 함수가 개체를 변경하지 않는다는 것을 신뢰해야 합니다. 코드에 동일한 개체를 변경하는 새 위치를 추가할 때마다 해당 개체가 어떻게 생겼는지 추적하기가 점점 더 어려워지고 테스트하기가 더 어려워집니다.
대신 객체나 배열을 취하는 함수가 있는 경우 해당 객체나 배열이 읽기 전용인 것처럼 작동하도록 하십시오. 코드에서 새 개체 또는 배열을 만들고 필요에 따라 값을 추가합니다. 또는 Underscore 또는 Lodash를 사용하여 작업하기 전에 전달된 개체 또는 배열을 복제합니다. 더 좋은 방법은 읽기 전용 데이터 구조를 생성하는 Immutable.js와 같은 도구를 사용하는 것입니다.
// alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }
코드를 작성하기 전에 테스트를 작성하십시오
테스트 중인 코드보다 먼저 단위 테스트를 작성하는 프로세스를 테스트 주도 개발(TDD)이라고 합니다. 많은 개발자들이 TDD가 매우 유용하다고 생각합니다.
먼저 테스트를 작성함으로써 개발자는 이를 사용하는 개발자의 관점에서 노출하려는 API에 대해 생각해야 합니다. 또한 불필요하게 복잡한 솔루션을 과도하게 엔지니어링하는 대신 테스트에서 시행하는 계약을 충족하기에 충분한 코드만 작성하도록 하는 데 도움이 됩니다.
실제로 TDD는 모든 코드 변경 사항에 대해 커밋하기 어려울 수 있는 분야입니다. 그러나 시도해 볼 가치가 있는 것처럼 보이면 모든 코드를 테스트 가능하게 유지하는 좋은 방법입니다.
마무리
우리는 복잡한 JavaScript 앱을 작성하고 테스트할 때 매우 쉽게 넘어갈 수 있는 몇 가지 함정이 있다는 것을 알고 있습니다. 그러나 이러한 팁을 사용하고 항상 코드를 가능한 한 간단하고 기능적으로 유지하는 것을 기억하면 테스트 범위를 높게 유지하고 전체 코드 복잡성을 낮게 유지할 수 있습니다!
- 자바스크립트 개발자가 저지르는 가장 흔한 실수 10가지
- 니드포 스피드: 자바스크립트 코딩 챌린지 회고전