mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			299 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# JavaScript/TypeScript unit tests
 | 
						|
 | 
						|
Our node-based unit tests system is the preferred way to test
 | 
						|
JavaScript/TypeScript code in Zulip. We prefer it over the [Puppeteer
 | 
						|
black-box whole-app testing](testing-with-puppeteer.md),
 | 
						|
system since it is much (>100x) faster and also easier to do correctly
 | 
						|
than the Puppeteer system.
 | 
						|
 | 
						|
You can run this test suite as follows:
 | 
						|
 | 
						|
```bash
 | 
						|
tools/test-js-with-node
 | 
						|
```
 | 
						|
 | 
						|
See `test-js-with-node --help` for useful options; even though the
 | 
						|
whole suite is quite fast, it still saves time to run a single test by
 | 
						|
name when debugging something.
 | 
						|
 | 
						|
The JS unit tests are written to work with node. You can find them
 | 
						|
in `web/tests`. Here is an example test from
 | 
						|
`web/tests/stream_data.test.cjs`:
 | 
						|
 | 
						|
```js
 | 
						|
(function test_get_by_id() {
 | 
						|
    stream_data.clear_subscriptions();
 | 
						|
    var id = 42;
 | 
						|
    var sub = {
 | 
						|
        name: 'Denmark',
 | 
						|
        subscribed: true,
 | 
						|
        color: 'red',
 | 
						|
        stream_id: id
 | 
						|
    };
 | 
						|
    stream_data.add_sub_for_tests('Denmark', sub);
 | 
						|
    sub = stream_data.get_sub('Denmark');
 | 
						|
    assert.equal(sub.color, 'red');
 | 
						|
    sub = sub_store.get(id);
 | 
						|
    assert.equal(sub.color, 'red');
 | 
						|
}());
 | 
						|
```
 | 
						|
 | 
						|
The names of the node tests generally align with the names of the
 | 
						|
modules they test. If you modify a JS module in `web/src` you should
 | 
						|
see if there are corresponding test in `web/tests`. If
 | 
						|
there are, you should strive to follow the patterns of the existing tests
 | 
						|
and add your own tests.
 | 
						|
 | 
						|
A good first test to read is
 | 
						|
[example1.test.cjs](https://github.com/zulip/zulip/blob/main/web/tests/example1.test.cjs).
 | 
						|
(And then there are several other example files.)
 | 
						|
 | 
						|
## How the node tests work
 | 
						|
 | 
						|
Unlike the [Puppeteer unit tests](testing-with-puppeteer.md),
 | 
						|
which use a headless Chromium browser connected to a running Zulip
 | 
						|
development server, our node unit tests don't have a browser, don't
 | 
						|
talk to a server, and generally don't use a complete virtual DOM (a
 | 
						|
handful of tests use the `jsdom` library for this purpose) because
 | 
						|
those slow down the tests a lot, and often don't add much value.
 | 
						|
 | 
						|
Instead, the preferred model for our unit tests is to mock DOM
 | 
						|
manipulations (which in Zulip are almost exclusively done via
 | 
						|
`jQuery`) using a custom library
 | 
						|
