-
Notifications
You must be signed in to change notification settings - Fork 7
Home
Tyrtle is a Javascript unit testing framework that emphasises simplicity, cleanliness and expressiveness.
It runs either in the browser or on NodeJS. In a CommonJS environment (eg: Node, or the browser running RequireJS), nothing is added to the global space, and with plain Javascript in the browser, only one variable is added to the window object. This keeps it clean and ensures that it won't interfere with your codebase.
- Setup
- Browsers
- Node.JS
- assert('hello')('world')('!')
- Writing assertions
- Custom assertions
- Global vs Per-Module assertions
- Re-using assertions
- Custom assertions
- Test Helpers
- Asynchronous tests
- Asynchronous helpers
To use Tyrtle, you should include the Tyrtle.js
file as well as your choice of renderers. Currently, Tyrtle only ships with one HTML renderer (surprisingly called html
), but it is very simple to
write your own renderer. The packaged html renderer requires jQuery to be present on the page, and it has a CSS file to make the results page a bit prettier.
<link href="path/to/tyrtle/tyrtle.css" rel="stylesheet" />
<script src="jquery.js"></script>
<script src="path/to/tyrtle/Tyrtle.js"></script>
<script src="path/to/tyrtle/renderers/html.js"></script>
To run Tyrtle on Node, you must first install its dependencies. Thankfully, this is very simple using NPM. Simply run
$ npm install
...and that's it!
Time to start writing some tests. The first thing you should do is create an instance of Tyrtle.
var tyrtle = new Tyrtle();
Tyrtle requires that all tests are grouped into modules. A module consists of any number of tests, along with helpers (more on that later). These are defined in the body of the module, which is a function.
tyrtle.module("My first module", function () {
// this is the module body
});
Inside this body function, this
is an object that allows you to define tests and helpers. Creating a test is simply done by calling this.test()
.
tyrtle.module("My first module", function () {
this.test("My first test", function (assert) {
// this is the test body
});
});
You'll see that the test's body function receives a parameter, here it is called
assert
however you can call it whatever you like. Ifassert
is too much typing, you could even just call itA
. For this documentation, it will always be referred to asassert
.
The simplest assertion you can write in Tyrtle is the one most often used in unit testing -- checking that two values are the same -- so let's write one of those.
myTests.module("My first module", function () {
this.test("My first test", function (assert) {
assert.that(2 + 2).is(4).since("Two plus two should equal four");
});
});
The last thing that you have to do now is simple... just run the tests!
myTests.run();
Putting it all together, here's all the code you need:
<html>
<head>
<link rel="stylesheet" href="../tyrtle.css" type="text/css" />
</head>
<body>
<script src="jquery.js"></script>
<script src="Tyrtle.js"></script>
<script src="renderers/html.js"></script>
<script type="text/javascript">
var tyrtle = new Tyrtle();
tyrtle.module("My first module", function () {
this.test("My first test", function (assert) {
assert.that(2 + 2).is(4).since("Two plus two should equal four");
});
});
tyrtle.run();
</script>
</body>
</html>
For Node, Tyrtle comes with a test harness which runs on the command line. Read more about that on the test harness page. If you want to write your own harness, or your tests are basic enough to not warrant a test harness, this is possible too. Read on for details.
In Node, you'll just need to set the renderer too.
var Tyrtle = require('Tyrtle'),
renderer = require('Tyrtle/renderers/node'),
tyrtle = new Tyrtle()
;
Tyrtle.setRenderer(renderer);
tyrtle.module("My first module", function () {
this.test("My first test", function (assert) {
assert.that(2 + 2).is(4).since("Two plus two should equal four");
});
});
tyrtle.run();
Then, simply execute that file to see the results:
$ node my-file.js
As mentioned, Tyrtle is designed for expressiveness. Tests are very readable, and it's easy to remember their format.
var x = 3, y = 4;
assert.that(x).is.not(y).since("x and y should be different.");
This style was created to keep them as easy to read as possible, as well as promote consistency. The subject of the assertion is always first, followed by your expectation and then the reason why.
The basic format of assertions goes like this:
assert.that(/*subject*/).is/*.comparison*/(/*expected value*/).since(/*assertion message*/);
Though this makes assertions very easy to understand and write, this style of "prose programming" isn't everyone's cup of tea, so it's good to know that there's a healthy dose of syntactic sugar available. The properties that
, is
and since
are all completely optional which means that each of the following statements are equivalent to the above assertion.
assert.that(x).is.not(y)("x and y should be different");
assert(x).is.not(y)("x and y should be different");
assert.that(x).not(y).since("x and y should be different");
assert(x).not(y)("x and y should be different");
Also, since the most common assertion is an equality check (.is()
), it is the default, so it's even shorter to write:
assert.that(x).is(3).since("x should be 3");
// is the same as this:
assert(x)(3)("x should be 3");
Though it's highly recommended to add helpful assertion messages, they are optional, and as mentioned before, you can assign the assert
object to any name, so if you wanted to go completely crazy, the assertion could even be this if you like:
a(x)(3)();
It's up to you to decide how terse or expressive you want to be with your tests.
You can also read the full list of built-in assertions.
If the built-in assertions don't exactly meet your requirements, don't worry! It's very easy to add your own, either per-module or globally.
Let's say you're writing tests for a "Person" class you're developing. Ordinarily, you'd probably have to write something like this:
// check that the person is voting age
assert.that(myPerson.age >= 18).is.ok().since("Expected person to be an adult");
assert.that(myPerson.friends.indexOf(otherPerson)).is.not(-1).since("They should be friends");
Though these assertions will accurately test your code, it is not immediately apparent what is being tested, and if a test fails, the error message won't be that helpful. Look at the second assertion there for an example: "Failed: Actual value was the same as the unexpected value -1
: They should be friends". Whaaa?
Instead, it's much nicer to abstract this logic away and keep the assertions clean. What is the subject of your assertion? In both these cases, the subject is myPerson
, not the index of some array, or the age of the person itself.
To add assertions, use this.addAssertions
in a module.
tyrtle.module("Person class tests", function () {
this.addAssertions({
adult : function (subject) {
return subject.age >= 18;
},
friendsWith : function (subject, otherPerson) {
return subject.friends.indexOf(otherPerson) !== -1;
}
});
this.test("my test", function (assert) {
assert.that(myPerson).is.adult().since("Expected person to be an adult.");
assert.that(myPerson).is.friendsWith(otherPerson).since("...");
});
});
Isn't that way easier to read now? If the business logic behind your assertions change, you now have only one single place to change it, too.
Custom assertions consist of a single function which always receives the subject as its first argument, followed by any other arguments passed to the assertion itself. To indicate success the function should return true
or nothing at all. To indicate failure, you can simply return false
, but there will be no default assertion message. To add a message, return a string
and it will be prepended to the since
message.
{
adult : function (subject) {
return subject.age >= 18 || "Subject is not yet an adult";
}
}
If our previous test would fail, it would now yield this error message: Failed: Subject is not yet an adult: Expected person to be an adult
. Better, but what about telling us a little more information? Variable interpolation is supported with numbered syntax: {0} {1} {2}
, etc, with each number corresponding to the arguments of the assertion. If you want to interpolate some different variables, then you can return an array. The first element should be the message, and all further elements are added to the interpolation list.
{
adult : function (subject) {
return subject.age >= 18
|| ["{1} is not yet an adult (he/she is only {2})", subject.name, subject.age]
;
}
}
This yields Failed: Alice is not yet an adult (he/she is only 16)
.
It is worth noting as well that the variable interpolation is done by the current renderer. This means that it can be formatted in special ways as suits the environment where you'll be reading the message. For example, if an
object
is added by the HTML renderer, then it adds a link to the report which you can click on to inspect the full element in your javascript console. The Node renderer adds colour to highlight different data types.
Note that custom assertions can override the built-in methods, such as
.not()
,.ok()
, etc. Though this can be dangerous if misused, it gives you the power to make Tyrtle behave as you want it to.
If you want to leverage the built-in assertions or other custom assertions from inside a custom assertion, this can be done by using this
. Inside an assertion function, this
is the assert
object. For example, if you want to write an assertion which will check that a variable is a string and is longer than 5 characters, you could use the built-in assertion ofType
like so:
{
longEnough : function (subject) {
// remember, this === assert
this(subject).is.ofType('string')("Supplied variable is not a string");
return subject.length > 5 || ["String is not long enough, only {1} characters long", subject.length];
}
}
When you are adding custom assertions, you can either add them globally (meaning that your assertion will be available in every test), or per-module (the assertions only exist for the module they were defined in). If you have assertions which are quite specific to the class you are testing in a module, then it would make sense to only add it there, whereas assertions which apply to many separate parts of your tests may warrant a global assertion to be added.
To add global assertions, use Tyrtle.addAssertions
:
Tyrtle.addAssertions({
'assertionName' : function (subject) {
// your check here
},
'anotherAssertion' : ...
});
To add per-module assertions, use this.addAssertions
inside a module:
tyrtle.module("My Module", function () {
this.addAssertions({
'assertionName' : ...
});
});
Astute readers would have noticed that the pattern used for executing assertions is a chain of function calls. By breaking the chain created in a normal execution, you can re-use code in repetitive sections of your tests.
var x = "woo hoo",
a = assert(x)
;
a.ok()("X should be truthy");
a.endsWith("hoo")("X should end with 'hoo'");
Remember that for non-object variables, the value of the variable is stored, meaning that the following code would fail:
var x = "woo hoo",
a = assert(x)
;
x = "blah";
a.is('blah')(); // fail, the subject is still 'woo hoo'
Conversely, for objects, this can work to your advantage very nicely:
var obj1 = { foo : 'A' },
obj2 = { foo : 'A' },
a = assert(obj1).equals(obj2) // set up the assertion, but don't execute it yet.
;
a(); // execute the assertion. this would pass.
obj2.bar = 'B';
a(); // execute the same assertion, but this one will fail.
Tyrtle allows you to define test helpers for your modules. These are functions which run before or after the tests, and in other testing frameworks they are sometimes called setUp
and tearDown
, however in Tyrtle, they're named:
-
beforeAll
: run once before any of the tests in that module are executed -
afterAll
: run once after all the tests in that module have completed -
before
: run before each test is executed -
after
: run after each test has completed
Using them is simple:
myTests.module("Tests with helpers", function () {
var counter,
someObject
;
this.beforeAll(function () {
someObject = new SomeClass();
});
this.afterAll(function () {
someObject.destroy(); // perform whatever cleanup you need to here
});
this.before(function () {
counter = 0;
});
this.after(function () {
// other cleanup that you might need between tests
});
this.test(/* as before */);
});
Each module may only define each helper once, and it does not matter where it is actually defined in the body function: they will apply to all tests regardless of whether it was defined before or after a test.
You will also see here one of the benefits of defining modules in a function. You are able to create variables in a closure (counter
and someObject
in the example above), which are accessible to all your tests and helpers, but are not visible from outside, and don't pollute the global namespace.
Sometimes you will need to write tests which perform some asynchronous function, such as an AJAX request or a call to setTimeout
. In these cases, the syntax for writing tests changes slightly. Firstly, tests require two functions when you define them.
- The first is the body function where the asynchronous calls should be executed
- The second is the assertion function where assertions can be performed
It is important to note that when you are writing asynchronous tests, assertions can not be made in the body function. Instead of receiving the assert
object like in regular, synchronous tests, the body function is passed a callback which should be executed when all asynchronous methods have completed. The callback can be passed an object which will then be used as the context for the assertion function, meaning that you can access its properties via this
.
this.test("My first asynchronous test", function (callback) {
/** body function **/
// wait 1 second and then call the callback
setTimeout(function () {
callback({
x : 3,
y : [1, 2, 3]
});
}, 1000);
}, function (assert) {
/** assertion function **/
assert.that(this.x).is(3)("x should be three");
assert.that(this.y[1]).is(2)("The second value of y should be 2");
});
If your helper functions require some asynchronous code, Tyrtle can handle that too. All you need to do is define an argument in the helper's body function. That argument will be a function which is to be called once the helper is complete.
this.module("Module with asynchronous helpers", function () {
var testData;
this.beforeAll(function (callback) {
// this helper has an argument, therefore it is asynchronous
$.get("test-data.php", function (data) {
testData = data;
callback();
});
});
this.afterAll(function () {
// this helper has no arguments, therefore it is synchronous
testData = null;
});
});
When writing any asynchronous tests or helpers, it's important to ensure that the callback function will always get executed eventually, otherwise Tyrtle will wait indefinitely for it to report that it is complete.