조롱은 코드 냄새

스모크 아트 큐브 스모크 — MattysFlicks — (CC BY 2.0)
참고 :이 글은 JavaScript ES6 +의 기능 프로그래밍 및 컴포지션 소프트웨어 기술을 처음부터 배우는“소프트웨어 구성”시리즈 (현재 책!)의 일부입니다. 계속 지켜봐 주시기 바랍니다. 앞으로 더 많은 것들이 있습니다!
<이전 | << 다시 시작

내가 TDD 및 단위 테스트에 대해 가장 큰 불만 중 하나는 사람들이 단위를 분리하는 데 필요한 모든 조롱으로 어려움을 겪고 있다는 것입니다. 어떤 사람들은 단위 테스트가 어떻게 의미가 있는지 이해하려고 노력합니다. 실제로 개발자가 실제 구현 코드를 전혀 사용하지 않는 단위 테스트 파일 전체를 작성하기 위해 모의, 가짜 및 스텁에서 너무 길을 잃은 것을 보았습니다. 죄송합니다.

스펙트럼의 다른 쪽 끝에서 개발자가 TDD의 교리에 빠져 들어가서 코드베이스를 더 복잡하게 만들어야하더라도 필요한 수단을 통해 100 % 코드 범위를 절대적으로 달성해야한다고 생각하는 것이 일반적입니다. 그것을 뽑아.

나는 종종 사람들에게 조롱이 코드 냄새라고 말하지만 대부분의 개발자들은 TDD 기술의 한 단계를 거쳐 100 % 단위 테스트 범위를 달성하고 모의를 광범위하게 사용하지 않는 세상을 상상할 수 없습니다. 응용 프로그램에 목을 짜기 위해 의존성 주입 기능을 장치 주위에 포장하거나 서비스를 의존성 주입 컨테이너에 포장하는 경향이 있습니다.

Angular는 의존성 주입을 모든 Angular 구성 요소 클래스에 직접 베이킹하여 사용자를 의존성 주입을 분리의 주요 수단으로 보도록 유혹함으로써이를 극단으로 끌어 올렸습니다. 그러나 의존성 주입은 디커플링을 수행하는 가장 좋은 방법은 아닙니다.

더 나은 디자인으로 이어지는 TDD

효과적인 TDD를 배우는 과정은 더 많은 모듈 식 응용 프로그램을 구축하는 방법을 배우는 과정입니다.

TDD는 복잡한 효과가 아니라 코드에 단순화 효과를주는 경향이 있습니다. 더 테스트 가능하게 만들 때 코드를 읽거나 유지하기가 어려워 지거나 의존성 주입 상용구로 코드를 부풀려 야하는 경우 TDD가 잘못되었습니다.

전 세계를 조롱 할 수 있도록 앱에 의존성 주입을 낭비하는 데 시간을 낭비하지 마십시오. 도움보다 당신을 다치게 할 가능성이 매우 높습니다. 더 테스트 가능한 코드를 작성하면 코드가 간단 해집니다. 더 적은 수의 코드 라인과 더 읽기 쉽고 유연하며 유지 보수가 가능한 구성이 필요합니다. 의존성 주입은 반대 효과가 있습니다.

이 본문은 두 가지를 가르치기 위해 존재합니다.

  1. 의존성 주입없이 분리 된 코드를 작성할 수 있으며
  2. 코드 적용 범위를 최대화하면 수익이 줄어 듭니다. 적용 범위가 100 %에 가까울수록 응용 프로그램 코드가 복잡해지면서 더 가까워 질수록 응용 프로그램의 버그를 줄이는 중요한 목표를 파괴 할 수 있습니다.

더 복잡한 코드에는 종종 더 복잡한 코드가 수반됩니다. 집을 깔끔하게 유지하려는 것과 같은 이유로 정리되지 않은 코드를 생성하려고합니다.

  • 클러 터가 많을수록 버그를 숨길 수있는 더 편리한 장소가 생겨 더 많은 버그가 발생합니다.
  • 잃어 버릴 위험이 적을 때 찾고있는 것을 쉽게 찾을 수 있습니다.

코드 냄새는 무엇입니까?

"코드 냄새는 일반적으로 시스템의 더 깊은 문제에 해당하는 표면 표시입니다."~ Martin Fowler

코드 냄새는 무언가가 잘못되었거나 무언가를 바로 고쳐야한다는 것을 의미하지는 않습니다. 경험을 향상시킬 수있는 기회를 경고하는 것은 경험 법칙입니다.

이 텍스트와 제목은 모든 조롱이 나쁘거나 절대 조롱해서는 안된다는 것을 암시하지 않습니다.

또한 다른 유형의 코드에는 다른 수준 (및 다른 종류) 모의가 필요합니다. 일부 코드는 주로 I / O를 용이하게하기 위해 존재하며,이 경우 테스트 I / O 이외의 작업은 거의 없으며 모의 수를 줄이면 단위 테스트 범위가 0에 가까워 질 수 있습니다.

코드에 로직이없는 경우 (파이프 및 순수 컴포지션 만 해당) 통합 또는 기능 테스트 범위가 100 %에 가깝다고 가정하면 0 % 단위 테스트 범위를 사용할 수 있습니다. 그러나 논리 (조건식, 변수에 대한 대입, 단위에 대한 명시 적 함수 호출 등)가있는 경우에는 단위 테스트 적용 범위가 필요할 수 있으며 코드를 단순화하고 조롱 요구 사항을 줄일 수있는 기회가있을 수 있습니다.

모의 란 무엇입니까?

모의는 단위 테스트 프로세스 동안 실제 구현 코드를 나타내는 테스트 이중입니다. 모의는 테스트 실행 중에 테스트 대상이 어떻게 조작했는지에 대한 주장을 생성 할 수 있습니다. 테스트에서 이중이 단언을 생성하는 경우 특정 단어의 의미에서 모의입니다.

