如何使用Jasmine为私有方法编写Angular / TypeScript的单元测试


195

如何在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/

如果您做过任何事情,请给我建议解决这个问题的更好方法?

聚苯乙烯

  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”,则在某些情况下范围测试会变得非常复杂。
四元组

18
这并不是说您没有测试公共接口,显然您会这样做,但是测试私有方法使您能够测试一系列短的可管理块(与您最初编写它们的原因相同,为什么要撤消操作)涉及到测试),并且仅仅因为对公共接口的测试是有效的(也许调用函数限制了输入范围)并不意味着在您添加更多高级逻辑并从其他方法调用它们时,私有方法就没有缺陷。新的父函数,
四元组

5
如果您使用TDD对它们进行了正确的测试,则您将不会试图弄清楚您以后应该做什么,而您应该正确地对其进行测试。
四元论

Answers:


342

我与您同在,尽管“仅对公共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 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
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
我认为当您尝试调用私有/受保护的方法时,打字稿会产生掉毛错误。
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
也许在评论之前先阅读该帖子。我明确指出并证明,测试私有人员是测试实施的一种嗅觉,而不是行为,这会导致脆弱的测试。
马丁

1
想象一个对象,它为您提供一个介于0和私有属性x之间的随机数。如果您想知道x是否由构造函数正确设置,则测试x的值要比进行一百次测试来检查获得的数字是否在正确的范围内容易得多。
戈多

1
@ user3725805这是测试实现而非行为的示例。最好隔离私有数字的来源:常量,配置,构造函数,然后从那里进行测试。如果私有不是来自其他来源,则它属于“魔数”反模式。
马丁

1
为什么不允许测试实现?单元测试可以很好地检测出意外的变化。当构造器由于某种原因忘记设置数字时,测试立即失败并警告我。当有人更改实现时,测试也会失败,但是我更喜欢采用一种测试,而不是检测不到错误。
戈多

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

对这篇文章的坏处感到抱歉,但我不得不考虑一些似乎尚未涉及的事情。

首先,最重要的是-当我们发现自己需要在单元测试中与班上的私人成员接触时,通常这是一个巨大的,危险的信号,我们在战略或战术方法中就无济于事,并且无意间违反了单一责任主体不属于的行为。感觉需要访问的方法实际上只不过是构造过程的一个隔离子例程,这是这种情况最常见的一种。但是,这有点像您的老板希望您准备去上班,并且还有些反常的需要知道您经历了什么早上的例行程序才能进入该状态...

发生这种情况的另一种最常见的情况是,您发现自己试图测试众所周知的“神类”。它本身就是一种特殊的问题,但是由于需要了解过程的详细信息而遭受相同的基本问题-但这已成为话题。

在这个特定的示例中,我们已经有效地将完全初始化Bar对象的职责分配给了FooBar类的构造函数。在面向对象的编程中,核心原则之一是,构造函数是“神圣的”,并且应防止使用无效数据,这些无效数据会使自己的内部状态无效并使其准备在下游的其他地方失败(这可能是一个非常深的问题)。管道。)

我们在这里未能通过允许FooBar对象接受在构造FooBar时尚未准备好的Bar来做到这一点,并且已经通过“黑客攻击” FooBar对象将事情纳入自己的行为中进行了补偿手。

这是由于未能遵守面向对象编程的另一种规定(对于Bar而言)的结果,即对象的状态应完全初始化,并准备在创建后立即处理对其公共成员的任何传入调用。现在,这并不意味着在所有实例中都立即调用了构造函数。当您的对象具有许多复杂的构造方案时,最好将setters的可选成员公开给根据创建设计模式(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);

请参阅以下代码: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规则,但是要回答这个问题,这就是我如何测试私有方法。我欢迎任何人就此提出建议。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.