[angular] Jasmine으로 개인 메소드의 Angular / TypeScript에 대한 단위 테스트를 작성하는 방법

각도 2에서 개인 함수를 어떻게 테스트합니까?

class FooBar {

    private _status: number;

    constructor( private foo : Bar ) {
        this.initFooBar();

    }

    private initFooBar(){
        this.foo.bar( "data" );
        this._status = this.fooo.foo();
    }

    public get status(){
        return this._status;
    }

}

내가 찾은 해결책

  1. 테스트 코드 자체를 클로저 안에 넣거나 클로저 안에 코드를 추가하여 외부 범위에있는 기존 객체의 로컬 변수에 대한 참조를 저장합니다.

    나중에 도구를 사용하여 테스트 코드를 제거하십시오.
    http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

이 문제를 해결하는 더 좋은 방법을 제안하십시오.

추신

  1. 이와 비슷한 유형의 질문에 대한 대부분의 대답은 문제에 대한 해결책을 제공하지 않으므로이 질문을하는 이유입니다.

  2. 대부분의 개발자는 개인 기능을 테스트하지 않는다고 말하지만 기능이 잘못되었거나 옳다고 말하지는 않지만 개인의 경우를 테스트해야 할 필요성이 있습니다.



답변

“공용 API 만 단위 테스트”하는 것이 좋은 목표이지만, 간단하지 않은 경우가 있으며 API 또는 단위 테스트 중 하나를 타협하는 것 중에서 선택하는 느낌이 드는 경우가 있습니다. 당신은 이것이 바로 당신이 요구하는 것이기 때문에 이미 알고 있습니다. 그래서 나는 그것에 들어 가지 않을 것입니다. 🙂

TypeScript에서 단위 테스트를 위해 개인 멤버에 액세스 할 수있는 몇 가지 방법을 발견했습니다. 이 수업을 고려하십시오.

class MyThing {

    private _name:string;
    private _count:number;

    constructor() {
        this.init("Test", 123);
    }

    private init(name:string, count:number){
        this._name = name;
        this._count = count;
    }

    public get name(){ return this._name; }

    public get count(){ return this._count; }

}

클래스 멤버 TS를 제한 액세스를 사용하더라도 private, protected, public이 JS있는 것은 아니기 때문에, 컴파일 된 JS, 아니 개인 회원이 없습니다. TS 컴파일러에만 사용됩니다. 그 때문에:

  1. any액세스 제한에 대해 경고하지 않도록 컴파일러를 주장 하고 이스케이프 처리 할 수 있습니다 .

    (thing as any)._name = "Unit Test";
    (thing as any)._count = 123;
    (thing as any).init("Unit Test", 123);

    이 접근법의 문제점은 컴파일러가 단순히 당신이 무엇을하고 있는지 전혀 알지 any못하므로 원하는 유형 오류가 발생하지 않는다는 것입니다.

    (thing as any)._name = 123; // wrong, but no error
    (thing as any)._count = "Unit Test"; // wrong, but no error
    (thing as any).init(0, "123"); // wrong, but no error

    리팩토링이 더 어려워 질 것입니다.

  2. 배열 액세스 ( [])를 사용 하여 개인 멤버를 확보 할 수 있습니다 .

    thing["_name"] = "Unit Test";
    thing["_count"] = 123;
    thing["init"]("Unit Test", 123);

    펑키하게 보이지만 TSC는 실제로 직접 액세스 한 것처럼 유형을 확인합니다.

    thing["_name"] = 123; // type error
    thing["_count"] = "Unit Test"; // type error
    thing["init"](0, "123"); // argument error

    솔직히 말해서 이것이 왜 효과가 있는지 모르겠습니다. 이것은 의도적 인 “탈출 해치” 로, 유형 안전을 잃지 않으면 서 개인 회원에게 액세스 할 수 있습니다. 이것이 바로 당신이 단위 테스트를 원하는 것입니다.

다음은 TypeScript Playground실제 예제입니다 .

TypeScript 2.6 편집

다음과 같은 다른 옵션 은 다음 줄의 모든 오류를 간단히 억제하는 사용하는 것입니다 // @ts-ignore( TS 2.6에 추가됨 ).

