Пример использования метапрограммирования

Для чего нужны шпионы

Шпионы позволяют проверять функции для определенных шаблонов вызовов, например, для данного экземпляра класса:

const foo = new (class Foo {
    bar () { }
})

Мы можем подключить шпиона к методу bar:

foo.bar = chai.spy (foo.bar)

Затем сгенерируйте несколько вызовов:

foo.bar ('qux')

И, наконец, убедитесь, что он был правильно вызван:

foo.bar.should.have.been.called.with ('qux').once

Пока все выглядит хорошо. Но когда у вас есть много тестов с большим количеством классов и методов, это явное переопределение метода как бы мешает ...

Собирается декларативно

Было бы однозначно круто, если бы это можно было записать так:

const foo = new (class Foo {
   @will.have.been.called.with (42).once
   bar () { }
   @will.have.been.called.twice
   zap () { }
})
foo.bar (42)
foo.zap (); foo.zap ()

В этой статье я покажу вам, как реализовать это с помощью новейших функций метапрограммирования ECMAScript: декораторов, прокси и символов.

Реализация

Мы нацелены на Mocha, подключившись к его функции `it`, собирая вызовы декораторов и выполняя шпионские проверки после завершения теста.

Вот как выглядит обычный тест:

import 'chai-spies-decorators'
describe ('chai-spies-decorator', () => {
    it ('works', () => {
         /* Here's our test */
    })
})

Основная идея состоит в том, чтобы изменить функцию `it` так, чтобы она заключала в себе наш тест следующим образом:

checks = gatherChecks ()
/* Here's our test */
checks.forEach (check => check ())

Вся магия скрыта внутри gatherChecks. Он определяет глобальное свойство will:

Object.defineProperty (global, 'will', {
   configurable: true,
   get () { ... }

Что при доступе сгенерирует прокси-объект, который фиксирует цепочку обращений к свойствам и вызовов функций:

will.have.been.called.with (42).once
// captures ['have', 'been', 'called', 'with', [42], 'once']

Когда эта цепочка применяется к методу класса в качестве декоратора…

@will.have.been.called.with (42).once
method () { ... }

… Он выводит средство доступа к свойству, в котором мы генерируем (и запоминаем) «шпионскую» версию этого метода, а также обратный вызов проверки, который гарантирует, что метод был вызван в соответствии с контрактом:

const memo = Symbol () // for referencing the hidden state property
return {
    get () {
        if (this[memo]) {
            return this[memo] }  
           
        else {
            const spied = chai.spy (method)
            checks.push (() => {
                // executes the captured ['have', 'been', ...]
                // on a spied method
            })
            return (this[memo] = spied)
       }
   }
}

Проверка выполняется после завершения теста, генерируя цепочку утверждений Chai из захваченного представления массива. Таким образом, мы можем переводить вызовы API, даже не зная о них заранее.

Полный код

Реализация доступна на GitHub:



Как пакет NPM:

npm install chai-spies-decorators