alle new
This commit is contained in:
erik committed 2026-10-01 06:46:51 +02:00
1 parent da64472251
commit 5b20adee48
1605 files changed
+396517 -1674

No files matched your search

Generated Vendored
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2013, Michael Williamson
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+184
View File
@@ -0,0 +1,184 @@
# duck.js -- rich matchers with helpful messages on match failure
duck.js allows you to perform assertions on complex objects.
When those assertions fail, duck.js will try to produce helpful error messages.
For instance, suppose you want to assert the same property on an array of objects:
```javascript
var duck = require("duck");
var isArray = duck.isArray;
var hasProperties = duck.hasProperties;
var users = fetchUsers();
duck.assertThat(users, isArray([
hasProperties({name: "Bob"}),
hasProperties({name: "Jim"}),
]));
```
which might produce an error message like:
```
Expected [object with properties {
name: 'Bob'
}, object with properties {
name: 'Jim'
}]
but element at index 0 didn't match:
value of property "name" didn't match:
was 'Jim'
expected 'Bob'
expected object with properties {
name: 'Bob'
}
element at index 1 didn't match:
value of property "name" didn't match:
was 'Bob'
expected 'Jim'
expected object with properties {
name: 'Jim'
}
```
## API
The below is a quick reference to the API.
For more examples, take a look at the tests.
### duck.assertThat(value, matcher)
Assert that `value` satifies `matcher`.
If `value` satifies `matcher`, return normally, otherwise throw an
AssertionError describing the mismatch.
### duck.is(value)
If `value` is a matcher, return that matcher,
otherwise return `duck.equalTo(value)`.
### duck.equalTo(value)
Matcher for deep equality on `value`.
### duck.isObject(matcherObj)
An object `obj` matches `duck.isObject(matcherObj)` if:
* `obj` matches `duck.hasProperties(matcherObj)`, and
* there is no key that is present in `obj` but not in `matcherObj`
Sample usage:
```javascript
duck.isObject({
name: "Bob",
address: duck.isObject({
city: "Cambridge",
county: "UK"
})
})
```
`duck.is` is called on each value of the matcher object, meaning that the
above is equivalent to:
```javascript
duck.isObject({
name: duck.is("Bob"),
address: duck.isObject({
city: duck.is("Cambridge"),
county: duck.is("UK")
})
})
```
### duck.hasProperties(matcherProperties)
An object `obj` matches `duck.hasProperties(matcherProperties)` if,
for each `key` in `matcherProperties`, `matcherProperties[key].matches(obj[key])`
Sample usage:
```javascript
duck.hasProperties({
name: "Bob",
address: duck.hasProperties({
city: "Cambridge",
county: "UK"
})
})
```
`duck.is` is called on each value of the matcher object, meaning that the
above is equivalent to:
```javascript
duck.hasProperties({
name: duck.is("Bob"),
address: duck.hasProperties({
city: duck.is("Cambridge"),
county: duck.is("UK")
})
})
```
### duck.isArray(matcherArray)
An array `blah` matches `duck.isArray(matcherArray)` if:
* `blah.length == matcherArray.length`, and
* For `0 <= i < array.length`, `matcherArray[i].matches(blah[i])`
Sample usage:
```javascript
duck.isArray([
duck.hasProperties({name: "Bob"}),
duck.hasProperties({name: "Jim"}),
]))
```
`duck.is` is called on each element of the matcher array, meaning that the
following are equivalent:
```javascript
duck.isArray(["apple", "banana"])
duck.isArray([duck.is("apple"), duck.is("banana")])
```
### Matcher
Each matcher has the following methods:
#### matcher.matches(value)
Return `true` if `value` satifies this matcher, false otherwise.
#### matcher.describeMismatch(value)
Generate a string describing why `value` doesn't satisfy this matcher.
Behaviour is undefined if `value` actually satisifies the matcher.
#### matcher.matchesWithDescription(value)
Equivalent to:
```javascript
var isMatch = this.matches(value);
return {
matches: isMatch,
description: isMatch ? "" : this.describeMismatch(value)
};
```
Useful if you're likely to want both the boolean and the mismatch description.
#### matcher.describeSelf()
Generate a string describing the matcher.
## Thanks
Thanks to [Hamcrest](http://hamcrest.org/) for inspiration.
Generated Vendored Executable
+267
View File
@@ -0,0 +1,267 @@
var util = require("util");
var assert = require("assert");
var _ = require("underscore");
var inspect = function(value) {
return util.inspect(value, false, null);
};
exports.assertThat = function(value, matcher) {
var result = matcher.matchesWithDescription(value);
var message = "Expected " + matcher.describeSelf() +
"\nbut " + result.description;
assert.ok(result.matches, message);
};
exports.is = function(value) {
if (value && value._isDuckMatcher) {
return value;
} else {
return equalTo(value);
}
};
var equalTo = exports.equalTo = function(matchValue) {
return new Matcher({
matches: function(value) {
return _.isEqual(value, matchValue);
},
describeMismatch: function(value) {
return "was " + inspect(value);
},
describeSelf: function() {
return inspect(matchValue);
}
});
};
exports.isObject = function(object) {
var matchers = valuesToMatchers(object);
return new Matcher({
matchesWithDescription: function(value) {
var expectedKeys = ownKeys(object);
var hasPropertiesResult = exports.hasProperties(matchers).matchesWithDescription(value);
var unexpectedPropertyMismatches = ownKeys(value).filter(function(key) {
return expectedKeys.indexOf(key) === -1
}).map(function(key) {
return "unexpected property: \"" + key + "\"";
});
var mismatchDescriptions =
(hasPropertiesResult.matches ? [] : [hasPropertiesResult.description])
.concat(unexpectedPropertyMismatches);
if (mismatchDescriptions.length === 0) {
return {matches: true};
} else {
return {matches: false, description: mismatchDescriptions.join("\n")};
}
},
describeSelf: function() {
return formatObjectOfMatchers(matchers);
}
});
};
exports.hasProperties = function(object) {
var matchers = valuesToMatchers(object);
return new Matcher({
matchesWithDescription: function(value) {
var expectedKeys = ownKeys(object);
expectedKeys.sort(function(first, second) {
if (first < second) {
return -1;
} else if (first > second) {
return 1;
} else {
return 0;
}
});
var propertyResults = expectedKeys.map(function(key) {
var propertyMatcher = matchers[key];
if (!objectHasOwnProperty(value, key)) {
return {matches: false, description: util.format("missing property: \"%s\"", key)};
} else if (!propertyMatcher.matches(value[key])) {
var description = "value of property \"" + key + "\" didn't match:\n" +
" " + indent(propertyMatcher.describeMismatch(value[key]), 1) + "\n" +
" expected " + indent(propertyMatcher.describeSelf(), 1);
return {matches: false, description: description};
} else {
return {matches: true};
}
});
return combineMatchResults(propertyResults);
},
describeSelf: function() {
return "object with properties " + formatObjectOfMatchers(matchers);
}
});
};
exports.isArray = function(expectedArray) {
var elementMatchers = expectedArray.map(exports.is);
return new Matcher({
matchesWithDescription: function(value) {
if (value.length !== elementMatchers.length) {
return {matches: false, description: "was of length " + value.length};
} else {
var elementResults = _.zip(elementMatchers, value).map(function(values, index) {
var expectedMatcher = values[0];
var actual = values[1];
if (expectedMatcher.matches(actual)) {
return {matches: true};
} else {
var description = "element at index " + index + " didn't match:\n " + indent(expectedMatcher.describeMismatch(actual), 1)
+ "\n expected " + indent(expectedMatcher.describeSelf(), 1);
return {matches: false, description: description};
}
});
return combineMatchResults(elementResults);
}
},
describeSelf: function() {
return util.format("[%s]", _.invoke(elementMatchers, "describeSelf").join(", "));
}
});
};
var Matcher = function(matcher) {
this._matcher = matcher;
this._isDuckMatcher = true;
};
Matcher.prototype.matches = function(value) {
if (this._matcher.matches) {
return this._matcher.matches(value);
} else {
return this._matcher.matchesWithDescription(value).matches;
}
};
Matcher.prototype.describeMismatch = function(value) {
if (this._matcher.describeMismatch) {
return this._matcher.describeMismatch(value);
} else {
return this._matcher.matchesWithDescription(value).description;
}
};
Matcher.prototype.matchesWithDescription = function(value) {
if (this._matcher.matchesWithDescription) {
var result = this._matcher.matchesWithDescription(value);
if (result.matches) {
return {
matches: true,
description: ""
};
} else {
return result;
}
} else {
var isMatch = this.matches(value);
return {
matches: isMatch,
description: isMatch ? "" : this.describeMismatch(value)
};
}
};
Matcher.prototype.describeSelf = function() {
return this._matcher.describeSelf();
};
var combineMatchResults = function(results) {
var mismatches = results.filter(function(result) {
return !result.matches;
});
return combineMismatchs(mismatches);
};
var combineMismatchs = function(mismatches) {
if (mismatches.length === 0) {
return {matches: true};
} else {
var mismatchDescriptions = mismatches.map(function(mismatch) {
return mismatch.description;
});
return {matches: false, description: mismatchDescriptions.join("\n")};
}
};
var ownKeys = function(obj) {
var keys = [];
for (var key in obj) {
if (objectHasOwnProperty(obj, key)) {
keys.push(key);
}
}
return keys;
};
var objectHasOwnProperty = function(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
};
var objectMap = function(obj, func) {
var matchers = {};
_.forEach(obj, function(value, key) {
if (_.has(obj, key)) {
matchers[key] = func(value, key);
}
});
return matchers;
};
var valuesToMatchers = function(obj) {
return objectMap(obj, exports.is);
};
var formatObject = function(obj) {
if (_.size(obj) === 0) {
return "{}";
} else {
return util.format("{%s\n}", formatProperties(obj));
}
};
var formatProperties = function(obj) {
var properties = _.map(obj, function(value, key) {
return {key: key, value: value};
});
var sortedProperties = _.sortBy(properties, function(property) {
return property.key;
});
return "\n " + sortedProperties.map(function(property) {
return indent(property.key + ": " + property.value, 1);
}).join(",\n ");
};
var formatObjectOfMatchers = function(matchers) {
return formatObject(objectMap(matchers, function(matcher) {
return matcher.describeSelf();
}));
};
var indent = function(str, indentationLevel) {
var indentation = _.range(indentationLevel).map(function() {
return " ";
}).join("");
return str.replace(/\n/g, "\n" + indentation);
};
exports.any = new Matcher({
matchesWithDescription: function() {
return {matches: true};
},
describeSelf: function() {
return "<any>";
}
});
Generated Vendored Executable
+4
View File
@@ -0,0 +1,4 @@
.PHONY: test
test:
node_modules/.bin/nodeunit `find test -name \*.test.js -type f`
+23
View File
@@ -0,0 +1,23 @@
{
"name": "duck",
"version": "0.1.12",
"author": "Michael Williamson <mike@zwobble.org>",
"description": "Rich matchers inspired by Hamcrest. Useful for generating helpful assertion failure messages in tests.",
"license": "BSD",
"repository": {
"type": "git",
"url": "https://github.com/mwilliamson/duck.js.git"
},
"keywords": [
"test",
"assert",
"matcher",
"assertThat"
],
"dependencies": {
"underscore": "^1.13.1"
},
"devDependencies": {
"nodeunit": "~0.8.0"
}
}
Generated Vendored Executable
+160
View File
@@ -0,0 +1,160 @@
var duck = require("../");
var testMatcher = function(options) {
return function(test) {
var matcher = options.matcher;
var description = options.description;
var positives = options.positives;
var negatives = options.negatives;
test.equal(matcher.describeSelf(), description);
positives.forEach(function(positive) {
test.same(true, matcher.matches(positive));
var result = matcher.matchesWithDescription(positive);
test.same(true, result.matches);
test.same("", result.description);
});
negatives.forEach(function(negative) {
test.same(false, matcher.matches(negative.value));
test.same(negative.description, matcher.describeMismatch(negative.value));
var result = matcher.matchesWithDescription(negative.value);
test.same(false, result.matches);
test.same(negative.description, result.description);
});
test.done();
};
};
exports.isMatchesPrimitiveValues = testMatcher({
matcher: duck.is(1),
description: "1",
positives: [1],
negatives: [
{value: 2, description: "was 2"},
{value: "1", description: "was '1'"},
{value: null, description: "was null"}
]
});
exports.isMatchesObjectUsingIsEqual = testMatcher({
matcher: duck.is({}),
description: "{}",
positives: [{}],
negatives: [
{value: {hair: "none"}, description: "was { hair: 'none' }"}
]
});
exports.isDoesNothingToMatchers = testMatcher({
matcher: duck.is(duck.is(1)),
description: "1",
positives: [1],
negatives: [
{value: 2, description: "was 2"},
{value: "1", description: "was '1'"},
{value: null, description: "was null"}
]
});
exports.isObjectMatchesValuesExactly = testMatcher({
matcher: duck.isObject({
name: "Bob",
age: 24
}),
description: "{\n age: 24,\n name: 'Bob'\n}",
positives: [{name: "Bob", age: 24}],
negatives: [
{value: {name: "Bob"}, description: "missing property: \"age\""},
{value: {}, description: "missing property: \"age\"\nmissing property: \"name\""},
{value: {name: "bob", age: 24}, description: "value of property \"name\" didn't match:\n was 'bob'\n expected 'Bob'"},
{value: {name: "Bob", age: 24, hair: "none"}, description: "unexpected property: \"hair\""}
]
});
exports.isObjectSubDescriptionsAreIndented = testMatcher({
matcher: duck.isObject({artist: duck.isObject({name: "Bob"})}),
description: "{\n artist: {\n name: 'Bob'\n }\n}",
positives: [],
negatives: [
{
value: {artist: {name: "Jim"}},
description:
"value of property \"artist\" didn't match:\n" +
" value of property \"name\" didn't match:\n" +
" was 'Jim'\n" +
" expected 'Bob'\n" +
" expected {\n" +
" name: 'Bob'\n" +
" }"
}
]
});
exports.hasPropertiesBehavesAsIsObjectExceptIgnoresUnexpectedValues = testMatcher({
matcher: duck.hasProperties({
name: "Bob",
age: 24
}),
description: "object with properties {\n age: 24,\n name: 'Bob'\n}",
positives: [{name: "Bob", age: 24}, {name: "Bob", age: 24, hair: "none"}],
negatives: [
{value: {name: "Bob"}, description: "missing property: \"age\""},
{value: {}, description: "missing property: \"age\"\nmissing property: \"name\""},
{value: {name: "bob", age: 24}, description: "value of property \"name\" didn't match:\n was 'bob'\n expected 'Bob'"}
]
});
exports.isArrayMatchesLengthAndIndividualElements = testMatcher({
matcher: duck.isArray(["apple", "banana"]),
description: "['apple', 'banana']",
positives: [["apple", "banana"]],
negatives: [
{value: [], description: "was of length 0"},
{value: ["apple", "banana", "coconut"], description: "was of length 3"},
{value: ["apple", "coconut",], description: "element at index 1 didn't match:\n was 'coconut'\n expected 'banana'"}
]
});
exports.isArraySubDescriptionsAreIndented = testMatcher({
matcher: duck.isArray([duck.isObject({name: "Bob"})]),
description: "[{\n name: 'Bob'\n}]",
positives: [[{name: "Bob"}]],
negatives: [
{
value: [{name: "Jim"}],
description:
"element at index 0 didn't match:\n" +
" value of property \"name\" didn't match:\n" +
" was 'Jim'\n" +
" expected 'Bob'\n" +
" expected {\n" +
" name: 'Bob'\n" +
" }"
}
]
});
exports.isArraySubDescriptionsAreIndented = testMatcher({
matcher: duck.any,
description: "<any>",
positives: [{name: "Bob"}, null, undefined, 0, 1, "Bob", []],
negatives: []
});
exports.assertReturnsNormallyOnSuccess = function(test) {
duck.assertThat(42, duck.is(42));
test.done();
};
exports.assertRaisesErrorIfAssertionFails = function(test) {
try {
duck.assertThat(42, duck.is(43));
test.fail("Should throw error");
} catch (error) {
test.equal(error.name, "AssertionError");
test.equal(error.message, "Expected 43\nbut was 42");
}
test.done();
};