实例

时间:2018-04-20 22:50:30

标签: javascript node.js sinon slack proxyquire

我有一个Express应用程序,当某些端点被命中时,使用node-slack-sdk向Slack发帖。我正在尝试为一个路由编写集成测试,该路由在许多其他方面调用该库中的方法。

我想阻止Slack库中某些方法的所有默认行为,并简单地断言用某些参数调用这些方法。

我试图简化问题。如何在chat的实例中存根一个方法(实际上嵌套在WebClient内),阻止原始功能,并对其所调用的参数进行断言?

我已经尝试过很多无法解决的问题,所以我编辑了这个并提供了一个非常简化的设置:

index.html

const express = require('express');
const {WebClient} = require('@slack/client');
const app = express();
const web = new WebClient('token');

app.post('/', (req, res) => {

    web.chat.postMessage({
        text: 'Hello world!',
        token: '123'
    })
        .then(() => {
            res.json({});
        })
        .catch(err => {
            res.sendStatus(500);
        });
});

module.exports = app;

index.test.html

'use strict';
const app = require('../index');
const chai = require('chai');
const chaiHttp = require('chai-http');
const sinon = require('sinon');

const expect = chai.expect;
chai.use(chaiHttp);

const {WebClient} = require('@slack/client');


describe('POST /', function() {
    before(function() {
        // replace WebClient with a simplified implementation, or replace the whole module.
    });

    it('should call chat.update with specific arguments', function() {
        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                // assert that web.chat.postMessage was called with {message: 'Hello world!'}, etc
        });
    });
});

有一些事情使这很困难,而不像其他例子。其一,我们无法访问测试中的web实例,因此我们无法直接存根。二,该方法隐藏在chat属性web.chat.postMessage内,这也与我在sinon,proxyquire等文档中看到的其他示例不同。

3 个答案:

答案 0 :(得分:2)

您的示例的设计不是非常可测试的,这就是您遇到这些问题的原因。为了使其更具可测性和连贯性,最好传入WebClient对象和其他依赖项,而不是在路由中创建它们。

const express = require('express');
const {WebClient} = require('@slack/client');
const app = express();//you should be passing this in as well. But for the sake of this example i'll leave it


module.exports = function(webClient) {
   app.post('/', (req, res) => {

       web.chat.postMessage({
          text: 'Hello world!',
          token: '123'
       })
           .then(() => {
              res.json({});
           })
           .catch(err => {
              res.sendStatus(500);
           });
   })
   return app;
};

为了实现这一点,请在更高的模块中构建对象/路由。 (您可能需要编辑为您生成的快速文件。我不确定,我个人使用重构的快速版本来满足我的需求。)通过传入WebClient,您现在可以为测试创建存根。

'use strict';

const chai = require('chai');
const chaiHttp = require('chai-http');
const sinon = require('sinon');

const expect = chai.expect;
chai.use(chaiHttp);
const {WebClient} = require('@slack/client');
const web = new WebClient('token');
let app = require('../index')(web);

describe('POST /', function() {

    it('should call chat.update with specific arguments', function() {
        const spy = sinon.spy();
        sinon.stub(web.chat, 'postMessage').callsFake(spy);

        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                assert(spy.calledWith({message: 'Hello world!'}));
        });
    });
});

这称为Dependency Injection。而不是让索引模块构建它的依赖关系,WebClient,您的更高模块将传递依赖关系,以便它们控制它的较低模块的状态。您的更高模块,您的测试,现在具有为下层模块创建存根所需的控制,索引。

上面的代码只是快速的工作。我没有经过测试,看它是否有效,但它应该回答你的问题。

答案 1 :(得分:1)

所以@Plee,在结构方面有一些好处。但我的答案更多的是关于手头的问题,如何使测试工作和你需要理解的事情。为了更好地编写单元测试,你应该使用其他好的资源,比如书籍和文章,我假设在线上会有很多很好的资源

你在测试中做错的第一件事是第一行本身

const app = require('../index');

执行此操作,您加载index文件,然后执行以下代码

const {WebClient} = require('@slack/client');
const app = express();
const web = new WebClient('token');

现在模块已加载原始@slack/client并创建了一个在模块外无法访问的对象。所以我们失去了定制/间谍/存根模块的机会。

所以第一个拇指规则

  

切勿在测试中全局加载此类模块。或者在存根之前永远不加载

接下来我们想要的是,在我们的测试中,我们应该加载我们想要存根的原始客户端库

'use strict';
const {WebClient} = require('@slack/client');
const sinon = require('sinon');

既然我们无法在index.js中获取创建的对象,我们需要在创建对象时捕获它。这可以像下面这样完成

var current_client = null;

class MyWebClient extends WebClient {
    constructor(token, options) {
        super(token, options);
        current_client = this;
    }
}

require('@slack/client').WebClient = MyWebClient;

现在我们所做的是原始WebClient被我们的MyWebClient取代,当有人创建相同的对象时,我们只是在current_client中捕获它。这假定只从我们加载的模块中创建一个对象。

接下来是更新我们的before方法来存根web.chat.postMessage方法。因此,我们更新了我们的before方法,如下所示

before(function() {
    current_client = null;
    app = require('../index');
    var stub = sinon.stub();
    stub.resolves({});
    current_client.chat.postMessage = stub;
});

现在出现了测试功能,我们将在下面更新

it('should call chat.update with specific arguments', function() {
    return chai.request(app).post('/').send({})
        .then(function(res) {
            expect(res).to.have.status(200);
            expect(current_client.chat.postMessage
                .getCall(0).args[0]).to.deep.equal({
                text: 'Hello world!',
                token: '123'
            });
        });
});

,结果是积极的

Results

以下是我使用的完整index.test.jsindex.js未更改

'use strict';
const {WebClient} = require('@slack/client');
const sinon = require('sinon');

var current_client = null;

class MyWebClient extends WebClient {
    constructor(token, options) {
        super(token, options);
        current_client = this;
    }
}

require('@slack/client').WebClient = MyWebClient;

const chai = require('chai');
const chaiHttp = require('chai-http');

const expect = chai.expect;
chai.use(chaiHttp);


let app = null;
describe('POST /', function() {
    before(function() {
        current_client = null;
        app = require('../index');
        var stub = sinon.stub();
        stub.resolves({});
        current_client.chat.postMessage = stub;
    });

    it('should call chat.update with specific arguments', function() {
        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                expect(current_client.chat.postMessage
                    .getCall(0).args[0]).to.deep.equal({
                    text: 'Hello world!',
                    token: '123'
                });
            });
    });
});

答案 2 :(得分:1)

基于其他评论,您似乎处于一个代码库中,制作一个激烈的重构会很困难。所以这是我如何在不对index.js进行任何更改的情况下进行测试。

我在这里使用rewire库从索引文件中获取并存根web变量。

'use strict';

const rewire = require('rewire');
const app = rewire('../index');

const chai = require('chai');
const chaiHttp = require('chai-http');
const sinon = require('sinon');

const expect = chai.expect;
chai.use(chaiHttp);

const web = app.__get__('web');

describe('POST /', function() {
    beforeEach(function() {
        this.sandbox = sinon.sandbox.create();
        this.sandbox.stub(web.chat);
    });

    afterEach(function() {
        this.sandbox.restore();
    });

    it('should call chat.update with specific arguments', function() {
        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                const called = web.chat.postMessage.calledWith({message: 'Hello world!'});
                expect(called).to.be.true;
        });
    });
});