"모의 (mock)"라는 용어는 또한 모든 종류의 테스트 이중 사용을 지칭하기 위해 더 일반적으로 사용된다. 이 텍스트의 목적을 위해, 우리는“mock”과“test double”이라는 단어를 서로 바꾸어 널리 사용하여 사용합니다. 모든 테스트 복식 (인형, 스파이, 위조 등)은 피실험자가 단단히 결합 된 실제 코드를 나타내므로 모든 테스트 복식은 커플 링의 표시이므로 구현을 단순화하고 개선 할 수있는 기회가있을 수 있습니다 테스트중인 코드의 품질 동시에, 조롱 할 필요가 없기 때문에 조롱 할 필요가 없기 때문에 테스트 자체를 크게 단순화 할 수 있습니다.

단위 테스트 란 무엇입니까?

단위 테스트는 나머지 프로그램과 별도로 개별 단위 (모듈, 기능, 클래스)를 테스트합니다.

두 개 이상의 장치 간의 통합을 테스트하는 통합 테스트와 시뮬레이션 UI 조작에서 데이터 계층 업데이트에 이르는 완벽한 사용자 상호 작용 워크 플로를 포함하여 사용자 관점에서 응용 프로그램을 테스트하는 기능 테스트를 통한 대조 단위 테스트 사용자 출력에 (예를 들어, 앱의 화면 상 표현). 기능 테스트는 실행중인 애플리케이션의 컨텍스트에 통합 된 애플리케이션의 모든 단위를 테스트하기 때문에 통합 테스트의 서브 세트입니다.

일반적으로 장치는 장치의 공용 인터페이스 ( "공용 API"또는 "표면 영역") 만 사용하여 테스트됩니다. 이를 블랙 박스 테스트라고합니다. 블랙 박스 테스트는 단위의 구현 세부 사항이 단위의 공용 API보다 시간이 지남에 따라 더 많이 변경되는 경향이 있기 때문에 취성 테스트가 줄어 듭니다. 테스트에서 구현 세부 사항을 알고있는 화이트 박스 테스트를 사용하는 경우 공개 API가 계속 예상대로 작동하더라도 구현 세부 사항을 변경하면 테스트가 중단 될 수 있습니다. 다시 말해, 화이트 박스 테스트는 재 작업을 낭비합니다.

테스트 범위는 무엇입니까?

코드 범위는 테스트 사례에서 다루는 코드의 양을 나타냅니다. 적용 범위 보고서는 코드를 계측하고 테스트 실행 중에 어떤 라인이 운동했는지 기록하여 만들 수 있습니다. 일반적으로 높은 수준의 적용 범위를 만들려고하지만 코드 적용 범위는 100 %에 가까워 질수록 감소하는 수익을 제공하기 시작합니다.

내 경험상 ~ 90 % 이상으로 커버리지를 늘리면 버그 밀도가 낮을수록 지속적인 상관 관계가 거의없는 것 같습니다.

왜 그런가요? 100 % 테스트 된 코드가 코드가 의도 한대로 작동한다는 것을 100 % 확실하게 알고있는 것은 아닙니다.

그렇게 간단하지는 않습니다.

대부분의 사람들이 인식하지 못하는 것은 두 종류의 범위가 있다는 것입니다.

  1. 코드 적용 범위 : 얼마나 많은 코드가 사용되는지,
  2. 사례 적용 범위 : 테스트 스위트가 사용하는 사용 사례 수

사례 적용 범위는 실제 사용 환경, 실제 사용자, 실제 네트워크 및 해커가 의도적으로 악의적 인 목적으로 소프트웨어의 설계를 파괴하려는 의도로 실제 환경에서 코드가 작동하는 방식을 말합니다.

적용 범위 보고서는 사례 적용 취약점이 아니라 코드 적용 취약점을 식별합니다. 동일한 코드가 둘 이상의 사용 사례에 적용될 수 있으며 단일 사용 사례는 테스트 대상 외부의 코드 또는 별도의 응용 프로그램 또는 타사 API에 따라 달라질 수 있습니다.

유스 케이스에는 환경, 다중 장치, 사용자 및 네트워킹 조건이 포함될 수 있으므로 필요한 모든 유스 케이스를 단위 테스트 만 포함 된 테스트 스위트로 처리하는 것은 불가능합니다. 단위 테스트는 통합이 아닌 독립적 인 테스트 단위로 테스트합니다. 즉, 단위 테스트 만 포함하는 테스트 스위트는 통합 및 기능적 사용 사례 시나리오에서 항상 0 %에 가까운 케이스 범위를 갖습니다.

100 % 코드 범위는 100 % 케이스 범위를 보장하지 않습니다.

100 % 코드 범위를 대상으로하는 개발자는 잘못된 메트릭을 쫓고 있습니다.

타이트 커플 링이란 무엇입니까?

장치 테스트를 위해 장치 분리를 달성하기 위해 조롱해야 할 필요성은 장치 사이의 커플 링으로 인해 발생합니다. 긴밀한 결합으로 코드가 더 단단하고 부서지기 쉬워집니다. 변경이 필요할 때 깨질 수 있습니다. 일반적으로 코드 확장 및 유지 관리가 더 쉬워 지므로 자체 결합에 대한 결합이 적습니다. 모의 필요성을 제거하여 테스트를 더 쉽게한다는 사실은 케이크에 착빙하는 것입니다.

이를 통해 우리가 무언가를 조롱하는 경우 단위 간의 결합을 줄임으로써 코드를보다 유연하게 만들 수있는 기회가있을 수 있습니다. 완료되면 더 이상 모의가 필요하지 않습니다.

