如何在Typescript中单元测试私有方法

72
当我尝试对类中的私有方法进行单元测试时,出现了“私有方法只能在类内部访问”的错误。这里我添加了一个示例片段来展示我的类和 mocha 测试。请提供解决方案以实现对私有方法的单元测试。
类名:Notification.ts
class Notification {
 constructor() {}
 public validateTempalte() {
  return true;
 }

 private replacePlaceholder() {
  return true;
 }
}

单元测试:

import {Notification} from 'Notification';
import * as chai from "chai";

describe("Notification", function(){

  describe('#validateTempalte - Validate template', function() {
      it('it should return success', function() {
        const result = new Notification()
        chai.expect(result.validateTempalte()).to.be.equal(true);
      });
    });
  describe('#replacePlaceholder - Replace Placeholder', function() {
      it('it should return success', function() {
        const result = new Notification()
        // As expected getting error "Private is only accessible within class"
        chai.expect(result.replacePlaceholder()).to.be.equal(true);
      });
    });
});

作为一种解决方法,目前我正在将函数replacePlaceholder的访问说明符更改为public。但我不认为这是一个有效的方法。

作为一种临时解决方法,目前我正在将函数replacePlaceholder的访问修饰符更改为public。但我并不认为这是一个恰当的做法。

(Two possible translations depending on the context.)

3
你不应该测试你的单元除了公共接口以外的部分。 - toskv
11个回答

70

避免Typescript检测的一个可能的解决方案是动态访问属性(不说这是否好)。

myClass['privateProp'] 或对于方法:myClass['privateMethod']()


是的,如果您使用私有静态方法,应该使用类而不是变量,因此应该导出一个类。 - Ilya Kushlianski
7
这是正确答案,而不是被接受的那个答案,它说“你可以直接给他们打电话”。这个答案能够满足编译器的要求。 - Tobias Cudnik
2
测试私有方法的最佳解决方案 - Gudari
2
这是进行单元测试的明确方法:https://github.com/microsoft/TypeScript/issues/19335#issuecomment-338003928 - HolgerJeromin
这是一个非常好的答案。它保留了输入信息,而其他一些解决方案则没有。 - Automatico

57

从技术上讲,在当前版本的TypeScript中,私有方法只在编译时被检查为私有 - 所以您可以调用它们。

class Example {
    public publicMethod() {
        return 'public';
    }

    private privateMethod() {
        return 'private';
    }
}

const example = new Example();

console.log(example.publicMethod()); // 'public'
console.log(example.privateMethod()); // 'private'

我提到这个仅仅是因为你问了如何做到,而那就是你可以做到的方式。

正确答案

然而,那个私有方法必须被另一个方法调用...否则它根本不会被调用。如果您测试该其他方法的行为,则将在使用上下文中覆盖私有方法。

如果您特别测试私有方法,则您的测试将与实现细节紧密耦合(即,如果您重构实现,良好的测试无需更改)。

免责声明

如果您仍然在私有方法级别进行测试,则编译器可能在未来进行更改并使测试失败(即,如果编译器使方法“正确”地变为私有方法,或者如果ECMAScript的将来版本添加了可见性关键字等)。


34
我经常看到这种论点,但我认为它远非现实。如果你有一个复杂的功能,你希望将其拆分为较小的逻辑段以单独测试它们,从而减少开发时间、复杂性、错误发生的可能性和测试量。解决这个问题的标准方法是将所有段作为函数移动到“util”模块中并在那里导出它们(“它们就是官方的 util 方法,对吧?”),这只是混淆了潜在的困境,并削弱了可见范围的逻辑。 - NotX
5
如果您在较大的范围内更改了实现细节,那么删除与新逻辑不再适用的测试没有任何问题。它们存在是为了帮助您开发旧版本的代码。稍后,当您更改内部内容时,您需要用新的测试替换它们,以帮助您正确开发新逻辑。请记住,我在这里谈论的是复杂函数,其中单个段不是琐碎的。 - NotX
1
@JasonMcCarrell,我们对隔离的理解有所不同。Ian Cooper在2013年的“TDD Where Did It All Go Wrong”演讲中改变了我的看法:https://www.stevefenton.co.uk/2013/05/my-unit-testing-epiphany/。 - Fenton
1
@Fenton,感谢您的分享!您实际上引导了我走向一条我认为将会非常有益的道路。我以前持反对态度,但是那种编码方式让我望而却步。Cooper解释了TDD这种风格固有的抵抗力,以及他所参考的书中的教导如何兼顾快速和安全的测试。我希望这将为我(以及可能遇到这种情况的其他人)架起一座新的、更好的编码风格的桥梁! - nikojpapa
@TobiasCudnik 这确实鼓励不要测试私有函数! - Ichor de Dionysos
显示剩余3条评论

