diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..01506a4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,19 @@ +{ + "env": { + "node": true + }, + "rules": { + "strict": true, + "quotes": false, + "no-use-before-define": "func", + "no-unused-vars": [2, "all"], + "no-mixed-requires": [1, true], + "max-depth": [1, 5], + "max-len": [1, 80, 4], + "eqeqeq": false, + "no-path-concat": false, + "no-else-return": true, + "no-eq-null": true, + "no-lonely-if": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8b728b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +out/ +checkers.js +checkers.js.map +.repl/ +target/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..211ca16 --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +node_modules/ +out/ +checkers.js.map +.repl/ +target/ diff --git a/README.md b/README.md index d17e1a4..8b82fa5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,69 @@ # checkers -ClojureScript's test.check packaged up for JavaScript + +Property-based testing for JavaScript via ClojureScript's [test.check](https://github.com/clojure/test.check). + +test.check is a Clojure property-based testing tool inspired by [QuickCheck](http://www.quviq.com/products/erlang-quickcheck/). The core idea of test.check is that instead of enumerating expected input and output for unit tests, you write properties about your function that should hold true for all inputs. This lets you write concise, powerful tests. + +Checkers brings the power of test.check to plain ol' JavaScript. + +# Install + + npm install checkers --save + +# Usage + +```js +var checkers = require('checkers'); +var gen = checkers.gen; + +// Property is incorrect +checkers.forAll( + [gen.int], + function(i) { + return i * i > i; + } +).check(1000); + +// Property is now correct +checkers.forAll( + [gen.int], + function(i) { + return i * i >= i; + } +).check(1000); + +// Check property with a particular seed +checkers.forAll( + [gen.int], + function(i) { + return i * i >= i; + } +).check(1000, {seed: 1422111938215}); +``` + +## Documentation + +More coming soon! + +## TODO + +* Generator tests +* Generator docs +* Tutorial +* Better examples + +## Development + +See `npm run` or `package.json` for a list of available scripts. + +You will need [leiningen](http://leiningen.org/) in order to build locally. + +## License + +Distributed under the Eclipse Public License. + +checkers is Copyright © 2015 Glen Mailer and contributors. + +[test.check](https://github.com/clojure/test.check/) is Copyright +Rich Hickey, Reid Draper and contributors. + diff --git a/cljs/checkers.cljs b/cljs/checkers.cljs new file mode 100644 index 0000000..8af2c01 --- /dev/null +++ b/cljs/checkers.cljs @@ -0,0 +1,103 @@ +(ns checkers + (:require [cljs.test.check :as tc] + [cljs.test.check.properties :as prop] + [cljs.test.check.generators :as gen] + [goog.object])) + +;; Interop utils +(def format (aget (js/require "util") "format")) + +(defn- obj-seq + "Seq from enumerable keys of a JS Object" + [obj] + (for [k (goog.object/getKeys obj)] + [k (aget obj k)])) + +(defn- arrayify + "Force a generator's output to be a JS array" + [generator] + (fn [& args] (gen/fmap into-array (apply generator args)))) + +(defn- obj-assoc [obj k v] (doto obj (aset (clj->js k) v))) +(defn- into-object [associative] (reduce-kv obj-assoc #js {} associative)) +(defn- objectify + "Force a generator's output to be a JS object" + [generator] + (fn [& args] (gen/fmap into-object (apply generator args)))) + +;; Check API + +(defn format-args [arglist] + (.join (into-array (map #(format "%j" %) arglist)) ",")) + +(defn generate-message + [{:keys [num-tests fail seed] + {:keys [smallest]} :shrunk}] + (format "Failed after %d test(s)\nInput: %s\nShrunk to: %s\nSeed: %s" + num-tests (format-args fail) (format-args smallest) seed)) + +(defn check + "Wrap up quick-check to take options as a JS object and throw on failure" + [property n & [opts]] + (let [opts (apply concat (js->clj opts :keywordize-keys true)) + {:keys [result] :as r} (apply tc/quick-check n property opts)] + (if-not result + (let [ex (js/Error. (generate-message r))] + (aset ex "result" (clj->js r)) + (throw ex))))) + +(aset js/exports "forAll" + (fn [& args] + (let [p (apply prop/for-all* args)] + (aset p "check" #(check p %1 %2)) + p))) + +(aset js/exports "sample" (comp into-array gen/sample)) + +;; Generator API + +(aset js/exports "gen" #js {}) + + +(aset js/exports "gen" "fmap" gen/fmap) +(aset js/exports "gen" "return" gen/return) +(aset js/exports "gen" "bind" gen/bind) + +; Combinators & Helpers +(aset js/exports "gen" "resize" gen/resize) +(aset js/exports "gen" "choose" gen/choose) +(aset js/exports "gen" "oneOf" gen/one-of) +(aset js/exports "gen" "frequency" gen/frequency) +(aset js/exports "gen" "elements" gen/elements) +(aset js/exports "gen" "suchThat" gen/such-that) +(aset js/exports "gen" "notEmpty" gen/not-empty) +(aset js/exports "gen" "noShrink" gen/no-shrink) +(aset js/exports "gen" "shrink2" gen/shrink-2) + +; Data Types +(aset js/exports "gen" "boolean" gen/boolean) +(aset js/exports "gen" "tuple" (arrayify gen/tuple)) + +(aset js/exports "gen" "int" gen/int) +(aset js/exports "gen" "nat" gen/nat) +(aset js/exports "gen" "posInt" gen/pos-int) +(aset js/exports "gen" "negInt" gen/neg-int) +(aset js/exports "gen" "sPosInt" gen/s-pos-int) +(aset js/exports "gen" "sNegInt" gen/s-neg-int) + +(aset js/exports "gen" "array" (arrayify gen/vector)) +(aset js/exports "gen" "shuffle" (arrayify gen/shuffle)) + +(aset js/exports "gen" "obj" (objectify gen/map)) +(def ^:private gen-hash-map (objectify gen/hash-map)) +(aset js/exports "gen" "object" + (fn [obj] (apply gen-hash-map (apply concat (obj-seq obj))))) + +(aset js/exports "gen" "char" gen/char) +(aset js/exports "gen" "charAscii" gen/char-ascii) +(aset js/exports "gen" "charAlphanum" gen/char-alphanumeric) +(aset js/exports "gen" "charAlpha" gen/char-alpha) + +(aset js/exports "gen" "string" gen/string) +(aset js/exports "gen" "stringAscii" gen/string-ascii) +(aset js/exports "gen" "stringAlphanum" gen/string-alphanumeric) diff --git a/cljs/notice.txt b/cljs/notice.txt new file mode 100644 index 0000000..0d66693 --- /dev/null +++ b/cljs/notice.txt @@ -0,0 +1 @@ +// This file was generated by the ClojureScript compiler. diff --git a/package.json b/package.json new file mode 100644 index 0000000..b211924 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "checkers", + "version": "0.9.0", + "description": "Property-based testing for JavaScript via ClojureScript's test.check", + "main": "checkers.js", + "scripts": { + "clean": "lein clean", + "build": "lein cljsbuild once release", + "build-dev": "lein cljsbuild once dev", + "dev": "lein cljsbuild auto dev", + "test": "mocha", + "repl": "./scripts/repl", + "prepublish": "npm run clean && npm run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/glenjamin/checkers.git" + }, + "keywords": [ + "test", + "testing", + "property-based", + "quickcheck" + ], + "author": "Glen Mailer ", + "license": "EPL", + "bugs": { + "url": "https://github.com/glenjamin/checkers/issues" + }, + "homepage": "https://github.com/glenjamin/checkers", + "devDependencies": { + "lodash": "^2.4.1", + "mocha": "^2.1.0", + "source-map-support": "^0.2.9" + } +} diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..3229f68 --- /dev/null +++ b/project.clj @@ -0,0 +1,36 @@ +(defproject checkers "0.9.0-SNAPSHOT" + :description "Property-based testing for JavaScript via ClojureScript's test.check" + :url "https://github.com/glenjamin/checkers" + + :dependencies [[org.clojure/clojure "1.6.0"] + [org.clojure/clojurescript "0.0-2665"] + [org.clojure/test.check "0.7.0"]] + + :plugins [[lein-cljsbuild "1.0.4"]] + + :source-paths ["cljs"] + + :clean-targets ["out" "checkers.js" "checkers.js.map"] + + :cljsbuild { + :builds [{:id "dev" + :source-paths ["cljs"] + :compiler {:output-to "checkers.js" + :output-dir "out/dev" + :preamble ["notice.txt"] + :optimizations :simple + :pretty-print true + :cache-analysis true + :source-map "checkers.js.map" + :language-in :ecmascript5 + :language-out :ecmascript5}} + {:id "release" + :source-paths ["cljs"] + :compiler {:output-to "checkers.js" + :output-dir "out/release" + :preamble ["notice.txt"] + :optimizations :advanced + :pretty-print true + :cache-analysis true + :language-in :ecmascript5 + :language-out :ecmascript5}}]}) diff --git a/scripts/repl b/scripts/repl new file mode 100755 index 0000000..27afc98 --- /dev/null +++ b/scripts/repl @@ -0,0 +1,2 @@ +#!/bin/sh +rlwrap lein trampoline run -m clojure.main scripts/repl.clj diff --git a/scripts/repl.clj b/scripts/repl.clj new file mode 100644 index 0000000..7371120 --- /dev/null +++ b/scripts/repl.clj @@ -0,0 +1,9 @@ +(require + '[cljs.repl :as repl] + '[cljs.repl.node :as node]) + +(repl/repl* (node/repl-env) + {:output-dir "out/repl" + :optimizations :none + :cache-analysis true + :source-map true}) diff --git a/test/basic.test.js b/test/basic.test.js new file mode 100644 index 0000000..ce5cc12 --- /dev/null +++ b/test/basic.test.js @@ -0,0 +1,27 @@ +/*eslint-env mocha */ +var _ = require('lodash'); + +var checkers = require('..'); +var gen = checkers.gen; + +describe("checkers", function() { + it("checks that squaring makes things bigger or the same", function() { + checkers.forAll( + [gen.int], + function(i) { + return i * i >= i; + } + ).check(1000); + }); + it("checks Array.sort is the same as _.sortBy on strings", function() { + checkers.forAll( + [gen.array(gen.int)], + function(arr) { + var lodash = _.sortBy(arr, function(x) { return '' + x; }); + var stdlib = arr.slice(); + stdlib.sort(); + return _.isEqual(stdlib, lodash); + } + ).check(100); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..7080a02 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--require test/source-map-support diff --git a/test/source-map-support.js b/test/source-map-support.js new file mode 100644 index 0000000..ef7457f --- /dev/null +++ b/test/source-map-support.js @@ -0,0 +1 @@ +require('source-map-support').install();