Reputation: 10981
I am having difficulty getting my head around functions that return functions that take an argument and apply functions to it that return more functions. Or something like that. My head hurts.
I have the following code:
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);
});
The first two expectations independently test that there is an input field and a button, and that each are children of a form, but it cannot check that the two elements are children of the same form. It would still pass if the DOM was of the form <form><input/></form></form><button/></form>
.
The second two expectations check that there is a header and a list containing non-empty anchor elements with non-empty hrefs. It does not check that the list immediately follows the header, and if I put in a check for 'h2+ul' there is no way to prove that that is the same UL.
So, I want to add a new function that allows me to build compound tests: first obtain a nodelist from a given selector, then perform other jQuery-ish actions, so in the first example it would take a 'form' selector and then check that it had those two children. In the second example it would test for the existence of a H2 with given text node child, followed by a UL, and that the children of that UL are valid links.
The difficulty comes when I try to abstract the functions like node
and anyTextNode
(there are a quite few of them) to reduce duplication. They all call html()
and pass to that a function that performs the checks. The function that the calls to html()
return is then passed to the supertest expect()
call, which invokes it with the response of the server I am testing. I can't see a good design pattern to use.
Upvotes: 3
Views: 166
Reputation: 32148
Let's say you have an app
const app = express();
and it has a get method
app.get('/', method );
the responsibility of the controller should be to call (for example) template.render
with some data
const template = require('../path/to/templateEngine');
const method = (req, res) => {
const data = { a: 1 };
res.send(template.render('path/to/desided/template.ext', data));
}
it's responsibility is not to return specific markup so in your test you can mock the template (since it's a dependency)
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 });
})
})
and then test the template separately;
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);
});
});
});
Edit:
considering it's a template test you can write it something like this (not tested)
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');
})
})
});
Upvotes: 1
Reputation: 23372
Not sure if it solves all of your problems, but it might help find the right track:
By making node
accept both its search context and a query, and return its result, it can be chained. This makes your error messages easier to work with, and you can split up your queries. E.g.:
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")`
This calls the query for <form>
first, and throws if there isn't any (logging that the actual "form" element is missing, giving you an idea where to look for). Only after the <form>
has been found, the form is searched for an input.
The use-case for looking for multiple elements can be added by creating a nodes
function:
const nodes = (parent, queries) => queries
.map(q => node(parent, q));
In the example below I've made three documents. Our test wants to assert that:
The first two divs contain faulty HTML. The last one passes the test.
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>
Upvotes: 1