blob: 7c83afd74d4bcab244e0223d1623300b415938f3 [file] [log] [blame] [view]
<!--* freshness: { owner: 'olsen' reviewed: '2019-06-07' } *-->
<!-- Originally published June 7, 2019 -->
# Writing a JavaScript unit test
You have written some JS code and put it somewhere in chromium src. Now you want
to write unit tests for it.
[TOC]
## Testing JS code with no dependencies
Let's say you have written a file `awesome.js`. Next you will write your unit
tests in a file in the same directory called `awesome_test.unitjs`. The
`.unitjs` suffix simply means a javascript unit test. (There are a couple of
other suffix options, but .unitjs is the most common, and it will do for most
purposes).
### Writing the test itself
Here is an example to show how your test might look:
```js
/** Tests for awesome.js */
GEN_INCLUDE([
'awesome.js' // Include the file under test.
]);
AwesomeUnitTest = class extends testing.Test {}
const EXPECTED_AWESOME_NUMBER = 1e6;
TEST_F('AwesomeUnitTest', 'HowAwesomeExactly', function () {
// Some asserts to make sure the file under test is working as expected.
assertEquals(EXPECTED_AWESOME_NUMBER, awesome.find_awesome_number(), 'If this fails, contact Larry');
});
```
Note: This example shows the shorter way of writing a test, using the class
keyword (available since ECMAScript 6). There is an alternative but equivalent
syntax that uses the prototype keyword instead, which has a bit more
boilerplate. Codesearch shows that the prototype has been used more often in
the past, but prefer to use the shorter class syntax going forward.
### Writing the BUILD rules
Next you need to add a
[js2gtest](https://cs.chromium.org/chromium/src/chrome/test/base/js2gtest.gni)
rule in the same package. Let's say it's called `package_one`. Here is an
example to show how your BUILD rules might look:
```build
js2gtest("package_one_unitjs_tests") {
test_type = "unit"
sources = [
"awesome_test.unitjs",
# ... for simplicity, put all .unitjs files in this package into this one rule.
]
gen_include_files = [
"awesome.js",
]
}
source_set("unit_tests") {
testonly = true
deps = [":package_one_unitjs_tests"]
}
```
And finally, you will have to modify the huge `//chrome/test:unit_tests` build
rule to depend (directly or indirectly) on your new `//package_one:unit_tests`
target.
Once this is done, you can build and run the tests like normal:
```shell
chromium/src$ out/Default/unit_tests --gtest_filter="AwesomeUnitTest*"
```
## Testing JS code which depends on another library
You may have some other way that the dependencies are being correctly included
in production. However, it will probably be simplest not to try and make the
`js2gtest` rule recognize the dependency management system you are using, but
instead just declare all JS dependencies explicitly when writing the test. The
way to do this is by adding the needed JS source files to both `GEN_INCLUDE` in
the test and `gen_include_files` in the BUILD rule. These files will be included
in the order specified in the JS file. These files that are included can also
have their own calls to GEN_INCLUDE to transitively include other files, if
needed - these would then all need to be listed in the gen_include_files list in
the BUILD rule.
### Writing a test which depends on another library
```js {highlight="lines:4"}
/** Tests for awesome.js */
GEN_INCLUDE([
'//some/dependency.js',
'awesome.js',
]);
AwesomeUnitTest = class extends testing.Test {}
// ... etc ...
```
### Writing the BUILD rule for a test that depends on another library
```build {highlight="lines:7"}
js2gtest("package_one_unitjs_tests") {
test_type = "unit"
sources = [
"awesome_test.unitjs",
]
gen_include_files = [
"//some/dependency.js",
"awesome.js",
]
}
# ... etc ...
```
See
[SamlTimestampsTest](https://cs.chromium.org/chromium/src/chrome/browser/resources/chromeos/login/saml_timestamps_test.unitjs?q=//ui/webui/resources/js/cr.js)
and its
[BUILD rule](https://cs.chromium.org/chromium/src/chrome/browser/resources/chromeos/login/BUILD.gn?q=//ui/webui/resources/js/cr.js)
for an example of how it includes the `cr.js` library.
## Testing JS code which depends on the browser / the DOM {#browser-dep}
The tests above are run using a V8 interpreter. However, they are not run from
within a browser. That means all JS language features are available, but no
browser features are available. Specifically, the browser context (the global
object called `window`) is left undefined, and anything to do with the DOM, the
UI, rendering, parsing, event handling, mouse clicks, HTML, XML, SVG, canvas,
etc is not supported. It may be that you need some of this - perhaps you are
trying to write and test a web-based UI, or perhaps your code just expects an
XML parser to be available.
If you are creating an web-based UI, what you are now writing is called a
`webui` test. This document is about how to write JS unit tests generally -
to test UI in particular, see
[Testing WebUI with Mocha](https://www.chromium.org/developers/updating-webui-for-material-design/testing-webui-with-mocha).
If on the other hand, you are writing some JS functionality that just happens to
use a feature that is part of the browser, and not the language (such as the XML
parser), you should follow the instructions in this section. The current
best-practice is to write your unit test as before, but to declare it as a
`webui` test and add it to the `browser_tests` build rule. Ideally there would
be a different category for unit tests that don't have any UI and so aren't
webui, but simply need a particular browser feature, but using webui works for
now.
### Changes to your test to make it a webui test
```js
AwesomeUnitTest = class extends testing.Test {
/** @override */
get browsePreload() {
return DUMMY_URL;
}
}
```
### Changes to your build rule to make it a webui test
```build {highlight="lines:2,9,12"}
js2gtest("package_one_unitjs_tests") {
test_type = "webui"
sources = [
"awesome_test.unitjs",
]
gen_include_files = [
"awesome.js",
]
defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
}
source_set("browser_tests") {
testonly = true
deps = [":package_one_unitjs_tests"]
}
```
FIXME: It might be nice if "HAS_OUT_OF_PROC_TEST_RUNNER" was automatically
inferred from the test type.
And the final change is to remove your test from the `//chrome/test:unit_tests`
build rule, and add it instead to the `//chrome/test:browser_tests`. As you
would expect, you now run your test like so:
```shell
chromium/src$ out/Default/browser_tests --gtest_filter="AwesomeUnitTest*"
```
And now the browser context and global `window` object should be available in
your test.
Note: If your test is declared as a `unit` test, it must be part of
`//chrome/test:unit_tests`, and if your test is declared as a `webui` test, it
must be part of `//chrome/test:browser_tests`. If your test is included in the
wrong build rule, it will not compile, since it will be missing some necessary
dependencies.
## Troubleshooting
### js2gtest.js: Error reading file
Perhaps one of the files you are trying to read does not exist, has the wrong
name, or has not been properly declared in the list of `gen_include_files` in
`BUILD.gn`. The file that could not be found or read should be part of the error
message, but if for some reason it is not clear, you can narrow it down by
removing files from the `GEN_INCLUDE` directive one by one.
### ReferenceError: window is not defined
Or: document is not defined, DOMParser is not defined, frames is not defined,
alert is not defined...
This sounds like your unit test depends on some functionality that is part of
the browser, and not part of the JS language itself. See the
[section about depending on the browser](#browser-dep)
### Test passes locally but fails on the commit queue
This is probably due to a dependency not being properly declared - for instance,
you have a file in the `GEN_INCLUDE` directive, that is not included in the list
of `gen_include_files` in `BUILD.gn`, When run locally, the test may be able to
find the appropriate file, but the swarming bots that run the submit queue will
not necessarily be able to find files that have not been explicitly declared as
dependencies.
You can try running your test locally but in a more isolated way, so as to
reproduce the problem locally. Something like:
```shell
chromium/src$ tools/mb/mb.py run //out/Default browser_tests -- --gtest_filter="AwesomeUnitTest*"
```
### Duplicate output file {#duplicate}
The `js2gtest` rule copies various JS files to testdata, where they are read as
data when the test is run. This causes problems if two different js2gtest rule
instances both try to copy the same file to the same place in testdata. Having
two rules both copy the same source file to the same destination is a build
error - every file must be written only once.
It should be safe to have multiple js2gtest rules which have files in common in
the `gen_include_files` list, since these files are not copied. But, it will
cause a build error to have multiple js2gtest rules which have files in common
in either the `sources` list or the `extra_js_files` list, since all of these
files are copied to testdata.
To avoid this, only create one js2gtest rule per package which has all the
necessary sources in, and to include files from outside the package, use
gen_include_files and not extra_js_files.
The duplicate output file build error could look something like this:
```
ERROR at //chrome/test/base/js2gtest.gni: Duplicate output file.
copy(copy_target_name) {
^-----------------------
Two or more targets generate the same output:
test_data/ui/webui/resources/js/cr.js
```
If you encounter such a build error and fix it, you could still end up with
warnings on subsequent builds - something like the following:
```
warning: multiple rules generate test_data/ui/webui/resources/js/cr.js. builds involving this target will not be correct; continuing anyway [-w dupbuild=warn]
```
Get rid of this warning by doing a clean build.
## Advanced topics
### GEN_INCLUDE alternative
There is an alternative to `gen_include_files` that is called `extra_js_files`.
These are JS files that are not read during compilation, but are loaded at
runtime. Because they are not read at compile time, they can contain no
directives to be executed at compile time.
Compare this to `gen_include_files` which are both read at compile time, and
included at runtime. This means they support compile time directives like `GEN`
and `GEN_INCLUDE` which can generate code and include further files - and those
further files could have their own directives, and so on.
Whether or not compile time directives are needed or used, these two rules work
differently - extra_js_files copies source files into the `testdata` directory,
but gen_include_files leaves the source files where they are.
Warning: Avoid using `extra_js_files` because all files listed there are copied
into the testdata directory, which can easily lead to the "duplicate output
file" build error described in the [previous section](#duplicate).
However, if you find you need to use extra_js_files instead of
gen_include_files, this is how it is done:
#### Changes to your test to make it use extra_js_files
```js {highlight="lines:4-7"}
AwesomeUnitTest.prototype = {
__proto__: testing.Test.prototype,
extraLibraries: [
"awesome.js",
"//some/dependency.js",
],
};
```
#### Changes to your build rule to make it use extra_js_files
```build {highlight="lines:6"}
js2gtest("package_one_unitjs_tests") {
test_type = "unit"
sources = [
"awesome_test.unitjs",
]
extra_js_files = [
"awesome.js",
"//some/dependency.js"
]
}
```
### Including custom C++ code in the generated C++ test
There are a number of ways to add to the C++ code that is generated by your
.unitjs file.
Calling
[GEN(code)](https://cs.chromium.org/chromium/src/chrome/test/base/js2gtest.js?q=function.GEN%5C%28)
in your .unitjs file will cause the C++ code to be included verbatim in the
generated C++ test.
The function `TEST_F` (or alternative form `TEST_F_WITH_PREAMBLE`) can take a
[preamble](https://cs.chromium.org/chromium/src/chrome/test/base/js2gtest.js?q=preamble)
argument - the preamble is extra C++ code that is included in the generated C++
test, immediately before the code which makes the interpreter run the JS part of
the test.
You can define functions on your JS Test prototype, `testGenPreamble()` and
`testGenPostamble()`. The testGenPreamble function is used to generate code that
is included immediately before the JS part of your test, and the
testGenPostamble function is used to generate code that goes immediately after -
so you could for example put an "#ifdef" in the preamble and an "#endif" in the
postamble. If you define these functions, they should call GEN to output the
necessary C++ code.
Since these are relatively complex to use, it may help you to codesearch for
examples. Here are some useful examples:
[saml_password_attributes_test.unitjs](https://cs.chromium.org/chromium/src/chrome/browser/resources/gaia_auth_host/saml_password_attributes_test.unitjs) -
loads an XML file into a JS global variable before running the JS test.
Another thing that will make your life a little bit easier is being able to
check the C++ code that is generated. Look for it in
`out/Default/path/to/package_one/awesome_test-gen.cc` or equivalent.
Warning: Overuse of custom C++ code can make your test difficult to understand -
at this point you are running JS, that generates custom C++ - which will much
later be compiled and executed, in order to help set up the test environment,
within which the JS test itself will be run.
Tip: To avoid this complexity, consider if it would be clearer to simply write a
C++ test directly, instead of generating the C++ test from a JS file. You can
always run JS code from a C++ test, even if the C++ test was not generated from
a JS file. You could even use the generated output from a js2gtest rule as a
starting point for writing a custom C++ test which you then check in.