// @ts-ignore
thing._name = "Unit Test";

이것의 문제는 다음 줄의 모든 오류를 억제한다는 것입니다.

// @ts-ignore
thing._name(123).this.should.NOT.beAllowed("but it is") = window / {};

나는 개인적 @ts-ignore으로 코드 냄새를 고려 하고 문서에서 말하는 것처럼 :

이 주석은 매우 드물게 사용하는 것이 좋습니다 . 【강조 오리지날】


답변

개인 메소드를 호출 할 수 있습니다 . 다음 오류가 발생한 경우 :

expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
// TS2341: Property 'initFooBar' is private and only accessible within class 'FooBar'

그냥 사용하십시오 // @ts-ignore:

// @ts-ignore
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);


답변

대부분의 개발자 는 private function 테스트를 권장하지 않으므로 테스트하지 않겠 습니까?.

예 :

YourClass.ts

export class FooBar {
  private _status: number;

  constructor( private foo : Bar ) {
    this.initFooBar({});
  }

  private initFooBar(data){
    this.foo.bar( data );
    this._status = this.foo.foo();
  }
}

TestYourClass.spec.ts

describe("Testing foo bar for status being set", function() {

...

//Variable with type any
let fooBar;

fooBar = new FooBar();

...
//Method 1
//Now this will be visible
fooBar.initFooBar();

//Method 2
//This doesn't require variable with any type
fooBar['initFooBar']();
...
}

@Aaron, @Thierry Templier에게 감사합니다.


답변

개인용 메소드에 대한 테스트를 작성하지 마십시오. 이것은 단위 테스트의 요점을 무너 뜨립니다.

  • 수업의 공개 API를 테스트해야합니다.
  • 수업의 함축 내용을 테스트해서는 안됩니다.

class SomeClass {

  public addNumber(a: number, b: number) {
      return a + b;
  }
}

나중에 구현이 변경되지만 behaviour퍼블릭 API는 동일하게 유지되는 경우이 메소드에 대한 테스트를 변경할 필요가 없습니다 .

class SomeClass {

  public addNumber(a: number, b: number) {
      return this.add(a, b);
  }

  private add(a: number, b: number) {
       return a + b;
  }
}

메서드와 속성을 테스트하기 위해 공개하지 마십시오. 이것은 일반적으로 다음 중 하나를 의미합니다.

  1. API (공용 인터페이스)가 아닌 구현을 테스트하려고합니다.
  2. 테스트를 쉽게하려면 문제의 논리를 자체 클래스로 이동해야합니다.

답변

“비공개 메소드를 테스트하지 마십시오”의 요점은 실제로 클래스를 사용하는 사람처럼 클래스를 테스트하는 것 입니다.

5 개의 메소드가있는 공용 API가있는 경우 클래스의 모든 소비자가이를 사용할 수 있으므로 테스트해야합니다. 소비자는 클래스의 개인 메서드 / 속성에 액세스해서는 안됩니다. 즉, 공개 노출 기능이 동일하게 유지되면 개인 멤버를 변경할 수 있습니다.


내부 확장 가능 기능에 의존하는 경우 protected대신을 사용하십시오 private.
참고 protected여전히입니다 공용 API는 (!) , 단지 다르게 사용.

class OverlyComplicatedCalculator {
    public add(...numbers: number[]): number {
        return this.calculate((a, b) => a + b, numbers);
    }
    // can't be used or tested via ".calculate()", but it is still part of your public API!
    protected calculate(operation, operands) {
        let result = operands[0];
        for (let i = 1; i < operands.length; operands++) {
            result = operation(result, operands[i]);
        }
        return result;
    }
}

소비자가 서브 클래 싱을 통해 사용하는 것과 동일한 방식으로 단위 테스트 보호 속성

it('should be extensible via calculate()', () => {
    class TestCalculator extends OverlyComplicatedCalculator {
        public testWithArrays(array: any[]): any[] {
            const concat = (a, b) => [].concat(a, b);
            // tests the protected method
            return this.calculate(concat, array);
        }
    }
    let testCalc = new TestCalculator();
    let result = testCalc.testWithArrays([1, 'two', 3]);
    expect(result).toEqual([1, 'two', 3]);
});


답변

이것은 나를 위해 일했다 :

대신에:

sut.myPrivateMethod();

이:

sut['myPrivateMethod']();


답변

이 게시물의 괴상한 점에 대해 죄송하지만 만지지 않은 것 몇 가지에 무게를 두어야한다고 생각합니다.

가장 먼저-단위 테스트 중 클래스의 개인 멤버에 액세스해야하는 경우 전략적 또는 전술적 접근 방식에서 실수를했으며 부주의하게 단일 책임 원칙을 위반하여 큰 책임을지었습니다. 속하지 않는 행동. 실제로 구축 절차의 분리 된 서브 루틴에 지나지 않는 메소드에 액세스 할 필요성을 느끼는 것이 가장 흔한 일 중 하나입니다. 그러나 상사는 당신이 출근 준비를 위해 출근하기를 기대하고 그 주에 당신을 데려 오기 위해 어떤 아침 일과를 겪어야했는지에 대해 약간의 괴로운 필요성을 느끼는 것과 같습니다.

이런 일이 발생하는 가장 일반적인 예는 자신이 속담 “신 클래스”를 테스트하려고 할 때입니다. 그것은 그 자체로는 특별한 종류의 문제이지만 절차의 세부적인 내용을 알아야하는 것과 동일한 기본 문제로 어려움을 겪지 만 주제를 벗어납니다.

이 특정 예제에서는 FooBar 클래스의 생성자에 Bar 객체를 완전히 초기화하는 책임을 효과적으로 할당했습니다. 객체 지향 프로그래밍에서 핵심 테넌트 중 하나는 생성자가 “신성한”상태이며 자체 내부 상태를 무효화하고 다운 스트림 다른 곳에서 실패 할 가능성이있는 잘못된 데이터로부터 보호해야한다는 것입니다. 관로.)

