Alright is a library for asserting particular properties about data in JavaScript. The common use for this kind of thing is automated testing (TDD, BDD, etc), and Alright can be used with most popular testing frameworks with pluggable assertions, such as Mocha.
|
|
This document will guide you through the basics of Alright. This is a quick introduction, but once you finish this page you’ll know all you need to start writing assertions for your code-base.
Alright is a library that provides ways of asserting properties about pieces of data in JavaScript without verbosity, and in a way that it can be easily extended to support specific type of assertions. It also provides Sweet.js macros for writing concise assertions.
For example:
2 + 2 => 4
[1, 2, 3] should have('foo')
In this tutorial we’re going to use Alright together with Node.js, Mocha, and Sweet.js to write some tests. The process is pretty similar to all other platforms, and we show how to make things work in the browser at the end.
So, first things first, once you’ve got Node.js installed, you’ll want to start a new project and run the following commands:
$ mkdir test-project
$ cd test-project
$ npm install sweet.js alright mocha
With all in place, we’re ready to create our first test script. Make a file test/array.sjs with the following contents:
// The only requirement is that you have `alright` in your
// scope for the macros to work.
var alright = require('alright')
describe('Array#indexOf()', function() {
it('Should return the index of the item.', function() {
[1, 2, 3].indexOf(2) => 1
})
it('Should return -1 if the item is not present.', function() {
[1, 2, 3].indexOf(4) => -1
})
})
As you see, assertions are written in using the form: expression => expected, where expression is any computation for the value you want to test, and expected is the value you expect to get as result. The values are compared structurally, which means that [1, 2, 3] => [1, 2, 3] will succeed, for example. You can use expression => not expected to invert the expectation, so 2 => not 1 will succeed.
The keen JavaScripter might have noticed that we’re not quite writing JavaScript here, so first we need to turn this script into something a JS engine (in this case, Node’s v8) will understand. This is where the sjs compiler we’ve installed (through sweet.js) comes in. It takes a .sjs file, some macro definitions (which do tell the compiler how the magic should be done), and gives us JavaScript as we know it:
$ $(npm bin)/sjs --module alright/macros -o test/array.js test/array.sjs
Note
The $(npm bin) command allows one to get the path where local binaries are installed with npm, so you can use it to keep all of your dependencies localised instead of installing them globally.
Finally, we just need to run the test through Mocha:
$ $(npm bin)/mocha --reporter spec
Should all go well, you’ll see the following output:
Note
If you’re not using any build system, you can get npm to help you with automating this compiling/testing phase. Just put whatever you’d write in the command line inside of your package.json‘s script section. You can even leave out the $(npm bin) part, since npm searches inside that folder automagically:
{
( ... )
"scripts": {
"test-cc": "sjs -m alright/macros -o test/array.js test/array.sjs",
"test": "npm run test-cc && mocha --reporter spec"
},
( ... )
}
The most straight forward way of making assertions is the structural equality assertion (=>), but sometimes you’re interested in other properties as well. There are so many possibilities of properties you can come up with that it doesn’t make sense to come up with a special symbol for every one of them. So, instead, Alright allows you to provide a validation function.
Many validations are built right into Alright itself. In fact, the structural equality assertion is just a special case of this:
[1, 2] => [1, 2]
// Is the same as:
[1, 2] should alright.equal([1, 2])
Since they’re just regular functions, you can always write your own:
function beGreater(expected){ return function(actual) {
var divergence = alright.divergence.invertibleDivergence(
'{:actual} to be greater than {:expected}'
, '{:actual} to not be greater than {:expected}'
)
return alright.assert( actual > expected
, divergence.make({ actual : actual
, expected: expected }))
}}
3 should beGreater(2)
The same concepts explained here apply to all other JavaScript platforms, but if you’re not using a platform that has direct support for Node modules, you’ll want to use the UMD bundle (the single file that can be used in any platform, and any module system!). The easiest way of doing so is downloading the latest release and loading the alright.umd.js in your platform:
Common JS:
var alright = require('alright') ( ... )
AMD:
require(['alright'], function(alright) { ( ... ) })
Browser without a module system:
<script src="/path/to/alright.umd.js"></script> <script> /* alright is in the global scope here */ ( ... ) </script>
If you don’t want to use Sweet.js macros, you can use alright’s testing functions directly. Just use the verify function to throw an exception if an expectation isn’t met:
var _ = alright
_.verify( _.equals(1)(1) ) // same as: 1 => 1
_.verify( _.not(_.equals(1)(1)) ) // same as: 1 => not 1
Now that you get the idea behind Alright, you can start writing your assertions for testing your JavaScript code. Be sure to check out the Discover Alright documentation to learn everything you can get from the library.
This document will guide you through the concepts behind Alright. After reading this you’ll understand how Alright works, and how you can extend it to meet your needs, by providing new assertions or new ways of making assertions.
How do you make assertions composable and extensible? Well, definitely not by throwing exceptions, since those require the use of special constructs for when you don’t want a failed assertion to terminate the process. Instead, Alright uses two core concepts: Validations and Divergences. They’re derived from the idea that when you make an assertion about the property of an object, you have two different outcomes. Either the assertion succeeds, which is great, or it fails, in which case you want to provide a detailed description of the failure.
The Validation data structure captures the idea of an assertion either succeeding or failing, whereas the Divergence data structure captures the idea of providing a detailed description of the assertion.
By using these data structures instead of directly using Exceptions and throwing errors, we’re not only able to compose assertions and provide support for things like asynchronous assertions in a straight-forward manner, but we can do this in a fairly high-level and easily extensible way.
This also means that assertion functions do only one job. They tell you whether a property holds or not, and if not, tell you why it doesn’t hold. For example:
// :: Number → Number → Validation[Divergence, Divergence]
function greaterThan(a){ return function(b) {
var message = invertibleDivergence( '{:b} to be greater than {:a}'
, '{:b} to not be greater than {:a}'
).make({ a: a, b: b })
return b > a? Success(divergence)
: /* otherwise */ Failure(divergence)
}}
greaterThan(2)(3)
// => Failure(Divergence("2 to be greater than 3"))
greaterThan(3)(2)
// => Success(Divergence("2 to be greater than 3"))
Another function can then determine what to do with the successful or failed assertion, so it’s possible to support asynchronous and synchronous assertions in a fairly straight-forward manner, and even combine different assertions into a single one.
A Divergence is a structure that provides a description of an assertion. And it’s present to let the computations deal with such description at a high level, which is something you can’t do by using plain strings.
In a nutshell, a Divergence is any immutable object that implements the following interface:
type Divergence where
data :: { String → Any }
toString :: Void → String
inverse :: Void → Divergence (partial, throws)
data is a property that contains the values that participated in the assertion. By convention, an actual property stores the value being tested in the assertion, and an expected property stores the expected outcome of the test. Storing these values allows reporters (a testing framework, for example) to provide things like diffs when presenting a failed assertion to the user.
The usage of the toString() method is pretty straight-forward: it should give you a plain-text description of the assertion. For example, if the original assertion was 2 > 3, a Divergence.toString() for this assertion would return "2 to be greater than 3".
Lastly, the inverse() method returns a new Divergence object, with the same data, but that describes the negative version of the assertion. So, for an assertion like 2 > 3, inverting it would give you the assertion !(2 > 3).
Alright considers anything that fulfils the aforementioned Divergence interface to be a valid Divergence, the only other requirement is that you should treat your object as immutable. While you could easily write your own objects using object literals, Alright provides the functions divergence and invertibleDivergence to construct objects fulfilling this interface for you.
divergence is a function that takes in a template string in the format used by spice, and gives you a Divergence object that doesn’t have an inverse. invertibleDivergence takes two template strings and gives you a Divergence that has an inverse:
var divergences = require('alright').divergence
var d1 = divergences.divergence('{:a} to be greater than {:b}')
var d2 = divergences.invertibleDivergence('{:a} to be greater than {:b}')
To construct a specific Divergence for an assertion, you’d use the make method to provide the values that were part of the assertion:
var a = d1.make({ a: 1, b: 2 })
var b = d2.make({ a: 3, b: 5 })
Finally, whenever you invoke the toString() method, the template variables will be substituted by the provided values:
a.toString()
// => '1 to be greater than 2'
b.toString()
// => '3 to be greater than 5'
A Validation is data structure that can model two different cases: success and failure. Alright uses it for defining the result of each validation function. While any value fulfilling the interface below can be used, the suggested implementation to use is the Data.Validation module.
type Validation[α, β] <: Applicative[β], Functor[β] where
-- | Creates a validation containing successful value β
of :: β → Validation[α, β]
-- | Applies the successful function to an applicative,
-- but aggregates failures with a Semigroup.
ap :: (@Validation[α, β → γ], f:Applicative[_]) => [β] → f[γ]
-- | Transforms a successful value.
map :: (@Validation[α, β]) => (β → γ) → Validation[α, γ]
-- | Applies one function to each side of the validation.
fold :: (@Validation[α, β]) => (α → γ), (β → γ) → γ
-- | Swaps the validation values.
swap :: (@Validation[α, β]) => Void → Validation[β, α]
-- | Transforms both sides of the validation.
bimap :: (@Validation[α, β]) => (α → γ), (β → δ) → Validation[γ, δ]
For more information on the Validation structure, you can read the A Monad In Practicality: First-Class Failures blog post.
An assertion in Alright is just a function from values to Validation[Divergence, Divergence]. That is, it determines whether a particular set of values is valid or not, according to that property. At the lowest level, there’s the built-in assert function, which takes a Boolean value and a Divergence explaining the property being asserted, then returns the Validation describing whether the assertion was successful or not.
As such, the easiest way of writing your own custom assertions is to use the assert function, which is, in fact, how all built-in assertions are written. For example, if one was to write an assertion for values between a specific range:
var assert = require('alright').assert
var divergence = require('alright').divergence.invertibleDivergence
// :: Number → Number → Number → Validation[Divergence, Divergence]
function between(min){ return function(max){ return function(a) {
return assert( a > min && a < max
, divergence( '{:a} to be between {:min} and {:max}'
, '{:a} to not be between {:min} and {:max}'
).make({ a: a, min: min, max: max }))
}}}
between(2)(5)(3)
// => Success(Divergence("3 to be between 2 and 5"))
Note that since these assertions will be partially applied, it’s necessary to curry them. An easy way of writing a curried function would be to use the Core.Lambda module:
var curry = require('core.lambda').curry
// :: Number → Number → Number → Validation[Divergence, Divergence]
between = curry(3, between)
function between(min, max, a) {
return assert( a > min && a < max
, divergence( '{:a} to be between {:min} and {:max}'
, '{:a} to not be between {:min} and {:max}'
).make({ a: a, min: min, max: max }))
}
between(2, 5)(3)
// => Success(Divergence("3 to be between 2 and 5"))
If one wants to check for the inverse of this property, that is, if something is not between a certain range, it’s not necessary to write a new assertion. Given the role of Validation``s and ``Divergence``s in Alright, inverting some assertion is rather straight forward, and is provided by the built-in ``not function, although you could easily implement it yourself:
// :: Validation[Divergence, Divergence] → Validation[Divergence, Divergence]
function not(validation) {
return validation.swap().bimap(invert, invert)
function invert(divergence){ return divergence.inverse() }
}
not(between(2, 5)(3))
// => Failure(Divergence("3 to not be between 2 and 5"))
Up until now there have been no effects in any of the assertions we’ve made. While this did allow us to easily compose and abstract over these computations to provide a simple basis for making assertions, they’re not as useful for testing. This is where verification comes in.
By separating the assertions from their verification, Alright allows different verification strategies to be easily built on top of the existing assertions, without having to change anything. This way Alright supports synchronous assertions for testing frameworks that expect errors to be thrown, testing frameworks that expect specific functions to be called, or even asynchronous assertions using promises or any other concept.
Alright ships out of the box with support for synchronous assertions by throwing errors when expectations aren’t met, and asynchronous assertions for Promises/A+, Fantasy-Land monads, and monadic futures.
The verify function is used for synchronous assertions, and should work with any testing library that expects exceptions to be thrown to invalidate the test:
describe('Equality', function() {
it('Should fail', function() {
alright.verify(3, _.equals(2))
// => AssertionError('Expected 3 to structurally equal 2')
})
})
The verifyPromise function is used for asynchronous assertions, when the testing library expects Promises/A+ values to be returned from the testing function. Mocha and other libraries/frameworks support this:
describe('Equality', function() {
it('Should fail', function() {
return alright.verifyPromise(Promise.of(3), _.equals(2))
// => Promise(AssertionError('Expected 3 to structurally equal 2'))
})
})
Likewise, the verifyMonad and verifyFuture functions are used for asynchronous assertions when the testing library expects Monads or Futures to be returned from the testing function. These will be supported in the next version of the Hi-Five testing library.
Alright runs on all ECMAScript 5-compliant platforms without problems. It’s been successfully tested in the following platforms:
For legacy, ES3 platforms, like IE’s JScript, you’ll have to provide user-land implementations of the ES5 methods. You can do so by just including the es5-shim library.
Alright uses the Github tracker for tracking bugs and new features.
MIT/X11.