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


197

각도 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. 대부분의 개발자는 개인 기능을 테스트하지 않는다고 말하지만 기능이 잘못되었거나 옳다고 말하지는 않지만 개인의 경우를 테스트해야 할 필요성이 있습니다.


11
테스트는 개인 구현이 아닌 공용 인터페이스 만 테스트해야합니다. 공용 인터페이스에서 수행하는 테스트는 개인 부분도 포함해야합니다.
toskv

16
나는 답의 절반이 실제로 주석이어야하는 것을 좋아합니다. OP가 질문합니다. X는 어떻습니까? 받아 들여진 대답은 실제로 X를 수행하는 방법을 알려줍니다. 그러면 나머지 대부분이 돌아와서 말할 것입니다. X (명확하게 가능함)를 말할뿐만 아니라 Y를 수행해야합니다. 대부분의 단위 테스트 도구 (나는 아닙니다 JavaScript에 대해서만 이야기하면 개인 함수 / 메소드를 테스트 할 수 있습니다. 나는 왜 JS 랜드에서 잃어버린 것처럼 보이기 때문에 이유를 설명 할 것입니다 (분명히 절반의 대답이 주어짐).
쿼터니언

13
문제를 관리 가능한 작업으로 나누는 것이 좋은 프로그래밍 방법이므로 "foo (x : type)"함수는 개인 함수 a (x : type), b (x : type), c (y : another_type) 및 d ( z : yet_another_type). 이제 foo는 통화를 관리하고 물건을 모으기 때문에 스트림의 바위 뒷면과 같은 난기류를 생성합니다.이 그림자는 모든 범위를 테스트하기가 실제로 어렵습니다. 따라서 각 하위 범위 세트가 유효한지 확인하는 것이 더 쉽습니다. 상위 "foo"만 테스트하려고하면 경우에 따라 범위 테스트가 매우 복잡해집니다.
Quaternion

18
이것은 공개 인터페이스를 테스트하지 않는다는 것을 의미하는 것이 아니며 분명히 개인 메소드를 테스트하면 일련의 짧은 관리 가능한 청크를 테스트 할 수 있습니다 (처음에 작성한 청크와 동일한 이유, 실행 취소하려는 이유) 퍼블릭 인터페이스에 대한 테스트가 유효하기 때문에 (호출 기능이 입력 범위를 제한 할 수 있음) 고급 로직을 추가하고 다른 메소드에서 호출 할 때 프라이빗 메소드에 결함이 없음을 의미하지는 않습니다. 새로운 부모 함수
Quaternion

5
TDD를 사용하여 올바르게 테스트했다면 올바르게 테스트해야 할 때 나중에 뭘했는지 알아 내려고하지 않을 것입니다.
Quaternion

답변:


344

"공용 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으로 코드 냄새를 고려 하고 문서에서 말하는 것처럼 :

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


45
표준 단위 테스터 교리보다는 실제 솔루션과 함께 단위 테스트에 대한 현실적인 입장을 듣는 것이 좋습니다.
d512

2
(심지어 사용 사례로 단위 테스트를 인용) 동작의 일부 "공식적인"설명 : github.com/microsoft/TypeScript/issues/19335
아론 Beall

1
아래에 지적 된대로 // // @ -ts-ignore를 사용하십시오. 린터에게 개인 접근자를 무시하도록 지시
Tommaso

1
@Tommaso 그래, 그것은 또 다른 옵션이지만, 사용하는 것과 같은 단점이 있습니다 as any: 모든 유형 검사를 잃습니다.
Aaron Beall

2
@AaronBeall 덕분에 한동안 본 최고의 답변. 또한 원래 질문을 한 것에 대해 tymspy에게 감사드립니다.
nicolas.leblanc

27

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

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(/*...*/);

이 상단에 있어야합니다!
jsnewbie

2
이것은 확실히 다른 옵션입니다. as any유형 검사를 잃는 것과 동일한 문제가 발생합니다 . 실제로 전체 라인에서 유형 검사를 잃습니다.
Aaron Beall

20

대부분의 개발자 는 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에게 감사합니다.


1
개인 / 보호 된 메소드를 호출하려고 할 때 typescript에서 linting 오류가 발생한다고 생각합니다.
Gudgip

1
@Gudgip 형식 오류가 발생하고 컴파일되지 않습니다. :)
tymspy

10

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

  • 수업의 공개 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. 테스트를 쉽게하려면 문제의 논리를 자체 클래스로 이동해야합니다.

3
댓글을 달기 전에 게시물을 읽으십시오. 필자는 개인 테스트가 행동이 아니라 테스트 구현의 냄새라는 것을 분명히 말하고 입증하여 깨지기 쉬운 테스트로 이어진다.
Martin

1
0과 개인 속성 x 사이의 난수를 제공하는 객체를 상상해보십시오. 생성자가 x를 올바르게 설정했는지 알고 싶다면 수백 개의 테스트를 수행하여 숫자가 올바른 범위에 있는지 확인하는 것보다 x 값을 테스트하는 것이 훨씬 쉽습니다.
Galdor

1
@ user3725805 이것은 동작이 아니라 구현을 테스트하는 예제입니다. 상수, 구성, 생성자 및 개인 번호가 나오는 곳을 격리하는 것이 좋습니다. 개인이 다른 소스에서 온 것이 아니라면 "매직 넘버"반 패턴에 빠지게됩니다.
Martin

