ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

附言

大多数类似问题的答案都没有给出问题的解决方案,这就是我问这个问题的原因大多数开发人员说你不要测试私有函数,但我没有说他们是错的还是对的,但是我的案子有必要进行私人测试。

我喜欢一半的答案实际上应该是评论。 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 测试包具有反射工具的原因。即使公共方法正在访问这些私有成员,也很有可能无法覆盖边缘情况,除非您可以在单元测试中直接调用私有方法。

A
Aaron Beall

我支持你,尽管“只对公共 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 编译器。为此:

您可以断言任何并避免编译器警告您访问限制: (thing as any)._name = "Unit Test"; (任何东西)._count = 123; (任何事情).init("单元测试", 123);这种方法的问题是编译器根本不知道你在做什么,所以你不会得到想要的类型错误:(thing as any)._name = 123; // 错误,但没有错误(任何事情)._count = "Unit Test"; // 错误,但没有错误 (thing as any).init(0, "123"); // 错误,但没有错误 这显然会使重构更加困难。您可以使用数组访问 ([]) 来获取私有成员:thing["_name"] = "Unit Test";事物["_count"] = 123; thing["init"]("单元测试", 123);虽然看起来很时髦,但 TSC 实际上会验证类型,就像您直接访问它们一样:thing["_name"] = 123; // 类型错误 thing["_count"] = "Unit Test"; // 类型错误 thing["init"](0, "123"); // 参数错误 老实说,我不知道为什么会这样。这显然是一个故意的“逃生舱”,让您可以在不失去类型安全的情况下访问私人成员。这正是我认为你想要的单元测试。

这是一个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 有代码味道,正如文档所说:

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


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

你可以调用私有方法!

如果您遇到以下错误:

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

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

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


这应该在顶部!
这当然是另一种选择。它与 as any 存在相同的问题,即您丢失了任何类型检查,实际上您丢失了整行的任何类型检查。
对我来说,接受的答案更好,因为您必须编写的唯一更改是从 component.callPrivateMetod() 移动到 component['callPrivateMethod]()
我不喜欢仅仅为了测试而在服务中添加任何东西的想法。这正是我想要的!谁在乎测试中单行的TS! Typescript 比任何其他原因更受 Typescript 的影响。
t
tymspy

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

例如。

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


我认为当您尝试调用私有/受保护的方法时,打字稿会出现 linting 错误。
@Gudgip 它会给出类型错误并且不会编译。 :)
我需要有关参数化 interface Part { partNumber: string; } 测试的帮助,看起来像这个 unpickPart(index: number, part: Part): void { //do stuff } 需要以某种方式调用 unpick 部分。
b
brainmonger

这对我有用:

代替:

sut.myPrivateMethod();

这个:

sut['myPrivateMethod']();

是的,确实这是更简单的方法,您不需要添加任何代码行!
在这种情况下如何将参数传递给方法?
M
Martin

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

你应该测试你的类的公共 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 想要的任何东西。
如果为了测试而将私有方法公开是不可接受的,为什么可以将方法移到自己的类中!?这两种情况都涉及驱动实现结构的测试需求,除了后者更具侵入性:你有一个完整的类,其存在的唯一原因是让你测试一些东西。
D
Deepu Reghunath

使用方括号调用私有方法

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();可以访问吗?
这是一个非常好的解决方案,能够仍然保持 inteli 感知和测试功能
谢谢,这个例子很简短,简洁明了:)它帮助我测试了一个私有方法
C
CTS_AE

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

关闭注意事项

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


L
Leon Adler

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

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

R
Ryan Hansen

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

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

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

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

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

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

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

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

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


P
Pavlo

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


D
Devpool

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


S
Sani Yusuf

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

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

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