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

时间:2016-03-14 12:03:22

标签: angular unit-testing typescript jasmine

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

  2. 如果您有任何问题,请建议我更好的方法来解决这个问题吗?

    P.S

    1. 像这样的类似问题的大部分答案都不能解决问题,这就是我问这个问题的原因

    2. 大多数开发人员都说你不测试私人功能,但我不是说他们错了或是对的,但我的案例有必要进行私人测试。

12 个答案:

答案 0 :(得分:213)

我和你在一起,尽管“仅对单一测试公共API进行测试”是一个很好的目标,但有时它看起来并不那么简单,你觉得你在选择破坏API或单位时做出选择 - 试验。你已经知道了,因为这正是你要求做的,所以我不会深入研究它。 :)

在TypeScript中,我发现了一些可以访问私有成员的方法,以便进行单元测试。考虑这个课程:

radecs2 = radecs[radecs.dec > - 10 * u.degree]

即使TS使用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; } } privateprotected限制对类成员的访问,编译的JS也没有私有成员,因为这不是JS中的事情。它纯粹用于TS编译器。为此:

  1. 您可以断言public并逃避编译器警告您访问限制:

    any

    这种方法的问题是编译器根本不知道你正在做什么(thing as any)._name = "Unit Test"; (thing as any)._count = 123; (thing as any).init("Unit Test", 123); ,所以你没有得到所需的类型错误:

    any
  2. 您可以使用数组访问权限((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 )来获取私人成员:

    []

    虽然它看起来很时髦,但TSC实际上会验证这些类型,就像你直接访问它们一样:

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

    说实话,我不知道为什么会这样。这显然是一个intentional "escape hatch",可以让你在不丢失类型安全的情况下访问私人会员。这正是我认为你想要进行单元测试的。

  3. 这是working example in the TypeScript Playground

答案 1 :(得分:16)

由于大多数开发人员不建议测试私有功能,为什么不测试呢?

例如

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。

答案 2 :(得分:7)

不要为私有方法编写测试。这违背了单元测试的要点。

  • 您应该测试您班级的公共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(公共接口)。
  2. 您应该将有问题的逻辑移到自己的类中,以便更轻松地进行测试。

答案 3 :(得分: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]);
});

答案 4 :(得分:3)

对于这篇文章中的死讯感到抱歉,但我觉得有必要权衡一些似乎没有涉及的事情。

首先 - 当我们发现自己需要在单元测试期间接触私人成员时,通常是一个巨大的,肥胖的红旗,我们在战略或战术方法中愚弄,无意中违反了单一责任通过推动不属于的行为的主体。感觉需要访问实际上只是构造过程的孤立子程序的方法,这是最常见的事情之一;然而,这有点像你的老板期待你出现准备出发的工作,并且有一些不正常的需要知道你经历了什么样的早晨例行程序让你进入那个状态......

发生这种情况的另一个最常见的例子是当你发现自己试图测试众所周知的“上帝阶级”时。这本身就是一种特殊的问题,但是由于需要了解一个程序的私密细节而遇到同样的基本问题 - 但是这个问题正在逐渐脱离主题。

在这个具体的例子中,我们已经有效地将Bar对象的初始化责任分配给了FooBar类的构造函数。在面向对象的编程中,核心内容之一是构造函数是“神圣的”,应该防范无效数据,这些数据会使其内部状态无效,并使其在下游其他地方(在可能非常深的地方)失败管道。)

我们在这里没有做到这一点,因为允许FooBar对象接受在构造FooBar时尚未就绪的Bar,并通过对“FooBar”对象的“黑客攻击”进行补偿。它自己动手。

这是因为未能遵守面向对象编程的另一个目标(在Bar的情况下),即对象的状态应该完全初始化并准备好立即处理对其“公共成员”的任何传入呼叫创作之后。现在,这并不意味着在所有实例中调用构造函数之后立即执行。当你有一个具有许多复杂构造场景的对象时,最好将setters暴露给它的可选成员,这个对象是根据创建设计模式(Factory,Builder等等)实现的。在后一种情况下,你会将目标对象的初始化推送到另一个对象图中,该对象图的唯一目的是引导流量到达你所拥有的有效实例的那一点 - 并且产品不应该是被认为是“准备好”,直到这个创建对象提供它为止。

在您的示例中,Bar的“status”属性似乎不处于FooBar可以接受它的有效状态 - 因此FooBar会对其进行某些操作来纠正该问题。

我看到的第二个问题是,您似乎正在尝试测试代码而不是练习测试驱动的开发。在这个时间点,这绝对是我自己的看法;但是,这种类型的测试确实是一种反模式。你最终要做的就是陷入陷阱,意识到你有核心设计问题阻止你的代码在事后被测试,而不是编写你需要的测试并随后编写测试。无论哪种方式,您都会遇到问题,如果您真正实现了SOLID实现,您仍应该使用相同数量的测试和代码行。那么 - 当您在开发工作开始时能够解决问题时,为什么还要尝试对可测试代码进行逆向工程?

如果你这样做了,那么你早就意识到你将不得不写一些相当狡猾的代码来测试你的设计,并且很早就有机会通过改变行为重新调整你的方法到易于测试的实现。

答案 5 :(得分:1)

我同意@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

答案 6 :(得分:1)

许多人已经说过,只要您想测试私有方法,就不应该修改您的代码或编译器使其适合您。如今,TypeScript会拒绝人们迄今提供的大多数hacks。


解决方案

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(){
        // ...
    }
}

答案 7 :(得分:0)

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测试不会为您提供帮助,但是测试任何方法都可以。

答案 8 :(得分:0)

我采用的这一途径是我在类之外创建函数并将该函数分配给我的私有方法的途径。

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

现在我不知道我要打破哪种类型的OOP规则,但是要回答这个问题,这就是我如何测试私有方法。我欢迎任何人就此提出建议。

答案 9 :(得分:0)

您可以调用私有方法,并在出现错误时调用

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

答案 10 :(得分:0)

使用方括号调用私有方法

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

答案 11 :(得分:0)

这对我有用:

代替:

sut.myPrivateMethod();

此:

sut['myPrivateMethod']();