summaryrefslogtreecommitdiffstats
path: root/gin/test/expect.js
diff options
context:
space:
mode:
authorabarth@chromium.org <abarth@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-11-25 21:26:15 +0000
committerabarth@chromium.org <abarth@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-11-25 21:26:15 +0000
commit2f70342531d0cb7335ea88ec5579d0a8a8bb114f (patch)
treebf1883a26ccf8493b5419fb239190619c249481b /gin/test/expect.js
parentae6f06153630c4c0940a4630443ae4faf44ef409 (diff)
downloadchromium_src-2f70342531d0cb7335ea88ec5579d0a8a8bb114f.zip
chromium_src-2f70342531d0cb7335ea88ec5579d0a8a8bb114f.tar.gz
chromium_src-2f70342531d0cb7335ea88ec5579d0a8a8bb114f.tar.bz2
[Gin] Add a basic unit testing framework
Previously, we were using JavaScript bindings to gtest to unit test JavaScript code in Gin and Mojo. The gtest bindings were very basic and not very idiomatic. This CL introduces a simple AMD module call "expect" to help us write more idiomatic unit tests. The API for "expect" is based on the popular Jasmine unit testing framework for node.js. I investigated just importing one of Node's many unit testing frameworks, but they all try to do too much (e.g., drive the entire test harness via Node's file system interface). The "expect" modules doesn't try to drive the testing process. We just let gtest handle that. Instead, "expect" just provides a simple language in which to write test assertions. We'll likely evolve our testing strategy over time, but hopefully this CL is an improvement over the primitive gtest bindings. R=jochen@chromium.org TBR=joth@chromium.org BUG=none Review URL: https://codereview.chromium.org/85223002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@237145 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'gin/test/expect.js')
-rw-r--r--gin/test/expect.js289
1 files changed, 289 insertions, 0 deletions
diff --git a/gin/test/expect.js b/gin/test/expect.js
new file mode 100644
index 0000000..4154456
--- /dev/null
+++ b/gin/test/expect.js
@@ -0,0 +1,289 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+define(["console"], function(console) {
+ // Equality function based on isEqual in
+ // Underscore.js 1.5.2
+ // http://underscorejs.org
+ // (c) 2009-2013 Jeremy Ashkenas,
+ // DocumentCloud,
+ // and Investigative Reporters & Editors
+ // Underscore may be freely distributed under the MIT license.
+ //
+ function has(obj, key) {
+ return obj.hasOwnProperty(key);
+ }
+ function isFunction(obj) {
+ return typeof obj === 'function';
+ }
+ function isArrayBufferClass(className) {
+ return className == '[object ArrayBuffer]' ||
+ className.match(/\[object \w+\d+(Clamped)?Array\]/);
+ }
+ // Internal recursive comparison function for `isEqual`.
+ function eq(a, b, aStack, bStack) {
+ // Identical objects are equal. `0 === -0`, but they aren't identical.
+ // See the Harmony `egal` proposal:
+ // http://wiki.ecmascript.org/doku.php?id=harmony:egal.
+ if (a === b)
+ return a !== 0 || 1 / a == 1 / b;
+ // A strict comparison is necessary because `null == undefined`.
+ if (a == null || b == null)
+ return a === b;
+ // Compare `[[Class]]` names.
+ var className = toString.call(a);
+ if (className != toString.call(b))
+ return false;
+ switch (className) {
+ // Strings, numbers, dates, and booleans are compared by value.
+ case '[object String]':
+ // Primitives and their corresponding object wrappers are equivalent;
+ // thus, `"5"` is equivalent to `new String("5")`.
+ return a == String(b);
+ case '[object Number]':
+ // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is
+ // performed for other numeric values.
+ return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
+ case '[object Date]':
+ case '[object Boolean]':
+ // Coerce dates and booleans to numeric primitive values. Dates are
+ // compared by their millisecond representations. Note that invalid
+ // dates with millisecond representations of `NaN` are not equivalent.
+ return +a == +b;
+ // RegExps are compared by their source patterns and flags.
+ case '[object RegExp]':
+ return a.source == b.source &&
+ a.global == b.global &&
+ a.multiline == b.multiline &&
+ a.ignoreCase == b.ignoreCase;
+ }
+ if (typeof a != 'object' || typeof b != 'object')
+ return false;
+ // Assume equality for cyclic structures. The algorithm for detecting
+ // cyclic structures is adapted from ES 5.1 section 15.12.3, abstract
+ // operation `JO`.
+ var length = aStack.length;
+ while (length--) {
+ // Linear search. Performance is inversely proportional to the number of
+ // unique nested structures.
+ if (aStack[length] == a)
+ return bStack[length] == b;
+ }
+ // Objects with different constructors are not equivalent, but `Object`s
+ // from different frames are.
+ var aCtor = a.constructor, bCtor = b.constructor;
+ if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) &&
+ isFunction(bCtor) && (bCtor instanceof bCtor))
+ && ('constructor' in a && 'constructor' in b)) {
+ return false;
+ }
+ // Add the first object to the stack of traversed objects.
+ aStack.push(a);
+ bStack.push(b);
+ var size = 0, result = true;
+ // Recursively compare objects and arrays.
+ if (className == '[object Array]' || isArrayBufferClass(className)) {
+ // Compare array lengths to determine if a deep comparison is necessary.
+ size = a.length;
+ result = size == b.length;
+ if (result) {
+ // Deep compare the contents, ignoring non-numeric properties.
+ while (size--) {
+ if (!(result = eq(a[size], b[size], aStack, bStack)))
+ break;
+ }
+ }
+ } else {
+ // Deep compare objects.
+ for (var key in a) {
+ if (has(a, key)) {
+ // Count the expected number of properties.
+ size++;
+ // Deep compare each member.
+ if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack)))
+ break;
+ }
+ }
+ // Ensure that both objects contain the same number of properties.
+ if (result) {
+ for (key in b) {
+ if (has(b, key) && !(size--))
+ break;
+ }
+ result = !size;
+ }
+ }
+ // Remove the first object from the stack of traversed objects.
+ aStack.pop();
+ bStack.pop();
+ return result;
+ };
+
+ function describe(subjects) {
+ var descriptions = [];
+ Object.getOwnPropertyNames(subjects).forEach(function(name) {
+ if (name === "Description")
+ descriptions.push(subjects[name]);
+ else
+ descriptions.push(name + ": " + JSON.stringify(subjects[name]));
+ });
+ return descriptions.join(" ");
+ }
+
+ var predicates = {};
+
+ predicates.toBe = function(actual, expected) {
+ return {
+ "result": actual === expected,
+ "message": describe({
+ "Actual": actual,
+ "Expected": expected,
+ }),
+ };
+ };
+
+ predicates.toEqual = function(actual, expected) {
+ return {
+ "result": eq(actual, expected, [], []),
+ "message": describe({
+ "Actual": actual,
+ "Expected": expected,
+ }),
+ };
+ };
+
+ predicates.toBeDefined = function(actual) {
+ return {
+ "result": typeof actual !== "undefined",
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected a defined value",
+ }),
+ };
+ };
+
+ predicates.toBeUndefined = function(actual) {
+ // Recall: undefined is just a global variable. :)
+ return {
+ "result": typeof actual === "undefined",
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected an undefined value",
+ }),
+ };
+ };
+
+ predicates.toBeNull = function(actual) {
+ // Recall: typeof null === "object".
+ return {
+ "result": actual === null,
+ "message": describe({
+ "Actual": actual,
+ "Expected": null,
+ }),
+ };
+ };
+
+ predicates.toBeTruthy = function(actual) {
+ return {
+ "result": !!actual,
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected a truthy value",
+ }),
+ };
+ };
+
+ predicates.toBeFalsy = function(actual) {
+ return {
+ "result": !!!actual,
+ "message": describe({
+ "Actual": actual,
+ "Description": "Expected a falsy value",
+ }),
+ };
+ };
+
+ predicates.toContain = function(actual, element) {
+ return {
+ "result": (function () {
+ for (var i = 0; i < actual.length; ++i) {
+ if (eq(actual[i], element, [], []))
+ return true;
+ }
+ return false;
+ })(),
+ "message": describe({
+ "Actual": actual,
+ "Element": element,
+ }),
+ };
+ };
+
+ predicates.toBeLessThan = function(actual, reference) {
+ return {
+ "result": actual < reference,
+ "message": describe({
+ "Actual": actual,
+ "Reference": reference,
+ }),
+ };
+ };
+
+ predicates.toBeGreaterThan = function(actual, reference) {
+ return {
+ "result": actual > reference,
+ "message": describe({
+ "Actual": actual,
+ "Reference": reference,
+ }),
+ };
+ };
+
+ predicates.toThrow = function(actual) {
+ return {
+ "result": (function () {
+ if (!isFunction(actual))
+ throw new TypeError;
+ try {
+ actual();
+ } catch (ex) {
+ return true;
+ }
+ return false;
+ })(),
+ "message": "Expected function to throw",
+ };
+ }
+
+ function negate(predicate) {
+ return function() {
+ var outcome = predicate.apply(null, arguments);
+ outcome.result = !outcome.result;
+ return outcome;
+ }
+ }
+
+ function check(predicate) {
+ return function() {
+ var outcome = predicate.apply(null, arguments);
+ if (outcome.result)
+ return;
+ throw outcome.message;
+ };
+ }
+
+ function Condition(actual) {
+ this.not = {};
+ Object.getOwnPropertyNames(predicates).forEach(function(name) {
+ var bound = predicates[name].bind(null, actual);
+ this[name] = check(bound);
+ this.not[name] = check(negate(bound));
+ }, this);
+ }
+
+ return function(actual) {
+ return new Condition(actual);
+ };
+});