Comparing version 0.0.2 to 0.1.0
@@ -5,6 +5,11 @@ 'use strict' | ||
* @class | ||
* @param {number} sourceLine | ||
*/ | ||
function Obj() { | ||
function Obj(sourceLine) { | ||
// The inner value, either a hash map or a string | ||
this.value = Object.create(null) | ||
this.source = { | ||
begin: sourceLine, | ||
end: sourceLine | ||
} | ||
// Store the last prop name for each level | ||
@@ -54,4 +59,5 @@ this._lastProps = [] | ||
this._empty = false | ||
this.source.end++ | ||
} | ||
module.exports = Obj |
{ | ||
"name": "api-test", | ||
"version": "0.0.2", | ||
"version": "0.1.0", | ||
"author": "Sitegui <sitegui@sitegui.com.br>", | ||
@@ -5,0 +5,0 @@ "description": "API testing made simple", |
116
parse.js
@@ -47,31 +47,38 @@ /** | ||
var lines = [], | ||
originalLines = text.split(/\r?\n/), | ||
test = new Test, | ||
i | ||
i, line | ||
// Tokenizer | ||
text.split(/\r?\n/).forEach(function (line) { | ||
if (line[0] === '#') { | ||
// Header line | ||
lines.push(new Header(line)) | ||
} else if (line[0] === '\t') { | ||
// Object line | ||
if (!(lines[lines.length - 1] instanceof Obj)) { | ||
lines.push(new Obj(line)) | ||
try { | ||
// Lexical parsing (put tokens into lines array) | ||
for (i = 0; i < originalLines.length; i++) { | ||
line = originalLines[i] | ||
if (line[0] === '#') { | ||
// Header line | ||
lines.push(new Header(line, i)) | ||
} else if (line[0] === '\t') { | ||
// Object line | ||
if (!(lines[lines.length - 1] instanceof Obj)) { | ||
lines.push(new Obj(i)) | ||
} | ||
lines[lines.length - 1].push(line) | ||
} | ||
lines[lines.length - 1].push(line) | ||
} | ||
}) | ||
} catch (e) { | ||
// Add source code info | ||
e.message += getSourceContext(originalLines, i) | ||
throw e | ||
} | ||
// Syntax parsing (parse tokens from lines array) | ||
i = 0 | ||
// Header | ||
if (!checkHeader(lines[0], 1)) { | ||
throw new Error('The first line must be a header') | ||
} | ||
test.name = lines[0].value | ||
i = parseHeader(test, lines, i, originalLines) | ||
// Fixup | ||
i = 1 | ||
if (checkHeader(lines[i], 2, 'DB')) { | ||
i = 2 | ||
i++ | ||
while (i < lines.length && !checkHeader(lines[i], 2)) { | ||
i = parseDBItem(test, lines, i) | ||
i = parseDBItem(test, lines, i, originalLines) | ||
} | ||
@@ -81,3 +88,3 @@ } | ||
while (i < lines.length) { | ||
i = parseCase(test, lines, i) | ||
i = parseCase(test, lines, i, originalLines) | ||
} | ||
@@ -89,9 +96,21 @@ | ||
/** | ||
* Try to parse the test header | ||
* @throws if the syntax is invalid | ||
*/ | ||
function parseHeader(test, lines, i, originalLines) { | ||
if (!checkHeader(lines[i], 1)) { | ||
throwSyntaxError('Expected a header', lines[i], originalLines) | ||
} | ||
test.name = lines[i].value | ||
return i + 1 | ||
} | ||
/** | ||
* Try to parse a DB insertion/clear | ||
* @throws if the syntax is invalid | ||
*/ | ||
function parseDBItem(test, lines, i) { | ||
function parseDBItem(test, lines, i, originalLines) { | ||
var match | ||
if (!checkHeader(lines[i], 3)) { | ||
throw new Error('The first line of a DB insertion/clear must be "### ..."') | ||
throwSyntaxError('Expected "### ..."', lines[i], originalLines) | ||
} | ||
@@ -104,3 +123,3 @@ | ||
if (!(lines[i + 1] instanceof Obj)) { | ||
throw new Error('The second part of a DB insertion must be an {obj}') | ||
throwSyntaxError('Expected an {obj}', lines[i + 1], originalLines) | ||
} | ||
@@ -110,3 +129,3 @@ test.insertions.push(new Insertion(match[1], match[2], lines[i + 1].value)) | ||
} else { | ||
throw new Error('The first line of a DB insertion/clear must be either "### _docName_ in _collection_" or "### Clear _collection_"') | ||
throwSyntaxError('Expected either "### _docName_ in _collection_" or "### Clear _collection_"', lines[i], originalLines) | ||
} | ||
@@ -119,13 +138,13 @@ } | ||
*/ | ||
function parseCase(test, lines, i) { | ||
function parseCase(test, lines, i, originalLines) { | ||
if (!checkHeader(lines[i], 2)) { | ||
throw new Error('The first line of a test case must be "## _caseName_"') | ||
throwSyntaxError('Expected "## _caseName_"', lines[i], originalLines) | ||
} else if (!checkHeader(lines[i + 1], 3, 'Post')) { | ||
throw new Error('The second line of a test case must be "### Post"') | ||
throwSyntaxError('Expected "### Post"', lines[i + 1], originalLines) | ||
} else if (!(lines[i + 2] instanceof Obj)) { | ||
throw new Error('The third line of a test case must be an {obj}') | ||
throwSyntaxError('Expected an {obj}', lines[i + 2], originalLines) | ||
} else if (!checkHeader(lines[i + 3], 3, 'Out')) { | ||
throw new Error('The fourth line of a test case must be "### Out"') | ||
throwSyntaxError('Expected "### Out"', lines[i + 3], originalLines) | ||
} else if (!(lines[i + 4] instanceof Obj)) { | ||
throw new Error('The fifth line of a test case must be an {obj}') | ||
throwSyntaxError('Expected an {obj}', lines[i + 4], originalLines) | ||
} | ||
@@ -139,5 +158,5 @@ | ||
if (!checkHeader(lines[i], 3) || lines[i].value.indexOf('Find in ') !== 0) { | ||
throw new Error('The first line of a find must be "### Find in _collection_"') | ||
throwSyntaxError('Expected "### Find in _collection_"', lines[i], originalLines) | ||
} else if (!(lines[i + 1] instanceof Obj)) { | ||
throw new Error('The second line of a find must be an {obj}') | ||
throwSyntaxError('Expected an {obj}', lines[i + 1], originalLines) | ||
} | ||
@@ -165,6 +184,37 @@ testCase.finds.push(new Find(lines[i].value.substr(8).trim(), lines[i + 1].value)) | ||
* @param {string} line must start with '#' | ||
* @param {number} sourceLine | ||
*/ | ||
function Header(line) { | ||
function Header(line, sourceLine) { | ||
this.level = line.match(/^#+/)[0].length | ||
this.value = line.substr(this.level).trim() | ||
this.source = { | ||
begin: sourceLine, | ||
end: sourceLine + 1 | ||
} | ||
} | ||
/** | ||
* @param {string[]} originalLines | ||
* @param {number} start | ||
* @param {number} [end=start+1] | ||
* @param {number} [context=3] | ||
*/ | ||
function getSourceContext(originalLines, start, end, context) { | ||
end = end || start + 1 | ||
context = context || 3 | ||
var i, str = '\n\n-----' | ||
for (i = Math.max(0, start - context); i < end + context && i < originalLines.length; i++) { | ||
str += '\n' + (i >= start && i < end ? '>' : ' ') + ' ' + originalLines[i] | ||
} | ||
return str + '\n-----' | ||
} | ||
/** | ||
* @param {string} msg | ||
* @param {(Header|Obj)} token | ||
* @param {string[]} originalLines | ||
*/ | ||
function throwSyntaxError(msg, token, originalLines) { | ||
throw new Error(msg + getSourceContext(originalLines, token.source.begin, token.source.end)) | ||
} |
@@ -8,4 +8,2 @@ # API Test | ||
**WORKING IN PROGRESS** | ||
## Usage | ||
@@ -66,3 +64,3 @@ Create a test file 'test/api-test/sample.md' to test the 'item/get' endpoint, like this: | ||
The syntax for _docDescription_ is described at ## object syntax | ||
The syntax for _docDescription_ is described at bellow | ||
@@ -86,2 +84,47 @@ #### Clearing collections | ||
In all cases, the syntax is described at ## object syntax | ||
In all cases, the syntax is described bellow | ||
## Object syntax | ||
The syntax was designed to be concise and expressive. The values will be eval'ed as normal JS with a context with special variables (see default `context bellow`). | ||
The object can be a simple JS value, like: | ||
``` | ||
new Date | ||
``` | ||
Or an object with one property by line and tabs used to declare sub-objects: | ||
``` | ||
user: | ||
name: | ||
first: 'Happy' | ||
last: 'Customer' | ||
age: 37 + 2 | ||
country: 'cm'.toUpperCase() | ||
``` | ||
## Default context | ||
* `randomId()`: return a random mongo-id as a 24-hex-char string | ||
* `randomStr(len)`: return a random string with length `len` | ||
* `empty`: the empty object `{}` | ||
* `post`: the request body of the current test case | ||
* `out`: the response body of the current test case | ||
* `prev`: an object wity keys: | ||
* `post`: the request body of the previous request | ||
* `out`: the response body of the previous request | ||
## Options | ||
* `mongoUri`: the mongo uri to connect to. The hostname SHOULD be 'localhost' and the db name SHOULD contains 'test'. If not, the code will ask for confirmation. This protects one from dropping production data, since the tests automatically clear collections, before inserting docs. | ||
* `baseUrl`: the base API url. Every request url will be composed from this base and the test name. | ||
* `describe`, `it`, `before`: (optional) the mocha interface. Defaults to global mocha functions | ||
* `context`: (optional) define your own variables/functions accessible from object definitions, to help writing tests. | ||
## Examples | ||
See more test examples in the folder 'test/api-test' | ||
## Run test | ||
Run `node index` in 'test/api' to start the a simple API webservice. Then (in another terminal instance), run `npm test` in the project root folder. | ||
## Road map | ||
* Array notation: there is no way to declare an array yet | ||
* Better failure message |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24857
620
127