커플 링은 코드 단위 (모듈, 기능, 클래스 등)가 다른 코드 단위에 의존하는 정도입니다. 긴밀한 결합 또는 높은 수준의 결합은 종속성이 변경 될 때 장치가 파손될 가능성을 나타냅니다. 다시 말해, 커플 링이 단단할수록 애플리케이션을 유지 또는 확장하기가 더 어려워집니다. 느슨한 결합은 버그 수정 및 응용 프로그램을 새로운 사용 사례에 맞게 조정하는 복잡성을 줄입니다.

커플 링은 다른 형태를 취합니다.

  • 서브 클래스 커플 링 : 서브 클래스는 부모 클래스의 구현과 전체 계층 구조에 따라 달라집니다. OO 디자인에서 사용 가능한 가장 밀접한 형태의 커플 링입니다.
  • 제어 종속성 : 메소드 이름 전달 등과 같이 수행 할 작업을 지시하여 종속 항목을 제어하는 ​​코드 ... 종속성의 제어 API가 변경되면 종속 코드가 중단됩니다.
  • 변경 가능한 상태 종속성 : 변경 가능한 상태를 다른 코드와 공유하는 코드 (예 : 공유 객체의 속성을 변경할 수 있음) 돌연변이의 상대 타이밍이 변경되면 종속 코드가 손상 될 수 있습니다. 타이밍이 결정적이지 않은 경우, 모든 종속 유닛을 완전히 점검하지 않고 프로그램 정확성을 달성하는 것이 불가능할 수 있습니다. 예를 들어, 경쟁 조건이 돌이킬 수없는 복잡 할 수 있습니다. 하나의 버그를 수정하면 다른 종속 장치에 다른 버그가 나타날 수 있습니다.
  • 상태 형태 의존성 : 다른 코드와 데이터 구조를 공유하고 구조의 서브 세트 만 사용하는 코드. 공유 구조의 모양이 변경되면 종속 코드가 손상 될 수 있습니다.
  • 이벤트 / 메시지 커플 링 : 메시지 전달, 이벤트 등을 통해 다른 장치와 통신하는 코드

단단한 결합의 원인은 무엇입니까?

단단한 커플 링에는 많은 원인이 있습니다.

  • 돌연변이 대 불변성
  • 부작용 대 순도 / 절연 부작용
  • 책임 과부하 및 Do One Thing (DOT)
  • 절차 적 지시 vs 구조 설명
  • 클래스 상속 대 구성

명령형 및 객체 지향 코드는 기능 코드보다 긴밀한 결합에 더 취약합니다. 그렇다고해서 기능적 스타일로 프로그래밍하는 것이 코드를 밀접한 결합에 영향을 미치지는 않지만 기능적 코드는 구성 요소 단위로 순수한 기능을 사용하며 순수한 기능은 본질적으로 밀접한 결합에 덜 취약합니다.

순수한 기능 :

  • 동일한 입력이 주어지면 항상 동일한 출력을 반환하고
  • 부작용 없음

순수한 기능은 어떻게 결합을 줄입니까?

  • 불변성 : 순수한 함수는 기존 값을 변경하지 않습니다. 대신 새로운 것을 돌려줍니다.
  • 부작용 없음 : 순수 함수의 관찰 가능한 유일한 효과는 반환 값이므로 화면, DOM, 콘솔, 표준 출력과 같은 외부 상태를 관찰하는 다른 함수의 작동을 방해 할 가능성이 없습니다. , 네트워크 또는 디스크.
  • 한 가지 일 : 순수한 함수 한 가지 일 : 객체와 클래스 기반 코드를 괴롭히는 책임 과부하를 피하면서 일부 입력을 해당 출력에 매핑합니다.
  • 지침이 아닌 구조 : 순수한 함수는 안전하게 메모 할 수 있습니다. 즉, 시스템에 무한 메모리가있는 경우 순수한 함수는 함수 입력을 인덱스로 사용하여 테이블에서 해당 값을 검색하는 조회 테이블로 대체 될 수 있습니다. 다시 말해, 순수한 기능은 컴퓨터가 따라야하는 지침이 아니라 데이터 간의 구조적 관계를 설명하므로, 동시에 실행되는 서로 다른 두 개의 상충되는 지침 세트는 서로의 발끝을 밟아 문제를 일으킬 수 없습니다.

컴포지션은 조롱과 어떤 관련이 있습니까?

모두. 모든 소프트웨어 개발의 본질은 큰 문제를 더 작고 독립적 인 조각으로 분해하고 (분해) 솔루션을 함께 구성하여 큰 문제 (조성)를 해결하는 응용 프로그램을 형성하는 프로세스입니다.

분해 전략이 실패한 경우 조롱이 필요합니다.

큰 문제를 작은 부분으로 나누는 데 사용되는 장치가 서로 의존 할 때 조롱이 필요합니다. 달리 말하면, 우리의 추정 ​​원자 단위가 실제로 원자 적이 지 않을 때 조롱이 필요하며, 우리의 분해 전략이 더 큰 문제를 더 작은 독립적 인 문제로 분해하지 못했습니다.

분해가 성공하면 일반 컴포지션 유틸리티를 사용하여 조각을 다시 구성 할 수 있습니다. 예 :

  • 기능 구성 (예 : lodash / fp / compose)
  • 구성 요소 구성 (예 : 기능 구성으로 고차 구성 요소 작성)
  • 상태 저장소 / 모델 구성 (예 : Redux 겸용 감속기)
  • 믹스 인 또는 기능성 믹스 인과 같은 객체 또는 공장 구성
  • 트랜스 듀서와 같은 공정 구성
  • Promise 또는 Monadic 컴포지션 (예 : asyncPipe (), composeM ()이 포함 된 Kleisli 컴포지션, composeK () 등)
  • 기타…

일반 컴포지션 유틸리티를 사용하면 컴포지션의 각 요소를 다른 요소를 조롱하지 않고 개별적으로 테스트 할 수 있습니다.

