JasmineでプライベートメソッドのAngular / TypeScriptのユニットテストを書く方法


196

angular 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/

あなたが何かをしたなら、この問題を解決するより良い方法を私に提案してください?

PS

  1. このような同様のタイプの質問に対する答えのほとんどは問題の解決策を与えません、それが私がこの質問をしている理由です

  2. 開発者のほとんどは、プライベート関数をテストしないと言いますが、それらが間違っている、または正しいとは言いませんが、私のケースではプライベート関数をテストする必要があります。


11
テストは、プライベート実装ではなく、パブリックインターフェイスのみをテストする必要があります。パブリックインターフェイスで行うテストは、プライベート部分もカバーする必要があります。
toskv 2016年

16
答えの半分が実際にコメントであるべきなのが好きです。OPが質問します。Xはどうですか。受け入れられた答えは、実際にXの実行方法を示します。次に、残りのほとんどが向きを変えて言って、Xを通知しない(これは明らかに可能です)だけでなく、Yを実行する必要があります。ほとんどの単体テストツール(私はここではJavaScriptについてのみ言及しています)は、プライベート関数/メソッドをテストできます。JSランドで迷子になったように見える理由を説明します(どうやら、答えの半分が与えられています)。
クォータニオン2017年

13
問題を管理可能なタスクに分解することはプログラミングの実践に役立つため、関数「foo(x:type)」はプライベート関数a(x:type)、b(x:type)、c(y:another_type)およびd( z:yet_another_type)。fooが呼び出しを管理し、データをまとめているため、ストリーム内の岩の裏側、すべての範囲をテストすることを保証するのが非常に難しい影のような、一種の乱流が作成されます。そのため、範囲の各サブセットが有効であることを確認する方が簡単です。親の「foo」だけをテストしようとすると、範囲のテストが非常に複雑になります。
クォータニオン2017年

18
これは、パブリックインターフェイスをテストしないと言っているわけではありませんが、プライベートメソッドをテストすると、一連の短い管理可能なチャンクをテストできます(最初にそれらを書き込んだのと同じ理由で、なぜ元に戻すのですか?これは、テストに関しては)、そしてパブリックインターフェイスでのテストが有効である(呼び出し関数が入力範囲を制限している可能性がある)からといって、より高度なロジックを追加して他のメソッドから呼び出すときに、プライベートメソッドに欠陥がないわけではありません新しい親関数
クォータニオン2017年

5
それらをTDDで適切にテストした場合、それらを正しくテストする必要があるときに、後で何をしているのかを理解しようとはしません。
クォータニオン2017年

回答:


343

私はあなたと一緒にいますが、「パブリック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は、使用してクラスメンバーへのアクセスを制限していてもprivateprotectedpublicこれは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 2.6用に編集

次の行のすべてのエラーを抑制するだけの別のオプション// @ts-ignoreTS 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
Aaron Beall

1
以下で指摘するように、 `// @ ts-ignore`を使用するだけです。リンターにプライベートアクセサーを無視するように指示する
Tommaso

1
@Tommasoええ、それは別のオプションですが、使用するのと同じ欠点がありas anyます。型チェックがすべて失われます。
アーロンベア

2
私がしばらく見てきたベストアンサー、@ AaronBeallに感謝します。また、元の質問をしてくれたtymspyに感謝します。
nicolas.leblanc

26

プライベートメソッドを呼び出すことができます。次のエラーが発生した場合:

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型チェックが失われるのと同じ問題があり、実際には行全体で型チェックが失われます。
アーロンビール

19

ほとんどの開発者はプライベート関数のテストを推奨していないので、なぜそれをテストしないのですか?

例えば。

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 2018

1
@Gudgip型エラーが発生し、コンパイルされません。:)
tymspy 2018

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の値をテストする方がはるかに簡単です。
ガルドール

1
@ user3725805これは、動作ではなく実装のテストの例です。プライベート番号がどこから来るのかを特定する方が良いでしょう:定数、構成、コンストラクター-そしてそこからテストします。プライベートが他のソースからのものでない場合は、「マジックナンバー」アンチパターンに該当します。
マーティン

1
また、実装のテストが許可されないのはなぜですか?ユニットテストは、予期しない変更を検出するのに適しています。なんらかの理由でコンストラクタが数値を設定し忘れた場合、テストはすぐに失敗し、警告が表示されます。誰かが実装を変更するとテストも失敗しますが、未検出のエラーよりも1つのテストを採用することを好みます。
ガルドール

2
+1。すばらしい答えです。@TimJames正しい実践を伝えるか、欠陥のあるアプローチを指摘することが、SOのまさに目的です。OPが望むものを何でも実現するための、ハックに壊れやすい方法を見つける代わりに。
Syed Aqeel Ashiq 2017年

4

「プライベートメソッドをテストしない」のポイントは、実際に、クラスを使用する人のようにクラスをテストすることです。