48
在我的情况下,我使用对象的原型来访问私有方法。它运行良好且TS不报错。 例如:

在我的情况下,我使用对象的原型来访问私有方法。它运行良好且TS不报错。

例如:

class Example {
    private privateMethod() {}
}

describe() {
    it('test', () => {
        const example = new Example();
        const exampleProto = Object.getPrototypeOf(example);

        exampleProto.privateMethod();
    })
}

如果你使用静态方法,则使用 exampleProto.constructor.privateMethod();


我花了将近3天的时间,甚至两三次检查这个链接,但都没有成功。谢谢Andy。最后一个静态方法帮了我。 - R.G.Krish
1
注意,这对于测试类实例的私有属性值来说并不是一个好方法。 - SarahJessica
那个关于静态方法的小提示让我节省了很多时间。谢谢! - mikey
这行代码还有效吗? - Pratik Wadekar
我尝试使用 exampleProto.constructor.privateMethod() 来创建一个静态类,但是出现了 is not a function 错误。像 @IcyIcicle 的回答中所示,将其转换为 <any> 对我有用。 - Raphael Pinel

4
在HolgerJeromin的评论中,评论问题有一个简洁的解决方案,仍然使用属性语法。
解决方案是将你的对象/类强制类型转换为 any
例如:
(<any>myClass).privateMethod();
const value = (<any>myClass).privateValue;

(myClass as any).privateMethod();
const value = (myClass as any).privateValue;

这种方法可以同时满足编译器和VSCode的语法高亮显示。

以下是我从与此相关的问题中记录的一些笔记

  • 通过字符串访问更常见,尽管我不知道为什么它可能更类型安全。
  • 这些功能是有意为之的,因此它们比阻碍更有帮助。
  • 很可能有一种方法可以禁用此类功能,以便人们不会将这段代码复制粘贴到生产环境中。在tsconfig.json"noImplicitAny": true, 可能有所帮助。

对于 TSX:(myClass as any).privateValue - Max Reeder
@MaxReeder 当然!这些解决方案适用于所有包括 .tsx 的 TypeScript 环境。 - IcyIcicle

4

我的主观解决方案:您可以定义一个新的仅用于测试的接口,通过添加私有方法作为(隐式公共)接口方法来扩展原始接口。然后,将实例化的对象转换为这个新的测试类型。这既满足了tscVS code类型检查。使用我的解决方案的示例:

interface INotification {
  validateTemplate(): boolean,
}

class Notification implements INotification {
  constructor() {}

  public validateTemplate() {
    return true;
  }

  private replacePlaceholder() {
    return true;
  }
}

测试:

import {Notification} from 'Notification';
import * as chai from "chai";

interface INotificationTest extends INotification {
  replacePlaceholder(): boolean;
}

describe("Notification", function(){

  describe('#validateTemplate - Validate template', function() {
    it('it should return success', function() {
      const result = new Notification() as INotificationTest;
      chai.expect(result.validateTemplate()).to.be.equal(true);
    });
  });
  describe('#replacePlaceholder - Replace Placeholder', function() {
    it('it should return success', function() {
      const result = new Notification() as INotificationTest;
      // Works!
      chai.expect(result.replacePlaceholder()).to.be.equal(true);
    });
  });
});

优点:

  • tscvs code不会报错
  • IntelliSense(或任何其他自动完成)都可以使用
  • 简单易懂(主观评价)
  • 如果您不想定义原始接口(INotification),您可以只完全定义测试接口(INotificationTest),而不是扩展并以同样的方式进行转换。

缺点:

  • 增加了样板代码
  • 需要同时更新并保持两个接口同步
  • 通过显式转换为非原始类型可能引入错误。