컴포지션 자체는 선언적이므로 단위 테스트 가능 논리가 0으로 포함됩니다 (아마도 컴포지션 유틸리티는 자체 단위 테스트가 포함 된 타사 라이브러리 임).

이러한 상황에서는 단위 테스트에 의미가 없습니다. 대신 통합 테스트가 필요합니다.

익숙한 예를 사용하여 명령형과 선언적 구성을 대조해 보겠습니다.

// 함수 구성 OR
// 'lodash / fp / flow'에서 파이프 가져 오기;
const 파이프 = (... fns) => x => fns.reduce ((y, f) => f (y), x);
// 작성하는 함수
const g = n => n + 1;
const f = n => n * 2;
// 명령 구성
const doStuffBadly = x => {
  const afterG = g (x);
  const afterF = f (afterG);
  after afterF;
};
// 선언적 구성
const doStuffBetter = 파이프 (g, f);
console.log (
  doStuffBadly (20), // 42
  doStuffBetter (20) // 42
);

함수 구성은 함수를 다른 함수의 반환 값에 적용하는 프로세스입니다. 다시 말해, 함수 파이프 라인을 생성 한 다음 값을 파이프 라인으로 전달하면 값이 어셈블리 라인의 스테이지처럼 각 함수를 통과하여 다음 함수로 전달되기 전에 어떤 방식 으로든 값을 변환합니다. 관로. 결국 파이프 라인의 마지막 함수는 최종 값을 반환합니다.

initialValue-> [g]-> [f]-> 결과

패러다임에 관계없이 모든 주류 언어로 응용 프로그램 코드를 구성하는 주요 수단입니다. Java조차도 다른 클래스 인스턴스 간의 기본 메시지 전달 메커니즘으로 함수 (메소드)를 사용합니다.

함수를 수동 (제한적) 또는 자동 (선언적)으로 작성할 수 있습니다. 일급 기능이없는 언어에서는 선택의 여지가 없습니다. 당신은 명령에 갇혀 있습니다. JavaScript (및 거의 모든 다른 주요 인기 언어)에서는 선언적 구성으로 더 잘 수행 할 수 있습니다.

명령형 스타일이란 컴퓨터가 단계별로 작업을 수행하도록 명령하고 있음을 의미합니다. 사용법을 안내합니다. 위의 예에서 명령형 스타일은 다음과 같습니다.

  1. 인수를 취하여 x에 할당
  2. afterG라는 바인딩을 만들고 g (x)의 결과를 할당
  3. afterF라는 바인딩을 만들고 f (afterG)의 결과를 할당
  4. afterF의 값을 반환합니다.

명령형 버전에는 테스트해야 할 논리가 필요합니다. 나는 이것이 단순한 할당이라는 것을 알고 있지만 잘못된 변수를 전달하거나 반환하는 버그를 자주 보았습니다.

선언적 스타일은 컴퓨터간에 사물 간의 관계를 알리는 것을 의미합니다. 방정식 추론을 사용한 구조에 대한 설명입니다. 선언적 예는 다음과 같습니다.

  • doStuffBetter는 g와 f의 파이프 구성입니다.

그게 다야

f와 g에 자체 단위 테스트가 있고 pipe ()에 자체 단위 테스트가 있다고 가정하면 (Lodash의 flow () 또는 Ramda의 pipe () 사용) 단위 테스트에 대한 새로운 논리는 없습니다.

이 스타일이 제대로 작동하려면 우리가 구성하는 단위를 분리해야합니다.

커플 링을 어떻게 제거합니까?

커플 링을 제거하려면 먼저 커플 링 종속성의 출처를 더 잘 이해해야합니다. 커플 링의 조임 순서는 다음과 같습니다.

