如何在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 你不应该测试类的实现细节

例子

class SomeClass {

  public addNumber(a: number, b: number) {
      return a + b;
  }
}

如果之后实现发生变化,但公共API的行为保持不变,则该方法的测试不需要更改。

class SomeClass {

  public addNumber(a: number, b: number) {
      return this.add(a, b);
  }

  private add(a: number, b: number) {
       return a + b;
  }
}

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

您正在尝试测试实现,而不是API(公共接口)。 您应该将所讨论的逻辑移到它自己的类中,以使测试更容易。

其他回答

Aaron的答案是最好的,对我来说也很有用:) 我会给它投票,但遗憾的是我不能(失去声誉)。

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

例如:

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

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

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

你可以调用私有方法!

如果您遇到以下错误:

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

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

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

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?

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

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

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