mlunzena: Ich habe dazu bereits einmal Dokumentation geschrieben, allerdings auf englisch. Als Spatz in der Hand bleibt dieser Text zunächst unübersetzt.
Unit Tests JavaScript
Introduction
This wiki page contains a summary of the documentation of QUnit found on:
Citing the introductory words on its homepage:
QUnit is a powerful, easy-to-use, JavaScript test suite. It's used by the jQuery project to test its code and plugins but is capable of testing any generic JavaScript code (and even capable of testing JavaScript code on the server-side).
There is no easy definition of "unit testing" for JavaScript. Does it include having a DOM library? Does it include a specific browser of some vendor?
Each of these three levels could be tested:
We wanted to have midscale and full tests and thus decided to define JavaScript unit tests as "unit tests involving the DOM library in htmlunit".
How to write a simple JavaScript unit test
The general usage of QUnit is explained at http://docs.jquery.com/QUnit#Using_QUnit
The essential structure of a unit test is:
- arrange / given
- action / when
- assert / then
Assertions
There are three assertions provided by QUnit:
ok(state, [message])
test("a test", function() {
ok(true, "always fine");
ok(false, "this assertion fails");
});
This is the most basic assertion function provided by QUnit. It requires a single boolean argument. If this argument resolves to true
, this assertion passes. If it resolves to false
, this assertion fails. You can give an additional string to show a meaningful explanation, if this assertion fails.
equals(actual, expected, [message])
test("a test", function() {
var actual = 1;
equals(actual, 1);
equals(actual - 1, 0, "this message is added to the assertion result, useful for debugging");
});
equals
is equivalent to JUnit's assertEqual
(but the order is different). Use this for comparing non-boolean values, as it outputs both values on fail.
same(actual, expected, [message] )
test("same test", function() {
same(undefined, undefined, "same succeeds");
same("", 0, "same fails");
});
same
is stricter than equals
as comparison is done with ===
. So null
, 0
, the empty string ""
and undefined
are not the same. Moreover it does a deep, recursive comparison instead. This assertion should be used in most cases.
Writing test cases
Assertions are contained in Test cases. Try to keep tests atomic using at most a single assertion per case. This way you will know exactly why a test case fails and you will prevent invalid results from side effects.
Test cases are written by calling the test
method:
test("a test", function() {
ok(true, "always fine");
});
The signature of this method is:
test(name, [expected], test)
name
is obviously the name of the test.
expected
is an optional parameter containing the number of assertions expected to run. This is important for asynchronous tests.
test
is a function literal containing the actual testing code.
Grouping Tests
If you split your test cases keeping them atomic and free of side effects, you will end up with a lot of these. To organize them logically and to be able to run only certain groups of tests, you can employ the module
method to group the test cases.
module("group a");
test("a basic test example", function() {
ok( true, "this test is fine" );
});
test("a basic test example 2", function() {
ok( true, "this test is fine" );
});
module("group b");
test("a basic test example 3", function() {
ok( true, "this test is fine" );
});
test("a basic test example 4", function() {
ok( true, "this test is fine" );
});
After calling module
every test case will end up in that group. If you call module
again, all following test cases are grouped in that module. Additionally module
can be used for setting up fixtures and tearing them down:
module("lifecyle example", {
setup: function() {
this.testData = "foobar";
},
teardown: function() {
delete this.testData;
}
});
test("test with setup and teardown", function() {
same(this.testData, "foobar");
});
So the signature of this method is:
module(name, )
name
is the name of this module.
lifecycle
is optional and contains setup and teardown callbacks which are run before and after every test in this module.
Testing synchronous callbacks
Whenever you have to tests code with synchronous callbacks, there will be passing tests that should actually fail as the assertion is never evaluated.
test("a test", function() {
expect(1);
$("input").myPlugin({
initialized: function() {
ok(true, "plugin initialized");
}
});
});
For these circumstances you may use the method expect(amount)
to assert the number of assertions to get evaluated. expect
does nothing for you if you do not test code using callbacks. Note that the second, optional parameter to test
does exactly the same thing.
Asynchronous Tests
Unfortunately expect
may not help you with asynchronous callbacks which are used by XmlHttpRequests or DOM events. Testing is inherently synchronous and so you have to stop the test runner for a moment until that magic happens and get started afterwards.
test("a test", function() {
stop(500);
$.getJSON("/someurl", function(result) {
equals(result.value, "someExpectedValue");
start();
});
});
As you see there are two additional methods stop()
and start()
which allow you to test asynchronous callbacks. Just call stop
before asynchronous operations and start
after all assertions in order to ask the test runner to move onward. Obviously you risk, that start
may get never be called. To mitigate that fact you may provide a number of milliseconds as an timeout to stop
to make sure that the test runner continues after at most this given amount of time.
test("a test", function() {
stop(500);
$.getJSON("/someurl", function(result) {
equals(result.value, "someExpectedValue");
start();
});
});
Try to avoid that parameter to prevent hunting for false negatives.