단단한 커플 링 :

  • 클래스 상속 (커플 링에는 각 상속 계층과 각 하위 클래스가 곱 해짐)
  • 글로벌 변수
  • 기타 가변 글로벌 상태 (브라우저 DOM, 공유 스토리지, 네트워크 등)
  • 부작용이있는 모듈 가져 오기
  • 컴포지션의 암시 적 종속성 (예 : const enhancedWidgetFactory = compose (eventEmitter, widgetFactory, enhancements); widgetFactory는 eventEmitter에 의존합니다.
  • 의존성 주입 컨테이너
  • 의존성 주입 매개 변수
  • 제어 매개 변수
  • 가변 매개 변수

느슨한 결합:

  • 부작용없이 모듈 가져 오기 (블랙 박스 테스트에서 모든 가져 오기가 분리 될 필요는 없음)
  • 메시지 전달 / pubsub
  • 변경할 수없는 매개 변수 (여전히 상태 형태에 대한 종속성을 공유 할 수 있음)

아이러니하게도, 대부분의 커플 링 소스는 원래 커플 링을 줄 이도록 설계된 메커니즘입니다. 작은 문제 해결 솔루션을 완전한 응용 프로그램으로 재구성하려면 어떻게 든 통합하고 통신해야하기 때문에 이치에 맞습니다. 좋은 방법과 나쁜 방법이 있습니다. 가능할 때마다 긴밀한 결합을 유발하는 원인을 피해야합니다. 느슨한 커플 링 옵션은 일반적으로 건전한 응용 분야에서 바람직합니다.

너무 많은 서적과 블로그 게시물이“느슨한 커플 링”으로 분류 할 때“tight coupling”그룹에서 종속성 주입 컨테이너와 종속성 주입 매개 변수를 분류했다고 혼동 될 수 있습니다. 커플 링은 바이너리가 아닙니다. 그래디언트 스케일입니다. 이는 모든 그룹화가 다소 주관적이고 임의적이라는 것을 의미합니다.

간단하고 객관적인 리트머스 테스트로 선을 그립니다.

의존성을 조롱하지 않고 장치를 테스트 할 수 있습니까? 그것이 가능하지 않다면, 그것은 조롱 된 의존성과 밀접하게 관련되어 있습니다.

장치의 종속성이 많을수록 커플 링에 문제가있을 가능성이 높습니다. 이제 커플 링이 어떻게 발생하는지 이해 했으니 어떻게해야합니까?

  1. 클래스, 명령형 프로 시저 또는 변경 함수와 달리 순수한 함수를 구성의 원자 단위로 사용하십시오.
  2. 나머지 프로그램 논리에서 부작용을 분리하십시오. 이는 논리를 I / O (네트워크 I / O, 렌더링 UI, 로깅 등 포함)와 혼합하지 않음을 의미합니다.
  3. 명령 구성에서 종속 논리를 제거하여 자체 단위 테스트가 필요없는 선언적 구성이 될 수 있습니다. 논리가 없으면 단위 테스트에 의미가 없습니다.

즉, 네트워크 요청 및 요청 처리기를 설정하는 데 사용하는 코드에는 단위 테스트가 필요하지 않습니다. 대신 통합 테스트를 사용하십시오.

그것은 반복된다 :

단위 테스트 I / O를하지 마십시오.
I / O는 통합을위한 것입니다. 대신 통합 테스트를 사용하십시오.

통합 테스트를 위해 모의하고 위조하는 것은 완벽합니다.

순수한 기능 사용

순수한 함수를 사용하는 것은 약간의 연습이 필요하며, 그 연습 없이는 원하는 것을 수행하기 위해 순수한 함수를 작성하는 방법이 항상 명확하지는 않습니다. 순수한 함수는 전역 변수, 전달 된 인수, 네트워크, 디스크 또는 화면을 직접 변경할 수 없습니다. 그들이 할 수있는 것은 값을 돌려주는 것입니다.

배열이나 객체를 전달했는데 해당 객체의 변경된 버전을 반환하려는 경우 객체를 변경하여 반환 할 수 없습니다. 필요한 변경 사항으로 새 오브젝트 사본을 작성해야합니다. 비어있는 새 객체를 대상으로 사용하거나 배열 또는 객체 분산 구문을 사용하여 배열 접근 자 메서드 (돌연변이 메서드가 아님), Object.assign ()을 사용하여이를 수행 할 수 있습니다. 예를 들면 다음과 같습니다.

// 순수하지 않다
const signInUser = 사용자 => user.isSignedIn = true;
const foo = {
  이름 : '푸',
  isSignedIn : 거짓
};
// Foo가 돌연변이되었습니다
console.log (
  signInUser (foo), // true
  foo // {이름 : "Foo", isSignedIn : true}
);

대…

// 순수
const signInUser = user => ({... user, isSignedIn : true});
const foo = {
  이름 : '푸',
  isSignedIn : 거짓
};
// Foo가 돌연변이되지 않았습니다
console.log (
  signInUser (foo), // {이름 : "Foo", isSignedIn : true}
  foo // {이름 : "Foo", isSignedIn : 거짓}
);

또는 Mori 또는 Immutable.js와 같은 변경 불가능한 데이터 유형에 대한 라이브러리를 시도 할 수 있습니다. 언젠가 JavaScript에서 Clojure와 유사한 멋진 불변 데이터 유형을 얻을 수 있기를 희망하지만 숨을 쉬지 않습니다.

새로운 객체를 반환하면 기존 객체를 재사용하는 대신 새로운 객체를 생성하기 때문에 성능 저하가 발생할 수 있다고 생각할 수 있지만 다행히도 ID 비교를 사용하여 객체의 변경 사항을 감지 할 수 있습니다 (= == 확인)이므로 변경 사항이 있는지 확인하기 위해 전체 객체를 탐색 할 필요가 없습니다.

복잡한 상태 트리를 사용하여 각 렌더 패스와 함께 깊이 이동할 필요가없는 경우이 트릭을 사용하여 React 구성 요소를 더 빠르게 렌더링 할 수 있습니다. PureComponent에서 상속되며 얕은 prop 및 state 비교로 shouldComponentUpdate ()를 구현합니다. 신원 평등을 감지하면 상태 트리의 해당 부분에서 아무것도 변경되지 않았으며 깊은 상태 순회없이 이동할 수 있습니다.

순수한 함수도 메모 할 수 있습니다. 즉, 동일한 입력을 본 적이 있으면 전체 오브젝트를 다시 만들 필요가 없습니다. 메모리에 대한 계산 복잡성을 교환하고 미리 계산 된 값을 조회 테이블에 저장할 수 있습니다. 무제한 메모리를 필요로하지 않는 계산 비용이 많이 드는 프로세스의 경우 이는 훌륭한 최적화 전략 일 수 있습니다.

순수한 함수의 또 다른 속성은 부작용이 없기 때문에 분할 및 정복 전략을 사용하여 대규모 프로세서 클러스터에 복잡한 계산을 안전하게 배포 할 수 있다는 것입니다. 이 기법은 원래 그래픽 용으로 설계된 대규모 병렬 GPU를 사용하여 이미지, 비디오 또는 오디오 프레임을 처리하는 데 종종 사용되지만 현재는 과학 컴퓨팅과 같은 다른 많은 목적으로 사용됩니다.

다시 말해, 돌연변이가 항상 빠른 것은 아니며 매크로 최적화를 희생하면서 미세 최적화를 수행하기 때문에 종종 수십 배 느립니다.

나머지 프로그램 논리에서 부작용을 격리

부작용을 나머지 프로그램 논리와 분리하는 데 도움이되는 몇 가지 전략이 있습니다. 다음은 그중 일부입니다.

  1. pub / sub를 사용하여 뷰와 프로그램 로직에서 I / O를 분리하십시오. UI 뷰 또는 프로그램 로직에서 부작용을 직접 트리거하는 대신 이벤트 또는 의도를 설명하는 이벤트 또는 조치 오브젝트를 생성하십시오.
  2. I / O에서 논리를 분리합니다 (예 : asyncPipe ()를 사용하여 약속을 반환하는 함수 작성).
  3. I / O로 계산을 직접 트리거하는 대신 미래 계산을 나타내는 객체를 사용하십시오. 예를 들어 redux-saga의 call ()은 실제로 함수를 호출하지 않습니다. 대신 함수와 인수에 대한 참조가있는 객체를 반환하고 saga 미들웨어가이를 호출합니다. 이를 통해 call () 및 순수한 함수를 사용하는 모든 함수를 만들 수 있습니다.이 함수는 조롱없이 단위 테스트가 쉽습니다.

펍 / 서브 사용

발행 / 구독은 발행 / 구독 패턴의 줄임말입니다. 게시 / 구독 패턴에서 단위는 서로 직접 호출하지 않습니다. 대신 다른 장치 (가입자)가들을 수있는 메시지를 게시합니다. 게시자는 구독 할 유닛 (있는 경우)을 알 수 없으며 구독자는 게시자가 무엇을 게시 할 것인지 알 수 없습니다.

Pub / sub는 DOM (Document Object Model)에 구워집니다. 응용 프로그램의 모든 구성 요소는 마우스 이동, 클릭, 스크롤 이벤트, 키 입력 등과 같은 DOM 요소에서 전달 된 이벤트를 수신 할 수 있습니다. 모든 사람들이 jQuery로 웹 앱을 만들었을 때, jQuery 커스텀 이벤트는 DOM을 pub / sub 이벤트 버스로 전환하여 상태 렌더링에서 뷰 렌더링 문제를 분리하는 것이 일반적이었습니다.

펍 / 서브도 Redux로 구워집니다. Redux에서는 애플리케이션 상태 (스토어라고 함)에 대한 글로벌 모델을 작성합니다. 뷰 및 I / O 핸들러는 모델을 직접 조작하는 대신 조치 오브젝트를 상점에 전달합니다. 액션 객체에는 다양한 리듀서가 수신하고 응답 할 수있는 유형이라는 특수 키가 있습니다. 또한 Redux는 미들웨어를 지원하며 특정 미들웨어를 수신하고 이에 대응할 수 있습니다. 이런 식으로 뷰는 애플리케이션 상태가 처리되는 방식에 대해 아무것도 알 필요가 없으며 상태 로직은 뷰에 대해 아무것도 알 필요가 없습니다.

또한 미들웨어를 통해 디스패처로 패치하고 조치 로깅 / 분석, 스토리지 또는 서버와의 상태 동기화, 서버 및 네트워크 피어와의 실시간 통신 기능 패치와 같은 교차 절단 문제를 트리거하는 것이 쉽지 않습니다.

I / O에서 로직 분리

때로는 약속과 같은 모나드 컴포지션을 사용하여 컴포지션에서 종속 논리를 제거 할 수 있습니다. 예를 들어 다음 함수에는 모든 비동기 함수를 조롱하지 않고서는 단위 테스트를 할 수없는 논리가 포함되어 있습니다.

비동기 함수 uploadFiles ({사용자, 폴더, 파일}) {
  const dbUser = 대기 readUser (사용자);
  const folderInfo = 대기 getFolderInfo (폴더);
  if (await haveWriteAccess ({dbUser, folderInfo})) {
    return uploadToFolder ({dbUser, folderInfo, 파일});
  } else {
    새 오류 발생 ( "해당 폴더에 대한 쓰기 권한이 없습니다");
  }
}

도우미 의사 코드를 던져 실행 가능하게 만듭니다.

const log = (... args) => console.log (... args);
// 이것을 무시하십시오. 실제 코드에서는 가져올 것입니다
// 진짜.
const readUser = () => Promise.resolve (true);
const getFolderInfo = () => Promise.resolve (true);
const haveWriteAccess = () => Promise.resolve (true);
const uploadToFolder = () => Promise.resolve ( 'Success!');
// 횡설수설 시작 변수
const 사용자 = '123';
const 폴더 = '456';
const 파일 = [ 'a', 'b', 'c'];
비동기 함수 uploadFiles ({사용자, 폴더, 파일}) {
  const dbUser = await readUser ({사용자});
  const folderInfo = await getFolderInfo ({폴더});
  if (await haveWriteAccess ({dbUser, folderInfo})) {
    return uploadToFolder ({dbUser, folderInfo, 파일});
  } else {
    새 오류 발생 ( "해당 폴더에 대한 쓰기 권한이 없습니다");
  }
}
uploadFiles ({사용자, 폴더, 파일})
  .then (로그)
;

이제 asyncPipe ()를 통해 promise 구성을 사용하도록 리팩터링합니다.

const asyncPipe = (... fns) => x => (
  fns.reduce (비동기 (y, f) => f (y 대기), x)
);
const uploadFiles = asyncPipe (
  readUser,
  getFolderInfo,
  haveWriteAccess,
  uploadToFolder
);
uploadFiles ({사용자, 폴더, 파일})
  .then (로그)
;

약속에는 조건 분기가 내장되어 있기 때문에 조건 논리가 쉽게 제거됩니다. 아이디어는 논리와 I / O가 잘 섞이지 않기 때문에 I / O 종속 코드에서 논리를 제거하려고합니다.

이러한 종류의 구성을 작동 시키려면 다음 두 가지를 보장해야합니다.

  1. haveWriteAccess ()는 사용자에게 쓰기 권한이 없으면 거부합니다. 조건부 논리를 약속 컨텍스트로 옮기므로 단위 테스트 또는 전혀 걱정할 필요가 없습니다 (JS 엔진 코드에 자체 테스트가 있음을 약속합니다).
  2. 이러한 각 함수는 동일한 데이터 유형으로 가져와 해결됩니다. 이 컴포지션에 대해 {user, folder, files, dbUser ?, folderInfo? 키를 포함하는 객체 인 pipelineData 유형을 만들 수 있습니다. }. 이렇게하면 구성 요소간에 구조 공유 종속성이 생성되지만 다른 위치에서 이러한 기능의보다 일반적인 버전을 사용하고 얇은 줄 바꿈 기능으로이 파이프 라인에 특화 할 수 있습니다.

이러한 조건이 충족되면 다른 기능을 조롱하지 않고 각 기능을 서로 독립적으로 테스트하는 것이 쉽지 않습니다. 파이프 라인에서 모든 논리를 추출 했으므로이 파일에서 단위 테스트를 수행 할 의미가 없습니다. 테스트해야 할 것은 통합입니다.

기억하십시오 : 논리와 I / O는 분리 된 관심사입니다.
논리는 생각하고 있습니다. 효과는 행동입니다. 행동하기 전에 생각하십시오!

향후 계산을 나타내는 객체 사용

redux-saga에서 사용하는 전략은 향후 계산을 나타내는 객체를 사용하는 것입니다. 이 아이디어는 모나드를 반환하는 것과 유사하지만 항상 반환되는 모나드 일 필요는 없습니다. Monad는 체인 작업으로 함수를 구성 할 수 있지만 대신 명령형 코드를 사용하여 수동으로 함수를 연결할 수 있습니다. 다음은 redux-saga가 수행하는 방식에 대한 대략적인 스케치입니다.

// 나중에 사용할 console.log의 sugar
const log = msg => console.log (msg);
const 호출 = (fn, ... args) => ({fn, args});
const put = (msg) => ({msg});
// I / O API에서 가져옴
const sendMessage = msg => Promise.resolve ( '일부 응답');
// 상태 핸들러 / Reducer에서 가져옴
const handleResponse = 응답 => ({
  유형 : 'RECEIVED_RESPONSE',
  페이로드 : 응답
});
const handleError = err => ({
  유형 : 'IO_ERROR',
  탑재량 : 오류
});
기능 * sendMessageSaga (msg) {
  {
    const 응답 = yield call (sendMessage, msg);
    수율 풋 (handleResponse (응답));
  } catch (err) {
    수율 put (handleError (err));
  }
}

네트워크 API를 조롱하거나 부작용을 유발하지 않고 단위 테스트에서 수행되는 모든 호출을 볼 수 있습니다. 보너스 : 결정적이지 않은 네트워크 상태 등을 걱정하지 않고 응용 프로그램을 디버깅하기가 매우 쉽습니다.

네트워크 오류가 발생할 때 앱에서 발생하는 상황을 시뮬레이션하고 싶습니까? iter.throw (NetworkError)를 호출하면됩니다.

다른 곳에서는 일부 라이브러리 미들웨어가 기능을 주도하고 실제로 프로덕션 애플리케이션에서 부작용을 유발합니다.

const iter = sendMessageSaga ( '안녕하세요, 세계!');
// 상태와 값을 나타내는 객체를 반환합니다.
const step1 = iter.next ();
로그 (단계 1);
/ * =>
{
  완료 : 거짓,
  값 : {
    fn : sendMessage
    인수 : [ "Hello, world!"]
  }
}
* /

향후 계산을 검사하거나 호출하기 위해 산출 된 값에서 call () 객체를 구조화합니다.

const {value : {fn, args}} = step1;

효과는 실제 미들웨어에서 실행됩니다. 테스트 및 디버깅시이 부분을 건너 뛸 수 있습니다.

const step2 = fn (args);
step2.then (로그); // "일부 응답"

API 또는 http 호출을 조롱하지 않고 네트워크 응답을 시뮬레이션하려는 경우 시뮬레이션 된 응답을 .next ()로 전달할 수 있습니다.

iter.next (simulatedNetworkResponse);

거기에서 완료가 완료되고 함수 실행이 완료 될 때까지 .next ()를 계속 호출 할 수 있습니다.

단위 테스트에서 생성기 및 계산 표현을 사용하여 실제 부작용을 제외하고 모든 것을 시뮬레이션 할 수 있습니다. 가짜 응답을 위해 .next () 호출에 값을 전달하거나 반복자에서 오류를 발생시켜 가짜 오류 및 거부를 약속 할 수 있습니다.

이 스타일을 사용하면 부작용이 많은 복잡한 통합 워크 플로에서도 단위 테스트에서 아무것도 조롱 할 필요가 없습니다.

"코드 냄새"는 법이 아니라 경고 신호입니다. mocks는 악하지 않습니다.

더 나은 아키텍처를 사용하는 것에 대한 모든 것은 훌륭하지만 실제로는 다른 사람들의 API를 사용하고 레거시 코드와 통합해야하며 순수하지 않은 API가 많이 있습니다. 이러한 경우에 분리 된 테스트 복식이 유용 할 수 있습니다. 예를 들어, express는 공유 변경 가능 상태를 전달하고 연속 전달을 통해 부작용을 모델링합니다.

일반적인 예를 살펴 보겠습니다. 사람들은 Express 서버 정의 파일에 의존성 주입이 필요하다고 말하려고합니다. Express 앱에 들어가는 모든 것을 단위로 어떻게 테스트합니까? 예 :

const express = require ( 'express');
const app = express ();
app.get ( '/', 함수 (req, res) {
  res.send ( 'Hello World!')
});
app.listen (3000, 함수 () {
  console.log ( '포트 3000에서 수신 대기하는 앱 예!')
});

이 파일을 "단위 테스트"하려면 의존성 주입 솔루션을 작업 한 다음 모든 것에 대한 모의를 전달해야합니다 (express () 자체 포함). 이것이 다른 요청 핸들러가 다른 Express 기능을 사용하고 거기에있는 논리를 계산하는 매우 복잡한 파일 인 경우 해당 작업을 수행하려면 꽤 정교한 가짜를 생각해 내야 할 것입니다. 개발자가 표현, 세션 미들웨어, 로그 처리기, 실시간 네트워크 프로토콜과 같은 정교한 가짜 및 모의를 만드는 것을 보았습니다. 나는 조롱하는 어려운 질문에 직면했지만 정답은 간단합니다.

이 파일은 단위 테스트가 필요하지 않습니다.

Express 앱의 서버 정의 파일은 기본적으로 앱의 주요 통합 지점입니다. Express 앱 파일 테스트는 정의에 의해 프로그램 논리, Express 및 해당 Express 앱의 모든 처리기 간의 통합을 테스트하는 것입니다. 100 % 단위 테스트 범위를 달성 할 수있는 경우에도 통합 테스트를 건너 뛰지 않아야합니다.

이 파일을 단위 테스트하는 대신 프로그램 논리를 별도의 단위로 분리하고 해당 파일을 단위 테스트하십시오. 서버 파일에 대한 실제 통합 테스트를 작성하십시오. 즉, 실제로 네트워크에 도달하거나 적어도 실제 http 메시지를 작성하고 supertest와 같은 도구를 사용하여 헤더로 완성하십시오.

Hello World Express 예제를 리팩토링하여 테스트하기 쉽게하겠습니다.

hello 처리기를 자체 파일로 가져 와서 단위 테스트를 작성하십시오. 나머지 앱 구성 요소를 조롱 할 필요가 없습니다. 이것은 분명히 순수한 함수가 아니므로 .send ()를 호출하기 위해 응답 객체를 감시하거나 조롱해야합니다.

const hello = (요청, 해상도) => res.send ( 'Hello World!');

이런 식으로 테스트 할 수 있습니다. 선호하는 테스트 프레임 워크 기대에 맞게 if 문을 교체하십시오.

{
  const 기대 = 'Hello World!';
  const msg =`$ {expect}로 .send ()를 호출해야합니다 .`;
  const res = {
    보내기 : (실제) => {
      if (실제! == 예상) {
        새 오류를 던지십시오 (`NOT OK $ {msg}`);
      }
      console.log (`OK : $ {msg}`);
    }
  }
  hello ({}, 입술);
}

청취 핸들러를 자체 파일로 가져 와서 단위 테스트를 작성하십시오. 여기에도 같은 문제가 있습니다. Express 처리기는 순수하지 않으므로 로거를 감시하여 호출기를 확인해야합니다. 테스트는 이전 예제와 유사합니다.

const handleListen = (로그, 포트) => () => log (`포트 $ {port}!`에서 수신 대기하는 예제 앱);

서버 파일에 남은 것은 통합 로직입니다.

const express = require ( 'express');
const hello = require ( './ hello.js');
const handleListen = require ( './ handleListen');
const log = require ( './ log');
const 포트 = 3000;
const app = express ();
app.get ( '/', hello);
app.listen (포트, handleListen (포트, 로그));

이 파일에 대한 통합 테스트는 여전히 필요하지만 추가 단위 테스트는 케이스 적용 범위를 의미있게 향상시키지 않습니다. 로거를 handleListen ()에 전달하기 위해 최소한의 의존성 주입을 사용하지만 명시 적 앱에는 의존성 주입 프레임 워크가 필요하지 않습니다.

모의는 통합 테스트에 좋습니다

통합 테스트는 장치 간의 협업 통합을 테스트하기 때문에 다른 장치와 통신하는 동안 발생할 수있는 모든 다양한 조건을 재현하기 위해 가짜 서버, 네트워크 프로토콜, 네트워크 메시지 등을 완벽하게 확인하는 것이 좋습니다. 네트워크에서 별도의 시스템.

때로는 단위가 타사 API와 통신하는 방법을 테스트하고 싶을 때도 있고 API가 실제 테스트에 막대한 비용이 드는 경우도 있습니다. 실제 서비스에 대한 실제 워크 플로우 트랜잭션을 기록하고 가짜 서버에서 재생하여 장치가 실제로 별도의 네트워크 프로세스에서 실행되는 타사 서비스와 얼마나 잘 통합되는지 테스트 할 수 있습니다. 종종 "올바른 메시지 헤더를 보았습니까?"와 같은 것을 테스트하는 가장 좋은 방법입니다.

네트워크 대역폭을 조절하고 네트워크 지연을 발생 시키며 네트워크 오류를 발생 시키거나 통신 계층을 모의하는 단위 테스트를 사용하여 테스트 할 수없는 다른 많은 조건을 테스트하는 유용한 통합 테스트 도구가 많이 있습니다.

통합 테스트없이 100 % 사례 범위를 달성하는 것은 불가능합니다. 100 % 단위 테스트 범위를 달성하더라도이를 건너 뛰지 마십시오. 때로는 100 %가 100 %가 아닙니다.

다음 단계

  • 모든 개발 팀이 Cross Cutting Concerns 팟 캐스트에서 TDD를 사용해야하는 이유를 알아보십시오.
  • JS 치어 리더는 인스 타 그램에서의 모험을 기록하고 있습니다.

EricElliottJS.com에서 자세히 알아보기

EricElliottJS.com 회원은 유닛 테스트에 대한 비디오 강의를 이용할 수 있습니다. 회원이 아닌 경우 오늘 가입하십시오.

Eric Elliott는“JavaScript 응용 프로그램 프로그래밍”(O'Reilly) 및“Eric Elliott로 JavaScript 배우기”의 저자입니다. 그는 Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC 및 Usher, Frank Ocean, Metallica 등을 포함한 최고의 레코딩 아티스트를위한 소프트웨어 경험에 기여했습니다.

그는 세계에서 가장 아름다운 여성과 어디에서나 원격으로 일합니다.