为Node CommonJS模块创建不可变的类

时间:2018-07-31 12:15:59

标签: javascript node.js jestjs

我正在Node项目中创建一个基础类,我想使用Jest对其进行测试。我收到一个错误,暗示要在测试中使用“严格”模式,我想避免/修复该错误


〜/ lib / LogRecord.js

root

我正在对此进行测试:

module.exports = class LogRecord {
    constructor(level, message, timestamp) {
        this.level = level;
        this.message = message;
        this.timestamp = timestamp ? timestamp : Date.now();
    }

    get level() {
        return this.level;
    }

    get message() {
        return this.message;
    }

    get timestamp() {
        return this.timestamp;
    }
}

运行let LogRecord = require('../lib/logRecord'); describe('Test LogRecord functionality', () => { test('LogRecord constructor', () => { let timestamp = Date.now(); let logRecord = new LogRecord('INFO', 'Test Message', timestamp); expect(logRecord.level).toBe('INFO'); expect(logRecord.message).toBe('Test Message'); expect(logRecord.timestamp).toBe(timestamp); }); test('LogRecord is read-only', () => { let timestamp = Date.now(); let logRecord = new LogRecord('INFO', 'Test Message', timestamp); logRecord.level = 'WARN' logRecord.message = 'New Message' logRecord.timestamp = Date.now(); expect(logRecord.level).toBe('INFO'); expect(logRecord.message).toBe('Test Message'); expect(logRecord.timestamp).toBe(timestamp); }); }); 时,两个LogRecord测试都出现以下错误:

npm test

编辑-工人阶级

Test LogRecord functionality › LogRecord constructor

TypeError: Cannot set property level of #<LogRecord> which has only a getter

  1 | module.exports = class LogRecord {
  2 |     constructor(level, message, timestamp) {
> 3 |         this.level = level;
    |         ^
  4 |         this.message = message;
  5 |         this.timestamp = timestamp ? timestamp : Date.now();
  6 |     }

  at new LogRecord (lib/logRecord.js:3:9)
  at Object.test (test/logRecord.test.js:6:25)

2 个答案:

答案 0 :(得分:3)

测试是要确保您的代码执行了您认为的工作。考虑以下课程:

class Foo {
  constructor (bar) {
    this._bar = bar;
  }

  get bar () {
    return this._bar;
  }
}

这里bar只读的,无法设置bar属性:

let foo = new Foo('a foo');
foo.bar; // 'a foo'
foo.bar = 'am not'; // TypeError!

模块问题并非真正相关:因为注释类主体中链接的Logar始终是 严格模式,

因此,如果您希望属性是只读的,则无需担心编写它。工作流程可能看起来像这样:

  1. 编写空类Foo class Foo {}并构造一个实例foo = new Foo()
  2. 写测试以检查由于我们的类为空而导致bar失败
  3. 添加构造函数参数和getter
  4. 检查测试现在通过了
  5. 添加测试以确保尝试设置栏会引发预期错误*

如果您不希望使用只读属性,则可以添加一个setter:

class Foo {
  constructor (bar) {
    this._bar = bar;
  }

  get bar () {
    return this._bar;
  }

  set bar (value) {
    this._bar = value;
  }
}

在这种情况下,您将添加一个设置bar的测试,并读取更改后的值。

*您可能想知道为什么当规范保证了这种行为时为什么要进行此测试,并且我认为该测试是必需的,因为有人(对于调用者而言是透明的)可以将该类重构为老式构造函数,并且为错误创建向量:

// post refactor Foo
const Foo = function Foo(bar) {
  this.bar = bar; // danger! now writable!
};

希望这类事情会被知识渊博的审阅者发现,但是我还是会编写测试。

更新

如果您想要的是在构造函数中设置的保证的只读属性,则可以使用以下方法:

const data = new WeakMap();

module.exports = class Foo () {
  constructor (bar) {
    data.set(this, bar);
  }

  get bar () {
    return data.get(this);
  }
};

因为没有导出data,所以没有外部代码可以更改它。尝试设置实例的bar属性将引发错误。只是使用getter和setter定义下划线属性,这有点复杂,但是如果您要使用它,那么……我知道这种模式,因为我已经使用了它。

更新2

您只能为每个模块创建一个弱映射,而不是为每个类或实例创建一个。弱映射存储一个唯一数据条目,该条目键入到各个实例(即this):

const data = new WeakMap();

module.exports = {
  Foo: class Foo () {
    constructor (bar) {
      data.set(this, bar);
    }

    get bar () {
      return data.get(this);
    }
  },

  Bar: class Bar () {
    constructor (prop1, prop2) {

      // for multiple props we'll store an object...
      data.set(this, { prop2, prop1 });
    }

    get prop1 () {
      // ...and retrieve it and access it's props to return
      return data.get(this).prop1;
    }

    get prop2 () {
      return data.get(this).prop2;
    }
  }
};

请注意,使用吸气剂设置道具,但不会有任何设置器会抛出...

// in someotherfile.js
const { Foo } = require('path/to/file/with/foo.js');
const foo = new Foo('imma foo');
foo.bar; // 'imma foo'
foo.bar = 'no not really'; // TypeError!

// you can still set random properties that don't have a getter:
foo.baz = 'I do not throw';
foo.baz; // 'I do not throw'

答案 1 :(得分:2)

如果希望仅在对象初始化之后读取属性,则可以在构造函数中使用Object.freeze,并删除您的吸气剂:

class LogRecord {
    constructor(level, message, timestamp) {
        this.level = level;
        this.message = message;
        this.timestamp = timestamp ? timestamp : Date.now();
        Object.freeze(this);
    }
}

但这会冻结对象的所有属性。之后,您将无法修改,删除或添加任何内容。没有深入研究这一点,因此它可能也存在一些缺陷