1
그리고 왜 구현을 테스트 할 수 없습니까? 단위 테스트는 예기치 않은 변경을 감지하는 데 좋습니다. 어떤 이유로 든 생성자가 숫자 설정을 잊어 버린 경우 테스트가 즉시 실패하고 경고합니다. 누군가 구현을 변경하면 테스트도 실패하지만 감지되지 않은 오류보다 하나의 테스트를 선호합니다.
Galdor

2
+1. 좋은 대답입니다. @TimJames 올바른 연습을 지시하거나 결함이있는 접근법을 지적하는 것이 바로 SO의 목적입니다. OP가 원하는 것을 달성하기 위해 해키 취약한 방법을 찾는 대신.
Syed Aqeel Ashiq

4

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

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]);
});

3

이것은 나를 위해 일했다 :

대신에:

sut.myPrivateMethod();

이:

sut['myPrivateMethod']();

2

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

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

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

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

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

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

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

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

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


2

나는 @toskv에 동의합니다 : 나는 그렇게하지 않는 것이 좋습니다 :-)

그러나 개인 메서드를 실제로 테스트하려는 경우 TypeScript에 해당하는 코드가 생성자 함수 프로토 타입의 메서드에 해당한다는 것을 알 수 있습니다. 즉, 런타임시 사용할 수 있습니다 (컴파일 오류가있을 수 있음).

예를 들면 다음과 같습니다.

export class FooBar {
  private _status: number;

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

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

다음으로 번역됩니다 :

(function(System) {(function(__moduleName){System.register([], function(exports_1, context_1) {
  "use strict";
  var __moduleName = context_1 && context_1.id;
  var FooBar;
  return {
    setters:[],
    execute: function() {
      FooBar = (function () {
        function FooBar(foo) {
          this.foo = foo;
          this.initFooBar({});
        }
        FooBar.prototype.initFooBar = function (data) {
          this.foo.bar(data);
          this._status = this.foo.foo();
        };
        return FooBar;
      }());
      exports_1("FooBar", FooBar);
    }
  }
})(System);

이 plunkr 참조 https://plnkr.co/edit/calJCF?p=preview을 .


1

많은 사람들이 이미 언급했듯이 개인 메소드를 테스트하려는 경우 코드 또는 코드 변환기를 해킹해서는 안됩니다. 현대의 TypeScript는 사람들이 지금까지 제공 한 대부분의 해킹을 거부합니다.


해결책

TLDR ; 메서드를 테스트해야하는 경우 코드를 클래스에 분리하여 테스트 할 수 있도록 메서드를 공개해야합니다.

private 메소드를 사용하는 이유는 기능이 해당 클래스에 의해 반드시 노출 될 필요는 없기 때문에 기능이 속하지 않으면 자체 클래스로 분리되어야합니다.

이 기사를 통해 개인 메소드 테스트를 어떻게 수행 해야하는지 설명하는 훌륭한 작업을 수행했습니다. 여기에있는 일부 메소드와 이들이 왜 구현이 나쁜지에 대해서도 다룹니다.

https://patrickdesjardins.com/blog/how-to-unit-test-private-method-in-typescript-part-2

참고 :이 코드는 위에 링크 된 블로그에서 해제되었습니다 (링크 뒤에있는 내용이 변경되는 경우 복제 중입니다)

전에
class User{
    public getUserInformationToDisplay(){
        //...
        this.getUserAddress();
        //...
    }

    private getUserAddress(){
        //...
        this.formatStreet();
        //...
    }
    private formatStreet(){
        //...
    }
}
class User{
    private address:Address;
    public getUserInformationToDisplay(){
        //...
        address.getUserAddress();
        //...
    }
}
class Address{
    private format: StreetFormatter;
    public format(){
        //...
        format.ToString();
        //...
    }
}
class StreetFormatter{
    public toString(){
        // ...
    }
}

1

대괄호를 사용하여 개인 메소드 호출

TS 파일

class Calculate{
  private total;
  private add(a: number) {
      return a + total;
  }
}

spect.ts 파일

it('should return 5 if input 3 and 2', () => {
    component['total'] = 2;
    let result = component['add'](3);
    expect(result).toEqual(5);
});

0

Aaron 의 답변 이 최고이며 나를 위해 노력하고 있습니다 :) 나는 투표하지만 슬프게도 나는 (평판을 놓칠 수 없다).

개인 메소드를 테스트하는 것이 메소드를 사용하는 유일한 방법이며 다른쪽에는 깨끗한 코드가 있습니다.

예를 들면 다음과 같습니다.

class Something {
  save(){
    const data = this.getAllUserData()
    if (this.validate(data))
      this.sendRequest(data)
  }
  private getAllUserData () {...}
  private validate(data) {...}
  private sendRequest(data) {...}
}

이러한 모든 메소드를 한 번에 테스트하지 않는 것이 합리적입니다. 개인 메소드를 조롱해야하기 때문에 액세스 할 수 없기 때문에 조롱 할 수 없습니다. 이것은 단위 테스트가 이것을 전체적으로 테스트하기 위해 많은 구성이 필요하다는 것을 의미합니다.

이것은 모든 의존성으로 위의 방법을 테스트하는 가장 좋은 방법은 엔드 투 엔드 테스트입니다. 통합 테스트가 필요하지만 E2E 테스트는 TDD (Test Driven Development)를 연습하는 경우 도움이되지 않지만 테스트 어떤 방법이든 가능합니다.


0

이 경로는 클래스 외부에서 함수를 만들고 함수를 내 개인 메서드에 할당하는 경로입니다.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

이제 어떤 유형의 OOP 규칙을 위반했는지 알지 못하지만 질문에 대답하기 위해 개인 메서드를 테스트하는 방법입니다. 나는 이것에 대해 찬반 양론을 조언하는 사람을 환영합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.