繁体   English   中英

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

[英]How to write unit testing for Angular / TypeScript for private methods with Jasmine

您如何在 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. 大多数开发人员都说你不要测试私有函数,但我没有说它们是对还是错,但是我的案例有必要测试私有。

我支持你,尽管“只对公共 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是一种代码气味,正如文档所说:

我们建议您非常谨慎地使用此评论。 【重点原创】

你可以调用私有方法!

如果您遇到以下错误:

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

感谢@Moff452评论 你也可以写:

expect(new FooBar(/*...*/)['initFooBar']()).toEqual(/*...*/)

更新:

@ts-expect-error@ts-ignore的更好选择。 请参阅: https ://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#ts-ignore-or-ts-expect-error

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

例如。

你的类.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。

这对我有用:

代替:

sut.myPrivateMethod();

这个:

sut['myPrivateMethod']();

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

  • 你应该测试你的类的公共 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. 您应该将有问题的逻辑移到它自己的类中,以使测试更容易。

正如许多人已经说过的那样,尽管您想测试私有方法,但您不应该破解您的代码或转译器以使其为您工作。 现代 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.format();
        //...
    }
}

class Address {
    private format: StreetFormatter;

    public format() {
        //...
        format.toString();
        //...
    }
}

class StreetFormatter {
    public toString() {
        // ...
    }
}

关闭注意事项

您可以通过确保满足条件以便通过公共接口调用代码来隐式测试您的私有方法。 如果公共接口没有调用私有方法,那么该代码没有提供任何功能,应该被删除。 在上面的示例中,调用私有方法应该返回一些效果,即:具有地址的对象。 如果没有,例如代码在私有方法中发出一个事件,那么您应该开始寻求解耦它以便可以对其进行测试——即使在那个示例中,您可能会监听/订阅该事件并且能够那样测试它。 解耦会带来更好的可测试性和更容易的代码维护。

使用方括号调用私有方法

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

“不要测试私有方法”的重点实际上是像使用它的人一样测试类

如果您有一个包含 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]);
});

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

很抱歉这篇文章中的死灵,但我觉得有必要权衡一些似乎没有被触及的事情。

首先,当我们发现自己在单元测试期间需要访问某个类中的私有成员时,这通常是一个巨大的危险信号,我们在战略或战术方法中犯了错误,并且通过推动无意中违反了单一责任原则不属于它的行为。 感觉需要访问实际上只不过是构造过程的孤立子例程的方法是这种情况最常见的情况之一; 然而,这有点像你的老板希望你现身工作准备去上班,并且有一些反常的需要知道你经历了什么样的早晨例程才能让你进入那种状态......

发生这种情况的另一个最常见的例子是当你发现自己试图测试众所周知的“神级”时。 它本身就是一种特殊的问题,但也遇到了同样的基本问题,即需要了解程序的私密细节——但这已经跑题了。

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

我们在这里没有通过允许 FooBar 对象接受在构建 FooBar 时尚未准备好的 Bar 来做到这一点,并通过某种“黑客”来补偿 FooBar 对象以将事情纳入自己的手。

这是未能遵守面向对象编程的另一个原则(在 Bar 的情况下)的结果,即对象的状态应该完全初始化并准备好在创建后立即处理对其公共成员的任何传入调用。 现在,这并不意味着在所有实例中调用构造函数之后立即。 当您有一个具有许多复杂构造场景的对象时,最好将其可选成员的设置器公开给根据创建设计模式(工厂、生成器等)实现的对象。在后一种情况下,您会将目标对象的初始化推到另一个对象图中,该对象图的唯一目的是引导流量以使您到达具有您所请求的有效实例的点 - 并且产品不应该是被认为是“准备好”,直到这个创建对象提供了它。

在您的示例中,Bar 的“状态”属性似乎没有处于 FooBar 可以接受它的有效状态 - 因此 FooBar 对其进行了一些处理以纠正该问题。

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

如果你这样做了,那么你会更早地意识到你将不得不编写一些相当棘手的代码来测试你的设计,并且很早就有机会通过将行为转移到实现来重新调整你的方法很容易测试。

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 测试对您没有帮助,但是测试任何方法都会。

我采取的这条路线是我在类之外创建函数并将函数分配给我的私有方法。

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

现在我不知道我违反了哪种类型的 OOP 规则,但要回答这个问题,这就是我测试私有方法的方式。 我欢迎任何人就这方面的利弊提出建议。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM