Getting Started | API | Elements | Actions | Validators | Handlers | Configuration Options | Advanced Guides | Troubleshooting | About
Nitrogen has a simple unit testing framework which runs on the server and in the browser, and can be used for testing the functionality of your application, ensuring that objects render properly in the browser, and that actions are wired in the browser appropriately.
This means that currently, you need to be able to run it on a machine with a browser in it, or there is an option to initiate a test from an external browser.
Each individual test is initiated by a call to one of the `?wf_test` macros
and is used to test some simple cause and effect.
There are a few different ways to initiate a test:
?wf_test_auto(TestName, SetupFun, AssertionFun)
:: This is the most basic form of test. The test system will call SetupFun()
, and what that completes, will automatically initiate a postback that will call AssertionFun()
. AssertionFun is expected to return true
. Anything else will result in the test failing.
An example that tests that a value was properly set in the browser might be:
tests() ->
SetupFun = fun() -> wf:set(my_textbox, "Value") end,
AssertionFun = fun() -> wf:q(my_textbox) == "Value" end,
?wf_test_manual(set_textbox, SetupFun, AssertionFun).
If something is broken for some reason, (most common would be a typo in
the name of the element), then `wf:q(my_textbox)` would not return the
expected value, and the test would fail.
?wf_test_manual(TestName, SetupFun, AssertionFun)
:: This is very similar to the ?wf_test_auto
test, except that the postback is not automatically executed. Instead, you need to tell the test how to initiate the postback. The easiest way would be with the #event{}
or #click{}
actions.
An example of this might be to ensure that the expected postback is assigned to a button:
tests() ->
SetupFun = fun() -> wf:wire(my_button, #click{}) end,
AssertionFun = undefined,
?wf_test_manual(button_works, SetupFun, AssertionFun)
And in the `event/1` function that `my_button` posts back to, you would add:
?wf_test_event(button_works)
Note that this is safe to include in your normal posbacks, since if the
test is not running, this call will not do anything.
?wf_test_js(TestName, SetupFun, JS, AssertionFun)
:: This test method allows you to collect arbitrary javascript properties (like if you want to verify that an element on the page has a certain height, or maybe that some global javascript variable has been set to a specific value). The arguments are very similar, except for a few changes.
SetupFun
is exactly the same as in ?wf_test_manual
in that you do not need to execute a postback in it.
JS
is a Javascript string that returns a value (the last call should be actuall have a return
statement, as it's executed inside a javascript function). This value will be passed to the AssertionFun
function.
AssertionFun
is expected to be a function of arity 1, with the only argument being a list containing the return value(s) from JS
.
Here's an example to illustrate the usage. Here we will add a #panel
(which is just an HTML div
), then verify that its width is greater than zero:
SetupFun = fun() -> wf:insert_top(my_wrapper, #panel{id=my_new_panel, text="Hi Mom!"} end,
JS = "return objs('my_wrapper').width()",
AssertFun = fun([Width]) -> is_integer(Width) andalso Width > 0 end,
?wf_test_js(element_added, SetupFun, JS, AssertFun).
There is an optional last argument that can be passed to each of these
macros: A proplist containing options for `delay` and `timeout`
delay
(integer()
) :: The number of milliseconds to wait before executing the JS
term (if using ?wf_test_js
) or to wait before executing the postback (if using ?wf_test_auto
or ?wf_test_manual
). This would be used to allow a test to /process/ before executing (say, an image may need to be downloaded, or some jquery-ui scripts will need to be run, or we need to wait for another postback to happen before getting the data). This gives your script time to run before checking the value.
timeout
(integer()
) :: The number of milliseconds the testing system is willing to wait for the test to complete before timing out. This might be tripped if something is taking much longer than expected, or if javascript crashes and stops processing postbacks, or something else crashes. It will just mean that the ?wf_test_event
macro was ultimately never called (which is done automatically if you're using ?wf_test_auto
or ?wf_test_js
).
There are actually several ways to call each of the `?wf_test` macros:
?wf_test_auto(TestName, SetupFun, AssertionFun)
?wf_test_auto(TestName, SetupFun, AssertionFun, Options)
?wf_test_auto(TestName, {SetupFun, AssertionFun})
?wf_test_auto(TestName, {SetupFun, AssertionFun, Options})
?wf_test_manual(TestName, SetupFun, AssertionFun)
?wf_test_manual(TestName, SetupFun, AssertionFun, Options)
?wf_test_manual(TestName, {SetupFun, AssertionFun})
?wf_test_manual(TestName, {SetupFun, AssertionFun, Options})
?wf_test_js(TestName, SetupFun, JS, AssertionFun)
?wf_test_js(TestName, SetupFun, JS, AssertionFun, Options)
?wf_test_js(TestName, {SetupFun, JS, AssertionFun})
?wf_test_js(TestName, {SetupFun, JS, AssertionFun, Options})
The reasoning for the alternative options of having the second argument be a tuple will be more obvious in the next section.
Additionally, SetupFun
and AssertionFun
can be the atom undefined
. If this is the case, SetupFun
will do no setup (the equivilant of =fun() -> ok end), and
AssertionFunif set to
undefined= will simply return true
(which you may do if all you wish to do is verify that a postback happened, without caring about the contents of the postback or the state of the application at the time of the postback).
A test page allows for a collection of tests to be run, and the results of
each test gets aggregated on a per-page basis.
Starting a page test consists of calling `wf_test:start(TestFuns)`, where
`TestFun` is a function that consists of a series of `?wf_test_X()` calls
along with any additional setup or teardown for the tests.
A simple example would be to call `wf_test:start/1` in your page module's
`main()` function.
main() ->
wf_test:start(fun tests/0),
#template{file="mytemplate.html"}.
...
event(my_test_manual) ->
?wf_test_event(my_test_manual).
tests() ->
?wf_test_auto(my_test, my_test()),
?wf_test_manual(my_other_test, my_manual_test()).
my_test() ->
{
fun() -> wf:set(textbox, "NewVal") end,
fun() -> wf:q(textbox) == "NewVal"
}.
my_test_manual() ->
{
fun() ->
wf:insert_after(textbox, #button{postback=my_test_manual, id=mybutton}),
wf:wire(mybutton, #click{})
end,
undefined %% We just want to verify that the postback worked.
}.
If you have an existing page on which you would like to conduct tests
without cluttering up the module's page, instead building all the tests on
a separate module. You can do this with `wf_test:start_other(ModuleName, TestFun)`.
For example, say we want to test a module called `my_module` by creating a
module called `test_module`:
-module(test_module).
-compile(export_all).
-include_lib("nitrogen_core/include/wf.hrl").
main() -> wf_test:start_other(my_module, fun tests/0).
tests() ->
?wf_test_js(fill_form, fill_form()).
fill_form() ->
{
fun() ->
wf:set(first_name, "Jesse"),
wf:set(last_name, "Gumm"),
wf:wire(button, #click{})
end,
"return objs('confirmation').text()",
fun([Text]) ->
Text == "Thank you for your submission"
end,
[{delay, 500}]
}.
This will load the `my_module` page, starting the tests in the process.
The overarching implementation of the tests for an application is almost
completely done in configurations.
There are two configuration options you want to add to your `app.config`
file: `test_browsers` and `tests`.
test_browsers
is a list of browsers in which to test your app.tests
which is a list of paths to test.{test_browsers, [
"google-chrome",
"firefox"
]},
{tests, [
"/path/to/test",
"/path/to/other_test"
]},
Once this is set up, you'll want to run `wf_test:start_all(AppName)` (where
AppName, with the default setup is `nitrogen`). This will launch all tests
listed in the `tests` app var in order, and in each browser listed in
`test_browsers`.
If there is an `undefined` specified in `test_browsers`, then it will not
open a browser, but instead expect you to load a specific URL in the
browser of your choice to launch the tests.