[zjquery](https://github.com/zulip/zulip/blob/main/web/tests/lib/zjquery.cjs).
 | 
						|
 | 
						|
The
 | 
						|
[unit test file](https://github.com/zulip/zulip/blob/main/web/tests/zjquery.test.cjs)
 | 
						|
for `zjquery` is designed to be also serve as nice documentation for
 | 
						|
how to use `zjquery`, and is **highly recommended reading** for anyone
 | 
						|
working on or debugging the Zulip node tests.
 | 
						|
 | 
						|
Conceptually, the `zjquery` library provides minimal versions of most
 | 
						|
`jQuery` DOM manipulation functions, and has a convenient system for
 | 
						|
letting you set up return values for more complex functions. For
 | 
						|
example, if the code you'd like to test calls `$obj.find()`, you can
 | 
						|
use `$obj.set_find_results(selector, $value)` to set up `zjquery` so
 | 
						|
that calls to `$obj.find(selector)` will return `$value`. See the unit
 | 
						|
test file for details.
 | 
						|
 | 
						|
This process of substituting `jQuery` functions with our own code for
 | 
						|
testing purposes is known as "stubbing". `zjquery` does not stub all
 | 
						|
possible interactions with the dom, as such, you may need to write out
 | 
						|
the stub for a function you're calling in your patch. Typically the stub
 | 
						|
is just placed in the test file, to prevent bloating of `zjquery`
 | 
						|
with functions that are only used in a single test.
 | 
						|
 | 
						|
If you need to stub, you will see an error of this form:
 | 
						|
`Error: You must create a stub for $("#foo").bar`
 | 
						|
 | 
						|
The `zjquery` library itself is only about 500 lines of code, and can
 | 
						|
also be a useful resource if you're having trouble debugging DOM
 | 
						|
access in the unit tests.
 | 
						|
 | 
						|
It is typically a good idea to figure out how to stub a given function
 | 
						|
based on how other functions have been stubbed in the same file.
 | 
						|
 | 
						|
## Handling dependencies in unit tests
 | 
						|
 | 
						|
The other big challenge with doing unit tests for a JavaScript project
 | 
						|
is that often one wants to limit the scope the production code being
 | 
						|
run, just to avoid doing extra setup work that isn't relevant to the
 | 
						|
code you're trying to test. For that reason, each unit test file
 | 
						|
explicitly declares all of the modules it depends on, with a few
 | 
						|
different types of declarations depending on whether we want to:
 | 
						|
 | 
						|
- Exercise the module's real code for deeper, more realistic testing?
 | 
						|
- Stub out the module's interface for more control, speed, and
 | 
						|
  isolation?
 | 
						|
- Do some combination of the above?
 | 
						|
 | 
						|
For all the modules where you want to run actual code, add statements
 | 
						|
like the following toward the top of your test file:
 | 
						|
 | 
						|
```js
 | 
						|
zrequire('util');
 | 
						|
zrequire('stream_data');
 | 
						|
zrequire('Filter', 'js/filter');
 | 
						|
```
 | 
						|
 | 
						|
For modules that you want to completely stub out, use a pattern like
 | 
						|
this:
 | 
						|
 | 
						|
```js
 | 
						|
const reminder = mock_esm("../../web/src/reminder", {
 | 
						|
    is_deferred_delivery: noop,
 | 
						|
});
 | 
						|
 | 
						|
// then maybe further down
 | 
						|
reminder.is_deferred_delivery = () => true;
 | 
						|
```
 | 
						|
 | 
						|
One can similarly stub out functions in a module's exported interface
 | 
						|
with either `noop` functions or actual code.
 | 
						|
 | 
						|
Finally, there's the hybrid situation, where you want to borrow some
 | 
						|
of a module's real functionality but stub out other pieces. Obviously,
 | 
						|
this is a pretty strong code smell that the other module might be
 | 
						|
lacking in cohesion, but sometimes it's not worth going down the
 | 
						|
rabbit hole of trying to improve that. The pattern here is this:
 | 
						|
 | 
						|
```js
 | 
						|
// Import real code.
 | 
						|
zrequire('narrow_state');
 | 
						|
 | 
						|
// And later...
 | 
						|
narrow_state.stream = function () {
 | 
						|
    return 'office';
 | 
						|
};
 | 
						|
```
 | 
						|
 | 
						|
## Creating new test modules
 | 
						|
 | 
						|
The test runner (`index.cjs`) automatically runs all .test.cjs files in the
 | 
						|
`web/tests` directory, so you can simply start editing a file
 | 
						|
in that directory to create a new test.
 | 
						|
 | 
						|
## Verifying HTML templates with `mock_template`
 | 
						|
 | 
						|
As a project, we prefer [end-to-end
 | 
						|
testing][testing-philosophy-end-to-end] where possible, since those
 | 
						|
tests are usually both more effective at catching bugs and cheaper to
 | 
						|
maintain than tests that make heavy use of mocks.
 | 
						|
 | 
						|
One place where mocks can often be useful is when testing logic for
 | 
						|
rendering an HTML template in Zulip. The interesting logic that one
 | 
						|
wants to verify can be split between two places:
 | 
						|
 | 
						|
- Computing the **context data** object passed into the HTML template.
 | 
						|
- Conditional logic in the HTML template itself.
 | 
						|
 | 
						|
It can work well to write tests that verify properties of the computed
 | 
						|
HTML template, for example, is a given CSS class present in the
 | 
						|
result.
 | 
						|
 | 
						|
But often, one can write a more readable test by instead verifying
 | 
						|
the values of parameters in the context passed into the template
 | 
						|
rendering. The `mock_template` function in Zulip's testing library
 | 
						|
is designed to support this this.
 | 
						|
 | 
						|
We use `mock_template` in our unit tests to verify that the JS code is
 | 
						|
calling the template with the expected context data. And then we use
 | 
						|
the results of mock_template to supply the JS code with either the
 | 
						|
actual HTML from the template or some kind of zjquery stub.
 | 
						|
 | 
						|
The `mock_template` function accepts 3 parameters:
 | 
						|
 | 
						|
- The path within `web/templates` to the [Handlebars
 | 
						|
  template](../subsystems/html-css.md) that you'd like to mock.
 | 
						|
- Whether to call the actual template rendering function so that you
 | 
						|
  can verify the HTML generated by this specific template. Since
 | 
						|
  Handlebars rendering in tests takes time and rarely catches bugs, we
 | 
						|
  recommend using `false` if you're only planning to check the context
 | 
						|
  data.
 | 
						|
- A callback function that you can use to include assertions about
 | 
						|
  what parameters were passed into the template. This function
 | 
						|
  receives a `data` parameter, with the context data, and an `html`
 | 
						|
  parameter if the real template was rendered.
 | 
						|
 | 
						|
The following illustrates the two common patterns for using this method.
 | 
						|
 | 
						|
```js
 | 
						|
run_test("test something calling template", ({mock_template}) => {
 | 
						|
    mock_template("path/to/template.hbs", false, (data) => {
 | 
						|
        assert.deepEqual(data, {...};
 | 
						|
        // or assert.deepEqual(data.foo, {...});
 | 
						|
        return "stub-for-zjquery";
 | 
						|
    });
 | 
						|
 | 
						|
    mock_template("path/to/template.hbs", true, (data, html) => {
 | 
						|
        assert.deepEqual(data, {...};
 | 
						|
        assert.ok(html.startWith(...));
 | 
						|
        return html;
 | 
						|
    });
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
[testing-philosophy-end-to-end]: https://zulip.readthedocs.io/en/stable/testing/philosophy.html#integration-testing-or-unit-testing
 | 
						|
 | 
						|
## Coverage reports
 | 
						|
 | 
						|
You can automatically generate coverage reports for the JavaScript unit
 | 
						|
tests like this:
 | 
						|
 | 
						|
```bash
 | 
						|
tools/test-js-with-node --coverage
 | 
						|
```
 | 
						|
 | 
						|
If tests pass, you will get instructions to view coverage reports
 | 
						|
in your browser.
 | 
						|
 | 
						|
Note that modules that we don't test _at all_ aren't listed in the
 | 
						|
report, so this tends to overstate how good our overall coverage is,
 | 
						|
but it's accurate for individual files. You can also click a filename
 | 
						|
to see the specific statements and branches not tested. 100% branch
 | 
						|
coverage isn't necessarily possible, but getting to at least 80%
 | 
						|
branch coverage is a good goal.
 | 
						|
 | 
						|
The overall project goal is to get to 100% node test coverage on all
 | 
						|
data/logic modules (UI modules are lower priority for unit testing).
 | 
						|
 | 
						|
## Editor debugger integration
 | 
						|
 | 
						|
Our node test system is pretty simple, and it's possible to configure
 | 
						|
the native debugger features of popular editors to allow stepping
 | 
						|
through the code. Below we document the editors where someone has put
 | 
						|
together detailed instructions for how to do so. Contributions of
 | 
						|
notes for other editors are welcome!
 | 
						|
 | 
						|
## Webstorm integration setup
 | 
						|
 | 
						|
These instructions assume you're using the Vagrant development environment.
 | 
						|
 | 
						|
1. Set up [Vagrant in WebStorm][vagrant-webstorm].
 | 
						|
 | 
						|
2. In WebStorm, navigate to `Preferences -> Tools -> Vagrant` and
 | 
						|
   configure the following:
 | 
						|
 | 
						|
   - `Instance folder` should be the root of the `zulip` repository on
 | 
						|
     your host (where the Vagrantfile is located).
 | 
						|
   - `Provider` should be `virtualbox` on macOS and Docker on Linux
 | 
						|
   - In `Boxes`, choose the one used for Zulip (unless you use
 | 
						|
     Virtualbox for other things, there should only be one option).
 | 
						|
 | 
						|
   You shouldn't need to set these additional settings:
 | 
						|
 | 
						|
   - `Vagrant executable` should already be correctly `vagrant`.
 | 
						|
   - `Environment Variables` is not needed.
 | 
						|
 | 
						|
3. You'll now need to set up a WebStorm "Debug Configuration". Open
 | 
						|
   the `Run/Debug Configuration` menu and create a new `Node.js` config:
 | 
						|
   1. Under `Node interpreter:` click the 3 dots to the right side and
 | 
						|
      click on the little plus in the bottom left of the
 | 
						|
      `Node.js Interpreters` window.
 | 
						|
   1. Select `Add Remote...`.
 | 
						|
      1. In the `Configure Node.js Remote Interpreter`, window select `Vagrant`
 | 
						|
      1. Wait for WebStorm to connect to Vagrant. This will be displayed
 | 
						|
         by the `Vagrant Host URL` section updating to contain the Vagrant
 | 
						|
         SSH URL, e.g., `ssh://vagrant@127.0.0.1:2222`.
 | 
						|
      1. **Set the `Node.js interpreter path` to `/usr/local/bin/node`**
 | 
						|
      1. Hit `OK` 2 times to get back to the `Run/Debug Configurations` window.
 | 
						|
   1. Under `Working Directory` select the root `zulip` directory.
 | 
						|
   1. Under `JavaScript file`, enter `web/tests/lib/index.cjs`
 | 
						|
      -- this is the root script for Zulip's node unit tests.
 | 
						|
 | 
						|
Congratulations! You've now set up the integration.
 | 
						|
 | 
						|
## Running tests with the debugger
 | 
						|
 | 
						|
To use Webstorm to debug a given node test file, do the following:
 | 
						|
 | 
						|
1. Under `Application parameters` choose the node test file that you
 | 
						|
   are trying to test (e.g., `web/tests/message_store.test.cjs`).
 | 
						|
1. Under `Path Mappings`, set `Project Root` to `/srv/zulip`
 | 
						|
   (i.e. where the `zulip` Git repository is mounted in the Vagrant guest).
 | 
						|
1. Use the WebStorm debugger; see [this overview][webstorm-debugging]
 | 
						|
   for details on how to use it.
 | 
						|
 | 
						|
[webstorm-debugging]: https://blog.jetbrains.com/webstorm/2018/01/how-to-debug-with-webstorm/
 | 
						|
[vagrant-webstorm]: https://www.jetbrains.com/help/webstorm/vagrant-support.html?section=Windows%20or%20Linux
 |