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

Posted

技术标签:

【中文标题】如何使用 Jasmine 为私有方法编写 Angular / TypeScript 单元测试【英文标题】:How to write unit testing for Angular / TypeScript for private methods with Jasmine 【发布时间】:2016-06-29 11:06:28 【问题描述】:

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


我找到的解决方案

    将测试代码本身放在闭包内或在闭包内添加代码,该闭包存储对外部范围内现有对象的局部变量的引用。

    稍后使用工具剥离测试代码。 http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

如果你有做过的话,请给我一个更好的方法来解决这个问题?

附言

    大多数类似问题的答案都没有解决问题,这就是我问这个问题的原因

    大部分开发者都说你不测试私有函数,但我没有说它们是对是错,但我的情况有必要测试私有函数。

【问题讨论】:

我喜欢一半的答案实际上应该是 cmets。 OP问问题,你怎么X?接受的答案实际上告诉你如何做 X。然后大多数人转身说,我不仅不会告诉你 X(这显然是可能的),而且你应该做 Y。大多数单元测试工具(我不是这里只讨论 JavaScript)能够测试私有函数/方法。我将继续解释原因,因为它似乎在 JS 领域迷失了(显然,给出了一半的答案)。 将问题分解为可管理的任务是一种很好的编程习惯,因此函数“foo(x:type)”将调用私有函数 a(x:type), b(x:type), c (y:another_type) 和 d(z:yet_another_type)。现在因为 foo, 正在管理调用并将东西放在一起,它会产生一种湍流,就像溪流中岩石的背面,很难确保所有范围都经过测试的阴影。因此,更容易确保每个子范围的有效,如果您尝试单独测试父“foo”,则范围测试在某些情况下会变得非常复杂。 这并不是说你不测试公共接口,显然你这样做了,但是测试私有方法允许你测试一系列短的可管理块(与你编写它们的原因相同首先,为什么在测试时要撤消此操作),并且仅仅因为公共接口上的测试是有效的(可能调用函数限制了输入范围)并不意味着私有方法在添加时没有缺陷更高级的逻辑并从其他新的父函数中调用它们, 如果你用 TDD 正确地测试了它们,你就不会试图弄清楚你后来在做什么,当你应该正确地测试它们时。 @Quaternion 这条关于 TDD 的评论围绕着 OP 的问题,实际上并没有为测试私有方法的总体思路提供任何有价值的见解。有时您确实需要访问私有成员才能获得良好的测试覆盖率,这就是 Spring Boot 测试包具有反射工具的原因。即使公共方法正在访问这些私有成员,也很有可能无法覆盖边缘情况,除非您可以在单元测试中直接调用私有方法。 【参考方案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(/*...*/)

感谢@Moff452 his/her comment。你也可以写:

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

【讨论】:

这应该在顶部! 这当然是另一种选择。它与as any 存在相同的问题,因为您丢失了任何类型检查,实际上您丢失了整行的任何类型检查。 对我来说,接受的答案更好,因为您必须编写的唯一更改是从 component.callPrivateMetod() 移动到 component['callPrivateMethod]()【参考方案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 编译器。为此:

    您可以断言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
    

    这显然会使重构变得更加困难。

    您可以使用数组访问 ([]) 来获取私有成员:

    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
    

    说实话,我不知道为什么会这样。 这显然是一个intentional "escape hatch",让您可以访问私有成员而不会失去类型安全性。这正是我认为你想要的单元测试。

这是working example in the TypeScript Playground。

为 TypeScript 2.6 编辑

有些人喜欢的另一个选项是使用// @ts-ignore (added in TS 2.6),它会简单地抑制以下行中的所有错误:

// @ts-ignore
thing._name = "Unit Test";

问题在于,它抑制了以下行中的所有错误:

// @ts-ignore
thing._name(123).this.should.NOT.beAllowed("but it is") = window / ;

我个人认为@ts-ignore 是一种代码气味,正如文档所说:

我们建议您非常谨慎地使用此 cmets。 [强调原创]

【讨论】:

很高兴听到关于单元测试的现实立场以及实际解决方案,而不是标准的单元测试员教条。 对行为的一些“官方”解释(甚至引用单元测试作为用例):github.com/microsoft/TypeScript/issues/19335 只需使用` // @ts-ignore`,如下所述。告诉 linter 忽略私有访问器 @Tommaso 是的,这是另一种选择,但与使用 as any 有同样的缺点:你会丢失所有类型检查。 我在一段时间内看到的最佳答案,感谢@AaronBeall。另外,感谢 tymspy 提出最初的问题。【参考方案3】:

这对我有用:

代替:

sut.myPrivateMethod();

这个:

sut['myPrivateMethod']();

【讨论】:

是的,这确实是更简单的方法,你不需要添加任何代码行!【参考方案4】:

使用方括号调用私有方法

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

【讨论】:

1.我在 nodejs 中使用方括号时尝试了您的解决方案,需要使用 import 或 require。 2. 我的计算类看起来像 export let cal = new Calculate();可以访问吗? 这是一个非常好的解决方案,能够仍然保持智能感知和测试功能【参考方案5】:

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

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

export class MyClass 
  private _myPrivateFunction = someFunctionThatCanBeTested;


function someFunctionThatCanBeTested() 
  //This Is Testable

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

【讨论】:

【参考方案7】:

Aaron 的答案是最好的,并且对我有用 :) 我会投票,但遗憾的是我不能(失去声誉)。