우리는 FooBar 객체가 FooBar가 생성 될 때 준비되지 않은 Bar를 받아들이고 FooBar 객체가 자체적으로 문제를 해결하기 위해 일종의 “해킹”에 의해 보상 된 바를 받아 들여서이를 수행하지 못했습니다. 소유.

이는 객체 지향 프로그래밍의 다른 테넌트 (Bar의 경우)를 준수하지 못한 결과로, 객체의 상태가 완전히 초기화되고 생성 된 직후 공개 멤버에게 들어오는 호출을 처리 할 준비가되어 있어야합니다. 이제 이것은 모든 인스턴스에서 생성자가 호출 된 직후를 의미하지는 않습니다. 복잡한 구성 시나리오가 많은 개체가있는 경우 생성자 디자인 패턴 (Factory, Builder 등)에 따라 구현 된 개체에 선택적 멤버의 설정자를 노출시키는 것이 좋습니다. 후자의 경우

귀하의 예에서 Bar의 “status”속성은 FooBar가이를 승인 할 수있는 유효한 상태가 아닌 것 같습니다. 따라서 FooBar는 해당 문제를 해결하기 위해 무언가를 수행합니다.

내가보고있는 두 번째 문제는 테스트 중심 개발을 연습하지 않고 코드를 테스트하려고한다는 것입니다. 이 시점에서 이것은 내 자신의 의견이다. 그러나 이러한 유형의 테스트는 실제로 반 패턴입니다. 결국 테스트는 필요한 테스트를 작성하고 테스트에 프로그래밍하는 것이 아니라 실제로 테스트 후에 코드를 테스트 할 수 없도록하는 핵심 디자인 문제가 있다는 사실을 깨닫게됩니다. 어떤 방식 으로든 문제가 발생하더라도 SOLID 구현을 실제로 달성 한 경우에도 동일한 수의 테스트와 코드 줄을 사용해야합니다. 그렇다면 개발 노력을 시작할 때 문제를 해결할 수있을 때 왜 테스트 가능한 코드로 리버스 엔지니어링하려고합니까?

그렇게했다면 디자인을 테스트하기 위해 다소 이상한 코드를 작성해야한다는 사실을 훨씬 일찍 깨달았을 것입니다. 행동을 구현으로 전환하여 접근 방식을 재조정 할 수있는 기회를 일찍 얻었을 것입니다. 쉽게 테스트 할 수 있습니다.