5つのメソッドを持つパブリックAPIがある場合、クラスのすべてのコンシューマーがこれらを使用できるため、それらをテストする必要があります。コンシューマーはクラスのプライベートメソッド/プロパティにアクセスしないでください。つまり、公開された機能が同じである場合、プライベートメンバーを変更できます。


内部の拡張可能な機能に依存している場合は、protectedではなくを使用してくださいprivate。まだパブリックAPI(!)であり、使用方法が異なる
ことに注意してください。protected

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


2

この投稿のネクロには申し訳ありませんが、触れられていないように思われるいくつかの点を検討する必要があります。

何よりもまず-ユニットテスト中にクラスのプライベートメンバーにアクセスする必要がある場合、通常、戦略的または戦術的なアプローチで失敗し、誤って単一の責任の原則に違反しました。それが属していない動作。構築手順の分離されたサブルーチンにすぎないメソッドにアクセスする必要性を感じることは、これの最も一般的な発生の1つです。しかし、それは上司があなたがすぐに使える仕事に現れることを期待しているようなものであり、その状態にあなたを連れて行くためにあなたがどんな朝のルーチンを経​​験したかを知る必要があります...

これが発生するもう1つの最も一般的な例は、ことわざの「神のクラス」をテストしようとしている場合です。それ自体は特別な種類の問題ですが、手順の詳細を知る必要があるのと同じ基本的な問題に悩まされていますが、それは話題から外れています。

この特定の例では、Barオブジェクトを完全に初期化する責任をFooBarクラスのコンストラクタに効果的に割り当てています。オブジェクト指向プログラミングでは、コアテナントの1つは、コンストラクターが「神聖な」ものであり、自身の内部状態を無効にし、下流のどこか(非常に深い可能性がある場所)で障害が発生する可能性がある無効なデータから保護する必要があることです。パイプライン。)

ここでは、FooBarオブジェクトがFooBarの作成時に準備ができていないバーを受け入れることを許可し、FooBarオブジェクトを独自の方法で "ハッキング"することで補正して、これを行うことに失敗しました手。

これは、オブジェクト指向プログラミングの別のテナント(Barの場合)を遵守できなかった結果です。つまり、オブジェクトの状態は完全に初期化され、作成直後にそのパブリックメンバーへの着信呼び出しを処理する準備ができている必要があります。これは、すべてのインスタンスでコンストラクターが呼び出された直後を意味するわけではありません。多くの複雑な構築シナリオを持つオブジェクトがある場合、作成デザインパターン(ファクトリ、ビルダーなど)に従って実装されたオブジェクトのオプションメンバーへのセッターを公開することをお勧めします。後者の場合、

あなたの例では、バーの「ステータス」プロパティはFooBarがそれを受け入れることができる有効な状態にないようです-したがって、FooBarはその問題を修正するために何かを行います。

私が目にしている2番目の問題は、テスト駆動開発を実践するのではなく、コードをテストしようとしているように見えることです。これは現時点で間違いなく私自身の意見です。しかし、このタイプのテストは実際にはアンチパターンです。結局のところ、必要なテストを作成してテストにプログラミングするのではなく、コードがテスト後にテストできなくなるというコアデザインの問題に気づくという罠に陥っています。どちらの方法で問題を解決しても、本当に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 ; メソッドをテストする必要がある場合は、メソッドを公開してテストできるクラスにコードを分離する必要があります。

メソッドをプライベートにする理由は、機能が必ずしもそのクラスによって公開される必要はないため、機能がそこに属さない場合は、その機能を独自のクラスに分離する必要があるためです。

私はこの記事に出くわし、プライベートメソッドのテストにどのように取り組むべきかを説明するのに非常に役立ちました。ここではいくつかのメソッドと、それらがどのようにして悪い実装であるかについてもカバーしています。

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

アーロンの答えが一番で、私のために働いています:)私はそれを投票しますが、残念ながら私はできません(評判を逃しています)。

プライベートメソッドをテストすることがそれらを使用する唯一の方法であり、反対側にクリーンなコードがあることを言わなければなりません。

例えば:

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

これらのプライベートメソッドにモックアウトする必要があるため、これらのメソッドをすべて一度にテストしないことは、理にかなっています。アクセスできないためにモックアウトすることはできません。これは、全体としてこれをテストするための単体テストに多くの設定が必要であることを意味します。

これは、上記の方法をすべての依存関係でテストする最良の方法は、エンドツーエンドテストであると述べています。ここでは統合テストが必要ですが、TDD(テスト駆動開発)を実践している場合、E2Eテストは役に立ちませんが、テストはどの方法でもできます。


0

このルートは、クラスの外で関数を作成し、その関数をプライベートメソッドに割り当てるルートです。

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

違反しているOOPルールの種類はわかりませんが、質問に答えるために、これがプライベートメソッドをテストする方法です。私はこのことの長所と短所についてアドバイスする人を歓迎します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.