我不得不说测试私有方法是使用它们并在另一端拥有干净代码的唯一方法。

例如:

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

不一次测试所有这些方法很有意义,因为我们需要模拟出那些我们无法模拟出的私有方法,因为我们无法访问它们。这意味着我们需要对单元测试进行大量配置才能对其进行整体测试。

这就是说用所有依赖项测试上述方法的最佳方法是端到端测试,因为这里需要进行集成测试,但是如果您正在练习 TDD(测试驱动开发),则端到端测试对您没有帮助,但测试任何方法都会。

【讨论】:

【参考方案8】:

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

【讨论】:

【参考方案9】:

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

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

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

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

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

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

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

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

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

【讨论】:

【参考方案10】:

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

例如。

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。

【讨论】:

我认为 typescript 在您尝试调用私有/受保护方法时会出现 linting 错误。 @Gudgip 它会给出类型错误并且不会编译。 :) 我需要关于参数化 interface Part partNumber: string; 测试的帮助,看起来像这样 unpickPart(index: number, part: Part): void //do stuff 需要调用 unpick 部分。【参考方案11】:

“不要测试私有方法”的真正意义在于像使用它的人一样测试类

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

【讨论】:

【参考方案12】:

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

你应该测试你的类的公共 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;
  

不要为了测试而公开方法和属性。这通常意味着:

    您正在尝试测试实现而不是 API(公共接口)。 您应该将有问题的逻辑移到其自己的类中,以简化测试。

【讨论】:

也许在评论之前阅读这篇文章。我清楚地说明并证明测试私有是测试实现的味道而不是行为,这会导致脆弱的测试。 @user3725805 这是测试实现而不是行为的示例。最好隔离私有数字的来源:常量、配置、构造函数——并从那里进行测试。如果私有不是来自其他来源,那么它属于“幻数”反模式。 为什么不允许测试实现?单元测试可以很好地检测出意外的变化。当由于某种原因构造函数忘记设置数字时,测试会立即失败并警告我。当有人更改实现时,测试也会失败,但我更喜欢采用一种测试而不是出现未检测到的错误。 +1。很好的答案。 @TimJames 告诉正确的做法或指出有缺陷的方法是 SO 的目的。而不是找到一种脆弱的方式来实现 OP 想要的任何东西。 如果将私有方法公开以使其可以测试是不可接受的,为什么可以将方法移动到自己的类中!?这两种情况都涉及驱动实现结构的测试需求,但后者更具侵入性:你有一个完整的类,其存在的唯一原因是让你测试一些东西。

以上是关于如何使用 Jasmine 为私有方法编写 Angular / TypeScript 单元测试的主要内容,如果未能解决你的问题,请参考以下文章

在私有方法上使用 Jasmine spyon

如何使用 Angular 和 Jasmine 模拟 socket.io

如何在 (jasmine + karma) 中为以下方法编写测试,该方法在构造函数中注入了 ComponentFactoryResolver 和 ApplicationRef

如何在 Angular 7 中使用 Karma/Jasmine 为 App_Initializer 编写单元测试用例

使用 Angular 和 Jasmine/Karma 的私有方法进行测试和代码覆盖

如何使用 Jasmine 为以下 javascript 函数编写单元测试用例