web-dev-qa-db-ja.com

Jasmineによるプライベートメソッド用のAngular 2 / TypeScriptのためのユニットテストを書く方法

どうやってプライベート関数をangle 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/

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

P.S

  1. このような同じタイプの質問に対する答えの大部分は問題の解決策を提供していません。そのため、私はこの質問をします。

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

119
tymspy

「公開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プレイグラウンドでの実用的な例 です。

218
Aaron Beall

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

例えば。

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に感謝します。

16
tymspy

プライベートメソッドのテストを書かないでください。これは単体テストのポイントを破ります。

  • あなたはあなたのクラスのパブリックAPIをテストしているべきです
  • あなたはあなたのクラスの含意の詳細をテストしてはいけません

class SomeClass {

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

実装が後で変更されてもパブリックAPIのbehaviourが同じままである場合、このメソッドのテストを変更する必要はありません。

class SomeClass {

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

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

テストするためだけにメソッドやプロパティを公開しないでください。これは通常、次のいずれかを意味します。

  1. あなたはAPI(public interface)ではなく実装をテストしようとしています。
  2. テストを容易にするために、問題のロジックを独自のクラスに移動する必要があります。
7
Martin

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

あなたが5つのメソッドを持つパブリックAPIを持っているなら、あなたのクラスのどのコンシューマもこれらを使うことができるので、あなたはそれらをテストするべきです。コンシューマはあなたのクラスのプライベートメソッド/プロパティにアクセスしてはいけません。つまり、公開されているパブリック機能が同じであればプライベートメンバを変更できるということです。


拡張可能な内部機能に依存している場合は、protectedではなくprivateを使用してください。
protectedはまだpublic 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]);
});
4
Leon Adler

この投稿のネクロには申し訳ありませんが、私は触れられていないようないくつかの事柄を検討することを余儀なくされています。

まず第一に - 私たちがユニットテスト中にクラスのプライベートメンバーにアクセスする必要があることに気付いたとき、それは私たちの戦略的または戦術的アプローチにふざけ、誤ってプッシュして単一責任主体に違反したことです。属していない場合の動作実際には構築手続きの独立したサブルーチンにすぎないメソッドにアクセスする必要性を感じることは、これの最も一般的な発生の1つです。しかし、それはあなたの上司がすぐに仕事に出ることを期待していて、その状態にあなたを連れて行くためにあなたが何の朝のルーチンを通過したかを知る必要があります。

このことが起こる他の最も一般的な例は、あなたが自分自身がことわざのある「神の階級」を試すことを試みているのを見つけた時です。それはそれ自体特別な種類の問題ですが、手順の詳細を知る必要があるという同じ基本的な問題を抱えています - しかしそれは話題から外れています。

この特定の例では、Barオブジェクトを完全に初期化する責任をFooBarクラスのコンストラクタに効果的に割り当てました。オブジェクト指向プログラミングでは、コンストラクタは「神聖」であり、それ自体の内部状態を無効にし、下流のどこかで失敗する可能性がある無効なデータから守られるべきです(非常に深い可能性があります)。パイプライン。)

FooBarが構築された時点では準備ができていないBarをFooBarオブジェクトが受け入れることを許可することによってこれを行うことはできませんでした。そして、FooBarオブジェクトをある種の「ハッキング」して問題を解決しました。手。

これは、オブジェクト指向プログラミングの別の目的(Barの場合)に従わなかった結果です。つまり、オブジェクトの状態は完全に初期化され、作成直後にそのパブリックメンバーへの着信呼び出しを処理する準備ができているはずです。現在、これは、コンストラクタがすべてのインスタンスで呼び出された直後を意味するのではありません。複雑な構築シナリオが多数あるオブジェクトがある場合は、作成者の設計パターン(Factory、Builderなど)に従って実装されているオブジェクトに、そのオプションメンバーのセッターを公開することをお勧めします。後者の場合は、目的のオブジェクトの初期化を別のオブジェクトグラフにプッシュすることになります。その目的の1つは、要求しているインスタンスの有効なインスタンスを取得できるようにトラフィックを誘導することです。この作成オブジェクトがそれを提供した後まで "準備ができている"と見なされる。

あなたの例では、Barの "status"プロパティはFooBarがそれを受け入れることができる有効な状態にはないようです - そのためFooBarはその問題を修正するために何かをします。

私が目にしている2番目の問題は、テスト駆動開発を実践するのではなく、コードをテストしようとしているように見えることです。現時点でこれは私自身の意見です。しかし、この種のテストは本当にアンチパターンです。あなたがやろうとしているのは、必要なテストを書いてその後そのテストにプログラミングするのではなく、事後にコードをテストすることができないというコア設計上の問題があることに気付くことです。どちらの方法で問題が発生したとしても、SOLIDの実装を本当に達成したならば、まだ同じ数のテストとコード行になるはずです。それで - あなたが開発努力の開始時に問題に対処することができるのになぜなぜテスト可能なコードにあなたの方法をリバースエンジニアリングするのですか?

あなたがそうしていたなら、あなたはあなたのデザインに対してテストするためにいくらか風変わりなコードを書かなければならないであろうということにずっと早く気づいていたでしょう。簡単にテスト可能です。

3
Ryan Hansen

私は@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

エラーが発生したときにプライベートメソッドやを呼び出すことができます。

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

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

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

今、私はどのタイプのOOPルールを破っているのかわかりませんが、質問に答えるために、これがプライベートメソッドのテスト方法です。私はこれの賛否両論について助言することをだれでも歓迎します。

0
Sani Yusuf

Aaron による答えが私のために働いています:)私はそれを投票するでしょうが、残念ながら私はできません(評判を失う)。

プライベートメソッドをテストすることがそれらを使用し、反対側にきれいなコードを持っている唯一の方法であると私は言います。

例えば:

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
Devpool

オプションとして、privateの代わりにprotectedを使用し、yout testで "test"の子を宣言できます。

class FooBar {

    private _status: number;

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

    }

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

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

}

テスト中:

export class FooBarTest extends FooBar {
  ...
  initFooBar() {
    return super.initFooBar();
  }
  ...
}

...

it('initFooBar test', () => {
    const fooBarTest = new FooBarTest();
    fooBarTest.initFooBar();
    expect(fooBarTest.status).toEqual('data matata');
  });
...