我让您决定是否值得这样做。在我的情况下,优点大于缺点。我已经使用jest测试过了,但我认为mocha.js在这方面也没有什么不同。

编辑:但总的来说,我同意Fenton's的答案。


这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - Barry Michael Doyle

2
将私有函数提取到一个单独的函数中,但不要将其外部可见。
这样做在语义上相当正确,因为私有函数是私有的,除了类本身之外任何人都不应该访问它。

2

这是我的操作:

  • 创建一个对象来保存目标方法
  • 使用object['method'] 记法获取该方法,并将其作为命名函数分配给 holder 对象
  • 将目标方法的执行上下文绑定到测试目标
  • 在 holder 对象中监视存储的方法
  • 调用 holder 上的该方法
  • 创建您的断言
  it('should mapToGraph', () => {
    const holder = {fn: component['mapToGraph'].bind(component)};
    const spy = jest.spyOn(holder, 'fn');
    holder.fn(mockInquiryCategory);
    expect(spy).toBeCalled();
  });

没有类型受到伤害。


1

有趣的是,这只是一种 TypeScript 错误(而不是 JavaScript),因此您可以通过以下方式进行修复:

// @ts-expect-error

一切都运行良好。

我认为这是一个合法的解决方案,因为目标是在这种特定情况下抑制 TypeScript。


0

虽然这里的答案可以工作,但我不认为它们是干净和正确的。

如果你想要(而且你应该)干净的代码,我向你提供“模拟”。

假设以下示例:

class CombatController implements ICombatController {
    /**
     * Protected for being able to test it
     */
    protected checkForAttackStyleAndValidate(): boolean {
        return false;
    }
}

现在,我们要测试的类是CombatController。我们将checkForAttackStyleAndValidate()方法设置为protected,因为我们想要对它进行测试。我喜欢在方法上方加注释,说明它只是为了测试而设置为protected。
现在,实际的CombatControllerTest.ts文件。
class CombatControllerMock extends CombatController {
  public checkForAttackStyleAndValidate_pub(): boolean {
    return super.checkForAttackStyleAndValidate();
  }
}

describe() {
    it('test', () => {
        let cc: CombatControllerMock = new CombatControllerMock ();
        assert.equal(cc.checkForAttackStyleAndValidate_pub(), true);
    })
}

讨论

有人可能会认为,将私有方法更改为受保护的方法是不必要的。然而,在类中很少使用继承,如果使用了,当使用受保护的方法时,您可以通过注释区分该方法是否仅用于测试目的。虽然有点棘手,但您可以确定某些受保护的方法不应从父级调用,但是所有这些逻辑都已封装在组合类中。从外部来看,您得到了一个漂亮的类。

可重用性

需要时,您可以将CombatControllerMock移动到单独的类中,并在不同的测试中重复使用它。通过这种方式,您可以为具有多个依赖项(例如,游戏对象,例如玩家、法术等)的复杂实例创建集成测试和复杂测试。

可定制性

如有需要,您可以为特定方法定义自定义行为,例如,在模拟抽象类时,您永远不打算调用该方法。

class Player implements IPlayer {    
    public damage(i: int): boolean{
        return false;
    }
    public abstract giveItem(i: IInventoryItemProto): boolean;
}

因此,在一个永远不会调用giveItem()的测试中,我们对其进行模拟。

class PlayerMock extends Player { 
    public giveItem(i: IInventoryItemProto): boolean{ return false; }
}

而且你也可以在不同的测试中使用PlayerMock,创建更复杂的测试模拟组合。

备选方案

正如这里的人建议的那样,你实际上可以调用一个私有方法,但你永远不应该这样做,除非它被用于mock的_pub()方法变体。然而,我强烈建议不要从任何其他地方调用私有方法,除了测试代码或Mock。


0
// module.ts
  private async privateMethod = () => "private method executed"
  public async testPrivateMethods(...args) {
        if (process.env.NODE_ENV === 'development') {
            return this.privateMethod(...args);
        }
    }

现在我们可以调用私有方法进行测试。在 Jest 文件中:

// module.spec.js
describe('Module', () => {
    let service: Module = new Module();

    it('private method should be defined', () => {
        expect(service.testPrivateMethods).toBeDefined();
    });
}

您需要设置环境变量名为NODE_ENV,其值必须为development

// .env
NODE_ENV="development"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接