如何用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;
}


}

我找到了解决办法

  1. 将测试代码本身放在闭包中或在闭包中添加代码,以存储外部作用域中现有对象上局部变量的引用。

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

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

附:

  1. 大多数类似类型的问题的答案都没有给出问题的解决方案,这就是我问这个问题的原因

  2. 大多数开发人员说不要测试私有函数,但我不会说它们是错的还是对的,但我的案例中有必要测试私有函数。

185414 次浏览

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

不要为私有方法编写测试。这就破坏了单元测试的意义。

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

我同意你的观点,尽管“只对公共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没有private成员,因为这在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是一种代码气味,正如文档所说:

我们建议你使用这个注释很谨慎的。(强调原创)

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

如。

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。

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

如果你有一个带有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]);
});

很抱歉在这篇文章中出现了死灵,但我觉得有必要对一些似乎没有涉及到的事情进行权衡。

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

发生这种情况的另一个最常见的实例是当您发现自己试图测试众所周知的“神类”时。这本身是一种特殊的问题,但同样的基本问题是需要知道手术的详细信息——但这就跑题了。

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

在这里,我们没有做到这一点,因为我们允许FooBar对象接受一个在构造FooBar时还没有准备好的Bar,并通过某种“hack”FooBar对象来补偿它,让它自己来处理问题。

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

在你的例子中,Bar的“status”属性似乎不在一个FooBar可以接受的有效状态中——所以FooBar对它做了一些事情来纠正这个问题。

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

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

正如许多人已经指出的那样,尽管您想要测试私有方法,但您不应该通过修改代码或编译器来让它为您工作。现代的TypeScript会拒绝人们迄今为止提供的大部分hack。


解决方案

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

结束笔记

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

亚伦的答案是最好的,对我来说是有效的:) 我想给它投票,但遗憾的是我不能(失去声誉)

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

例如:

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

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

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

我采用的这种方法是在类外部创建函数,并将函数分配给我的私有方法。

export class MyClass {
private _myPrivateFunction = someFunctionThatCanBeTested;
}


function someFunctionThatCanBeTested() {
//This Is Testable
}

现在我不知道我破坏了哪种类型的OOP规则,但是为了回答这个问题,这是我测试私有方法的方法。我欢迎任何人就Pros &缺点。

你可以调用私有方法!

如果您遇到以下错误:

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 < / p >

使用方括号调用私有方法。

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

这招对我很管用:

而不是:

sut.myPrivateMethod();

这样的:

sut['myPrivateMethod']();