Node

时间:2018-08-22 12:37:11

标签: javascript node.js ecmascript-6 functional-programming

我很难理解那些返回带有参数的函数并对其应用返回更多函数的函数的函数。 或类似的东西。我的头很痛。

我有以下代码:

const app = require('../app'); // an express.js instance
const request = require('supertest');
const cheerio = require('cheerio'); // html parser and jquery selector engine

const html = assertion => response => assertion(cheerio.load(response.text));
const node = selector => html($ => {
  const nodeset = $(selector);
  if (nodeset.length === 0)
    throw new Error('Expected "' + selector + '" to match at least 1 node, but it matched 0.');
});
const anyTextNode = (selector, value) => html($ => {
    const nodeset = $(selector);
    if (nodeset.filter((index, element) => element.children[0].type === 'text' && element.children[0].data === value).length === 0)
        throw new Error('Expected "' + selector + '" to match at least 1 node containing "' + value + '", but found 0.');
});

describe('index route', () => {
  describe('html representation', () => {
    it('should display a search form', (done) => {
      request(app).get('/')
        .expect(node('form>input[type=search][name=q]'))
        .expect(node('form>button[type=submit]'))
        .end(done);
    });
    it('should display a list of links', (done) => {
      request(app).get('/')
        .expect(anyTextNode('body>h2', 'Links'))
        .expect(node('ul>li>a[rel="http..."][href][href!=""]:not(:empty)'))
        .end(done);
    });

前两个期望值独立测试是否存在一个输入字段和一个按钮,并且每个元素都是表单的子元素,但是无法检查这两个元素是否是 same 表单的子元素。如果DOM的格式为<form><input/></form></form><button/></form>,它仍然会通过。
后两个期望值检查是否有标头和包含非空href的非空锚元素的列表。它不会检查列表紧随标题之后,并且如果我检查“ h2 + ul”,则无法证明那是相同的UL。

因此,我想添加一个新功能,使我能够构建复合测试:首先从给定的选择器中获取节点列表,然后执行其他类似于jQuery的操作,因此在第一个示例中它将采用“表单”选择器然后检查它有两个孩子。在第二个示例中,它将测试是否存在具有给定文本节点子级的H2,后跟UL,并且该UL的子级是有效链接。

当我尝试抽象化nodeanyTextNode之类的功能(其中有很多)以减少重复时,困难就来了。它们都调用html()并传递给执行检查的函数。然后,对html()的调用返回的函数将传递给supertest expect()调用,该调用将通过我正在测试的服务器的响应来调用它。我看不到使用好的设计模式。

2 个答案:

答案 0 :(得分:1)

假设您有一个应用

const app = express();

它有一个get方法

app.get('/', method );

控制器的职责应该是调用(例如)template.render和一些数据

const template = require('../path/to/templateEngine');
const method = (req, res) => {
    const data = { a: 1 };
    res.send(template.render('path/to/desided/template.ext', data));
}

它的职责是不返回特定的标记,因此在测试中您可以模拟模板(因为它是依赖项)

const template = require('../path/to/template');
template.render = // some spy jest.fn() or sinon.mock() or chai.spy()

describe('method', () => {
    it('should call the templateEngine.render to render the desired template', () => {
        request(app).get('/');
        // the template might even be an external dependency
        expect(template.render)
            .toBeCalledWith('path/to/desired/template.ext', { a: 1 });
    })
})

然后分别测试模板;

const template = require('../path/to/templateEngine');
const cheerio = require('cheerio')

describe('template' => {
  describe('given some mock data', () => {
     const mockData = { /* some mock data */ }
     it('should render the template with mocked data', () => {
        expect(template.render('path/to/desired/template.ext', mockData))
     });
  });
  // and here in the template specification it will be much cleaner 
  // to use cheerio
  describe('form>button', () => {
    const $ = cheerio.load(template.render('path/to/desired/template.ext', mockData));
    it('to have exactly one submit button', () => {
      expect($('form>button[type=submit]')).toHaveLength(1);
    });
  });
});

修改:

考虑到这是模板测试,您可以编写类似这样的内容(未经测试)

const cheerio = require('cheerio');
const request = require('supertest');
// template will be Promise<response>
const template = request(app).get('/')

describe('template', () => {
  // if $ is the result from cheerio.load
  // cherrioPromise will be Promise<$>
  // and you can use $.find('selector') to write your assertions
  const cheerioPromise = template.then(response => cheerio.load(response.text()))
  it("should display a search form", () => {
    // you should be able to return promise instead of calling done
    return cheerioPromise.then($ => {
      expect($.find('form>input[type=search][name=q]')).toHaveLength(1);
      expect($.find('form>button[type=submit]')).toHaveLength(1)
    })
  });
  it('should display a list of links', () => {
    return cheerioPromise.then($ => {
      expect($.find('ul>li>a[rel="http..."][href][href!=""]:not(:empty)')) /// some expectation
    });
  });
  it('should have h2 with text "Links"', () => {
    return cheerioPromise.then($ => {
      expect($.find('body.h2').text()).toEqual('Links');
    })
  })
});

答案 1 :(得分:1)

不确定是否可以解决您的所有问题,但可能有助于找到正确的方法:

通过使node接受其搜索上下文和查询,返回其结果,可以将其链接起来。这使您的错误消息更易于使用,并且您可以拆分查询。例如:

const node = (parent, query) => {
  const child = parent.querySelector(query);

  if (!child) 
    throw `Node "${query}" does not exist in ${parent}`;

  return child;
}

node(node(document, "form"), "input"); // Instead of `node("form input")`

这首先调用<form>的查询,如果没有则抛出查询(记录实际的“ form”元素丢失,从而使您知道在哪里寻找)。仅找到<form>之后的 ,然后在表单中搜索输入。

可以通过创建nodes函数来添加用于查找多个元素的用例:

const nodes = (parent, queries) => queries
    .map(q => node(parent, q));

示例

在下面的示例中,我制作了三个文档。我们的测试想断言:

  • 有一个表格
  • 该表格具有以下所有特征:
    • 文本输入
    • 密码输入
    • 提交输入

前两个div包含错误的HTML。最后一个通过测试。

const node = (parent, query) => {
  const child = parent.querySelector(query);
  
  if (!child) 
    throw `Node "${query}" does not exist in ${parent.id || parent}`;
    
  return child;
}

const nodes = (parent, queries) => {
  return queries
    .map(q => node(parent, q));
};



// Test containers:
[ "twoForms", "noForm", "correctForm" ]
  .map(id => document.getElementById(id))
  .forEach(parent => {
    try {
      nodes(
        node(parent, "form"),
        [
          "input[type=text]",
          "input[type=password]",
          "input[type=submit]"
        ]
      );
      console.log(`Wrapper ${parent.id} has a correct form`);
    } catch(err) { console.log(err); }
  });
div[id] { padding: .5em; border: 1px solid black; }
<div id="twoForms">
  Two forms:
  <form>
    <input type="text" placeholder="username">
    <input type="password" placeholder="password">
  </form>
  <form>
    <input type="submit" value="sign in">
  </form>
</div>

<div id="noForm">
  No forms:
  <input type="text" placeholder="username">
  <input type="password" placeholder="password">
  <input type="submit" value="sign in">
</div>

<div id="correctForm">
  Passing form:
  <form>
    <input type="text" placeholder="username">
    <input type="password" placeholder="password">
    <input type="submit" value="sign in">
  </form>
</div>