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

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

P.S

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


当前回答

我同意你的观点,尽管“只对公共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使用private、protected、public来限制对类成员的访问,但编译后的JS没有private成员,因为这在JS中是不存在的。它纯粹用于TS编译器。因此:

You can assert to any and escape the compiler from warning you about access restrictions: (thing as any)._name = "Unit Test"; (thing as any)._count = 123; (thing as any).init("Unit Test", 123); The problem with this approach is that the compiler simply has no idea what you are doing right of the any, so you don't get desired type errors: (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 This will obviously make refactoring more difficult. You can use array access ([]) to get at the private members: thing["_name"] = "Unit Test"; thing["_count"] = 123; thing["init"]("Unit Test", 123); While it looks funky, TSC will actually validate the types as if you accessed them directly: thing["_name"] = 123; // type error thing["_count"] = "Unit Test"; // type error thing["init"](0, "123"); // argument error To be honest I don't know why this works. This is apparently an intentional "escape hatch" to give you access to private members without losing type safety. This is exactly what I think you want for your unit-testing.

下面是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是一种代码气味,正如文档所说:

我们建议您尽量少使用这些注释。(强调原创)

其他回答

正如许多人已经指出的那样,尽管您想要测试私有方法,但您不应该通过修改代码或编译器来让它为您工作。现代的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() {
        // ...
    }
}

结束笔记

You can implicitly test your private methods by making sure that conditions are met such that the code is called through the public interface. If the public interface does not call out to the private methods then that code is not providing any function and should be removed. In the example above there should be some effect that calling the private method should return ie: an object with an address. If there's not, for example the code emits an event in a private method, then you should start looking to decouple that so that it can be tested -- even in that example you would likely listen/subscribe to that event and would be able to test it that way. Decoupling leads to better testability and easier code maintenance later down the road as well.

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

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

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

First a foremost - when we find ourselves needing access to private members on a class during unit testing, it is generally a big, fat red flag that we've goofed in our strategic or tactical approach and have inadvertently violated the single responsibility principal by pushing behavior where it does not belong. Feeling the need to access methods that are really nothing more than an isolated subroutine of a construction procedure is one of the most common occurrences of this; however, it's kind of like your boss expecting you to show up for work ready-to-go and also having some perverse need to know what morning routine you went through to get you into that state...

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

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

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

This is the result of a failure to adhere to another tenent of object oriented programming (in the case of Bar,) which is that an object's state should be fully initialized and ready to handle any incoming calls to its' public members immediately after creation. Now, this does not mean immediately after the constructor is called in all instances. When you have an object that has many complex construction scenarios, then it is better to expose setters to its optional members to an object that is implemented in accordance with a creation design-pattern (Factory, Builder, etc...) In any of the latter cases, you would be pushing the initialization of the target object off into another object graph whose sole purpose is directing traffic to get you to a point where you have a valid instance of that which you are requesting - and the product should not be considered "ready" until after this creation object has served it up.

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

The second issue I am seeing is that it appears that you are trying to test your code rather than practice test-driven development. This is definitely my own opinion at this point in time; but, this type of testing is really an anti-pattern. What you end up doing is falling into the trap of realizing that you have core design problems that prevent your code from being testable after the fact, rather than writing the tests you need and subsequently programming to the tests. Either way you come at the problem, you should still end up with the same number of tests and lines of code had you truly achieved a SOLID implementation. So - why try and reverse engineer your way into testable code when you can just address the matter at the onset of your development efforts?

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

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

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

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

你可以调用私有方法!

如果您遇到以下错误:

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