node tests: Split up tutorial into smaller examples.

I didn't bother to keep the very last test from
tutorial, which has some complications related
to clearing stream_list data and was kind of
overkill.
This commit is contained in:
Steve Howell 2021-03-15 12:03:00 +00:00 committed by Steve Howell
parent bbec2d9625
commit 198c67a9bd
10 changed files with 766 additions and 810 deletions

View File

@ -44,7 +44,8 @@ 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
[tutorial.js](https://github.com/zulip/zulip/blob/master/frontend_tests/node_tests/tutorial.js).
[example1.js](https://github.com/zulip/zulip/blob/master/frontend_tests/node_tests/example1.js).
(And then there are several other example files.)
## How the node tests work

View File

@ -0,0 +1,73 @@
"use strict";
// This is a general tour of how to write node tests that
// may also give you some quick insight on how the Zulip
// browser app is constructed.
// The statements below are pretty typical for most node
// tests. The reason we need these helpers will hopefully
// become clear as you keep reading.
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
// We will use our special zrequire helper to import the
// Zulip code. We use zrequire instead of require,
// because it has some magic to clear state when we move
// on to the next test.
const people = zrequire("people");
const stream_data = zrequire("stream_data");
const util = zrequire("util");
// Let's start with testing a function from util.js.
//
// The most basic unit tests load up code, call functions,
// and assert truths:
assert(!util.find_wildcard_mentions("boring text"));
assert(util.find_wildcard_mentions("mention @**everyone**"));
// Let's test with people.js next. We'll show this technique:
// * get a false value
// * change the data
// * get a true value
const isaac = {
email: "isaac@example.com",
user_id: 30,
full_name: "Isaac Newton",
};
// The `people`object is a very fundamental object in the
// Zulip app. You can learn a lot more about it by reading
// the tests in people.js in the same directory as this file.
// Let's exercise the code and use assert to verify it works!
assert(!people.is_known_user_id(isaac.user_id));
people.add_active_user(isaac);
assert(people.is_known_user_id(isaac.user_id));
// Let's look at stream_data next, and we will start by putting
// some data at module scope. (You could also declare this inside
// the test, if you prefer.)
const denmark_stream = {
color: "blue",
name: "Denmark",
stream_id: 101,
subscribed: false,
};
// We introduce the run_test helper, which mostly just causes
// a line of output to go to the console. It does a little more than
// that, which we will see later.
run_test("verify stream_data persists stream color", () => {
stream_data.clear_subscriptions();
assert.equal(stream_data.get_sub_by_name("Denmark"), undefined);
stream_data.add_sub(denmark_stream);
const sub = stream_data.get_sub_by_name("Denmark");
assert.equal(sub.color, "blue");
});
// See example2.js in this directory.

View File

@ -0,0 +1,99 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
// Hopefully the basic patterns for testing data-oriented modules
// are starting to become apparent. To reinforce that, we will present
// few more examples that also expose you to some of our core
// data objects. Also, we start testing some objects that have
// deeper dependencies.
const message_store = zrequire("message_store");
const people = zrequire("people");
const stream_data = zrequire("stream_data");
const stream_topic_history = zrequire("stream_topic_history");
const unread = zrequire("unread");
// It's typical to set up a little bit of data at the top of a
// test module, but you can also do this within tests. Here we
// will set up things at the top.
const isaac = {
email: "isaac@example.com",
user_id: 30,
full_name: "Isaac Newton",
};
const denmark_stream = {
color: "blue",
name: "Denmark",
stream_id: 101,
subscribed: false,
};
const messages = {
isaac_to_denmark_stream: {
id: 400,
sender_id: isaac.user_id,
stream_id: denmark_stream.stream_id,
type: "stream",
flags: ["has_alert_word"],
topic: "copenhagen",
// note we don't have every field that a "real" message
// would have, and that can be fine
},
};
// We aren't going to modify isaac in our tests, so we will
// create him at the top.
people.add_active_user(isaac);
// We are going to test a core module called messages_store.js next.
// This is an example of a deep unit test, where our dependencies
// are easy to test.
run_test("message_store", () => {
message_store.clear_for_testing();
stream_data.clear_subscriptions();
stream_data.add_sub(denmark_stream);
const in_message = {...messages.isaac_to_denmark_stream};
assert.equal(in_message.alerted, undefined);
message_store.set_message_booleans(in_message);
assert.equal(in_message.alerted, true);
// Let's add a message into our message_store via
// add_message_metadata.
assert.equal(message_store.get(in_message.id), undefined);
message_store.add_message_metadata(in_message);
const message = message_store.get(in_message.id);
assert.equal(message, in_message);
// There are more side effects.
const topic_names = stream_topic_history.get_recent_topic_names(denmark_stream.stream_id);
assert.deepEqual(topic_names, ["copenhagen"]);
});
// Tracking unread messages is a very fundamental part of the Zulip
// app, and we use the unread object to track unread messages.
run_test("unread", () => {
unread.declare_bankruptcy();
stream_data.clear_subscriptions();
stream_data.add_sub(denmark_stream);
const stream_id = denmark_stream.stream_id;
const topic_name = "copenhagen";
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 0);
const in_message = {...messages.isaac_to_denmark_stream};
message_store.set_message_booleans(in_message);
unread.process_loaded_messages([in_message]);
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 1);
});

View File

@ -0,0 +1,100 @@
"use strict";
const {strict: assert} = require("assert");
const {set_global, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
// In the Zulip app you can narrow your message stream by topic, by
// sender, by PM recipient, by search keywords, etc. We will discuss
// narrows more broadly, but first let's test out a core piece of
// code that makes things work.
const {Filter} = zrequire("../js/filter");
const stream_data = zrequire("stream_data");
// This is the first time we have to deal with page_params. We
// use set_global to set it to our preferred state.
// (There are a few global variables in the Zulip codebase, and
// page_params has a lot of important data shared by various
// modules. Most of the data is irrelevant to out tests.)
set_global("page_params", {
// Use this to explicitly say we are not a special Zephyr
// realm, since we want to test the "normal" codepath.
realm_is_zephyr_mirror_realm: false,
});
const denmark_stream = {
color: "blue",
name: "Denmark",
stream_id: 101,
subscribed: false,
};
run_test("filter", () => {
stream_data.clear_subscriptions();
stream_data.add_sub(denmark_stream);
const filter_terms = [
{operator: "stream", operand: "Denmark"},
{operator: "topic", operand: "copenhagen"},
];
const filter = new Filter(filter_terms);
const predicate = filter.predicate();
// We don't need full-fledged messages to test the gist of
// our filter. If there are details that are distracting from
// your test, you should not feel guilty about removing them.
assert.equal(predicate({type: "personal"}), false);
assert.equal(
predicate({
type: "stream",
stream_id: denmark_stream.stream_id,
topic: "does not match filter",
}),
false,
);
assert.equal(
predicate({
type: "stream",
stream_id: denmark_stream.stream_id,
topic: "copenhagen",
}),
true,
);
});
// We have a "narrow" abstraction that sits roughly on top of the
// "filter" abstraction. If you are in a narrow, we track the
// state with the narrow_state module.
const narrow_state = zrequire("narrow_state");
run_test("narrow_state", () => {
stream_data.clear_subscriptions();
stream_data.add_sub(denmark_stream);
narrow_state.reset_current_filter();
// As we often do, first make assertions about the starting
// state:
assert.equal(narrow_state.stream(), undefined);
// Now set up a Filter object.
const filter_terms = [
{operator: "stream", operand: "Denmark"},
{operator: "topic", operand: "copenhagen"},
];
const filter = new Filter(filter_terms);
// And here is where we actually change state.
narrow_state.set_current_filter(filter);
assert.equal(narrow_state.stream(), "Denmark");
assert.equal(narrow_state.topic(), "copenhagen");
});

View File

@ -0,0 +1,144 @@
"use strict";
const {strict: assert} = require("assert");
const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
/*
Let's step back and review what we've done so far.
We've used fairly straightforward testing techniques
to explore the following modules:
filter
message_store
narrow_state
people
stream_data
util
We haven't gone deep on any of these objects, but if
you are interested, all of these objects have test
suites that have 100% line coverage on the modules
that implement those objects. For example, you can look
at people.js in this directory for more tests on the
people object.
We can quickly review some testing concepts:
zrequire - bring in real code
set_global - create stubs
assert.equal - verify results
------
Let's talk about our next steps.
An app is pretty useless without an actual data source.
One of the primary ways that a Zulip client gets data
is through events. (We also get data at page load, and
we can also ask the server for data, but that's not in
the scope of this conversation yet.)
Chat systems are dynamic. If an admin adds a user, or
if a user sends a messages, the server immediately sends
events to all clients so that they can reflect appropriate
changes in their UI. We're not going to discuss the entire
"full stack" mechanism here. Instead, we'll focus on
the client code, starting at the boundary where we
process events.
Let's just get started...
*/
// We are going to use mock versions of some of our libraries.
const activity = mock_esm("../../static/js/activity");
const message_live_update = mock_esm("../../static/js/message_live_update");
const pm_list = mock_esm("../../static/js/pm_list");
const settings_users = mock_esm("../../static/js/settings_users");
// Use real versions of these modules.
const people = zrequire("people");
const server_events_dispatch = zrequire("server_events_dispatch");
const bob = {
email: "bob@example.com",
user_id: 33,
full_name: "Bob Roberts",
};
run_test("add users with event", () => {
people.init();
const event = {
type: "realm_user",
op: "add",
person: bob,
};
assert(!people.is_known_user_id(bob.user_id));
// Let's simulate dispatching our event!
server_events_dispatch.dispatch_normal_event(event);
// And it works!
assert(people.is_known_user_id(bob.user_id));
});
/*
It's actually a little surprising that adding a user does
not have side effects beyond the people object. I guess
we don't immediately update the buddy list, but that's
because the buddy list gets updated on the next server
fetch.
Let's try an update next. To make this work, we will want
to override some of our stubs.
This is where we see a little extra benefit from the
run_test wrapper. It passes us in an object that we
can use to override data, and that works within the
scope of the function.
*/
run_test("update user with event", (override) => {
people.init();
people.add_active_user(bob);
const new_bob = {
email: "bob@example.com",
user_id: bob.user_id,
full_name: "The Artist Formerly Known as Bob",
};
const event = {
type: "realm_user",
op: "update",
person: new_bob,
};
// We have to stub a few things. We don't want to test
// the details of these functions, but we do want to
// verify that they run. Fortunately, the run_test()
// wrapper will tell us if we override a method that
// doesn't get called!
override(activity, "redraw", () => {});
override(message_live_update, "update_user_full_name", () => {});
override(pm_list, "update_private_messages", () => {});
override(settings_users, "update_user_data", () => {});
// Dispatch the realm_user/update event, which will update
// data structures and have other side effects that are
// stubbed out above.
server_events_dispatch.dispatch_normal_event(event);
const user = people.get_by_user_id(bob.user_id);
// Verify that the code actually did its main job:
assert.equal(user.full_name, "The Artist Formerly Known as Bob");
});

View File

@ -0,0 +1,124 @@
"use strict";
const {strict: assert} = require("assert");
const {mock_esm, set_global, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
/*
Our test from an earlier example verifies that the update events
leads to a name change inside the people object, but it obviously
kind of glosses over the other interactions.
We can go a step further and verify the sequence of operations that
happen during an event. This concept is called "stubbing", and you
can find libraries to help do stubbing. Here we will just build our
own lightweight stubbing system, which is almost trivially easy to
do in a language like JavaScript.
*/
// First we tell the compiler to skip certain modules and just
// replace them with {}.
const huddle_data = mock_esm("../../static/js/huddle_data");
const message_util = mock_esm("../../static/js/message_util");
const notifications = mock_esm("../../static/js/notifications");
const pm_list = mock_esm("../../static/js/pm_list");
const resize = mock_esm("../../static/js/resize");
const stream_list = mock_esm("../../static/js/stream_list");
const unread_ops = mock_esm("../../static/js/unread_ops");
const unread_ui = mock_esm("../../static/js/unread_ui");
set_global("home_msg_list", {});
// And we will also test some real code, of course.
const message_events = zrequire("message_events");
const message_store = zrequire("message_store");
const narrow_state = zrequire("narrow_state");
const people = zrequire("people");
const recent_topics = zrequire("recent_topics");
const isaac = {
email: "isaac@example.com",
user_id: 30,
full_name: "Isaac Newton",
};
people.add_active_user(isaac);
/*
Next we create a test_helper that will allow us to redirect methods to an
events array, and we can then later verify that the sequence of side effect
is as predicted.
(Note that for now we don't simulate return values nor do we inspect the
arguments to these functions. We could easily extend our helper to do more.)
The forthcoming example is a pretty extreme example, where we are calling a
pretty high level method that dispatches a lot of its work out to other
objects.
*/
function test_helper(override) {
const events = [];
return {
redirect: (module, func_name) => {
override(module, func_name, () => {
events.push([module, func_name]);
});
},
events,
};
}
run_test("insert_message", (override) => {
message_store.clear_for_testing();
override(pm_list, "update_private_messages", () => {});
override(recent_topics, "is_visible", () => false);
const helper = test_helper(override);
const new_message = {
sender_id: isaac.user_id,
id: 1001,
content: "example content",
};
assert.equal(message_store.get(new_message.id), undefined);
helper.redirect(huddle_data, "process_loaded_messages");
helper.redirect(message_util, "add_new_messages");
helper.redirect(notifications, "received_messages");
helper.redirect(resize, "resize_page_components");
helper.redirect(stream_list, "update_streams_sidebar");
helper.redirect(unread_ops, "process_visible");
helper.redirect(unread_ui, "update_unread_counts");
narrow_state.reset_current_filter();
message_events.insert_new_messages([new_message]);
// Even though we have stubbed a *lot* of code, our
// tests can still verify the main "narrative" of how
// the code invokes various objects when a new message
// comes in:
assert.deepEqual(helper.events, [
[huddle_data, "process_loaded_messages"],
[message_util, "add_new_messages"],
[message_util, "add_new_messages"],
[unread_ui, "update_unread_counts"],
[resize, "resize_page_components"],
[unread_ops, "process_visible"],
[notifications, "received_messages"],
[stream_list, "update_streams_sidebar"],
]);
// Despite all of our stubbing/mocking, the call to
// insert_new_messages will have created a very important
// side effect that we can verify:
const inserted_message = message_store.get(new_message.id);
assert.equal(inserted_message.id, new_message.id);
assert.equal(inserted_message.content, "example content");
});

View File

@ -0,0 +1,67 @@
"use strict";
const {strict: assert} = require("assert");
const {make_stub} = require("../zjsunit/stub");
const {run_test} = require("../zjsunit/test");
/*
The previous example was a bit extreme. Generally we just
use the make_stub helper that comes with zjsunit.
We will step away from the actual Zulip codebase for a
second and just explore a contrived example.
*/
run_test("explore make_stub", (override) => {
// Let's say you have to test the following code.
const app = {
notify_server_of_deposit(deposit_amount) {
// simulate difficulty
throw new Error(`We cannot report this value without wifi: ${deposit_amount}`);
},
pop_up_fancy_confirmation_screen(deposit_amount, label) {
// simulate difficulty
throw new Error(`We cannot make a ${label} dialog for amount ${deposit_amount}`);
},
};
let balance = 40;
function deposit_paycheck(paycheck_amount) {
balance += paycheck_amount;
app.notify_server_of_deposit(paycheck_amount);
app.pop_up_fancy_confirmation_screen(paycheck_amount, "paycheck");
}
// Our deposit_paycheck should be easy to unit test for its
// core functionality (updating your balance), but the side
// effects get in the way. We have to override them to do
// the simple test here.
override(app, "notify_server_of_deposit", () => {});
override(app, "pop_up_fancy_confirmation_screen", () => {});
deposit_paycheck(10);
assert.equal(balance, 50);
// But we can do a little better here. Even though
// the two side-effect functions are awkward here, we can
// at least make sure they are invoked correctly. Let's
// use stubs.
const notify_stub = make_stub();
const pop_up_stub = make_stub();
// This time we'll just use our override helper to connect the
// stubs.
override(app, "notify_server_of_deposit", notify_stub.f);
override(app, "pop_up_fancy_confirmation_screen", pop_up_stub.f);
deposit_paycheck(25);
assert.equal(balance, 75);
assert.deepEqual(notify_stub.get_args("amount"), {amount: 25});
assert.deepEqual(pop_up_stub.get_args("amount", "label"), {amount: 25, label: "paycheck"});
});

View File

@ -0,0 +1,157 @@
"use strict";
const {strict: assert} = require("assert");
const {mock_esm, set_global, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
/*
Let's continue to explore how we can test complex
interactions in the real code.
When a new message comes in, we update the three major
panes of the app:
* left sidebar - stream list
* middle pane - message view
* right sidebar - buddy list (aka "activity" list)
These are reflected by the following calls:
stream_list.update_streams_sidebar
message_util.add_new_messages
activity.process_loaded_messages
For now, though, let's focus on another side effect
of processing incoming messages:
unread_ops.process_visible
When new messages come in, they are often immediately
visible to users, so the app will communicate this
back to the server by calling unread_ops.process_visible.
In order to unit test this, we don't want to require
an actual server to be running. Instead, this example
will stub many of the "boundaries" to focus on the
core behavior.
The two key pieces here are as follows:
* Use mock_esm to avoid compiling the "real"
modules that are immaterial to our current
testing concerns.
* Use override(...) to simulate how we want
methods to behave. (Often we want them to
do nothing at all or return a simple
value.)
*/
const channel = mock_esm("../../static/js/channel");
const message_list = mock_esm("../../static/js/message_list");
const message_viewport = mock_esm("../../static/js/message_viewport");
const notifications = mock_esm("../../static/js/notifications");
const overlays = mock_esm("../../static/js/overlays");
const unread_ui = mock_esm("../../static/js/unread_ui");
const message_store = zrequire("message_store");
const recent_topics = zrequire("recent_topics");
const stream_data = zrequire("stream_data");
const unread = zrequire("unread");
const unread_ops = zrequire("unread_ops");
const current_msg_list = set_global("current_msg_list", {});
const home_msg_list = set_global("home_msg_list", {});
const denmark_stream = {
color: "blue",
name: "Denmark",
stream_id: 101,
subscribed: false,
};
run_test("unread_ops", (override) => {
stream_data.clear_subscriptions();
stream_data.add_sub(denmark_stream);
message_store.clear_for_testing();
unread.declare_bankruptcy();
const message_id = 50;
const test_messages = [
{
id: message_id,
type: "stream",
stream_id: denmark_stream.stream_id,
topic: "copenhagen",
unread: true,
},
];
override(recent_topics, "is_visible", () => false);
// Make our test message appear to be unread, so that
// we then need to subsequently process them as read.
unread.process_loaded_messages(test_messages);
// Make our window appear visible.
override(notifications, "is_window_focused", () => true);
// Make our "test" message appear visible.
override(message_viewport, "bottom_message_visible", () => true);
// Make us not be in a narrow (somewhat hackily).
message_list.narrowed = undefined;
// Set current_msg_list containing messages that can be marked read
override(current_msg_list, "all_messages", () => test_messages);
// Ignore these interactions for now:
message_list.all = {
show_message_as_read() {},
};
override(home_msg_list, "show_message_as_read", () => {});
override(notifications, "close_notification", () => {});
override(unread_ui, "update_unread_counts", () => {});
// Set up a way to capture the options passed in to channel.post.
let channel_post_opts;
override(channel, "post", (opts) => {
channel_post_opts = opts;
});
// Let the real code skip over details related to active overlays.
override(overlays, "is_active", () => false);
let can_mark_messages_read;
// Set up an override to point to the above var, so we can
// toggle it easily from within the test (and avoid complicated
// data setup).
override(current_msg_list, "can_mark_messages_read", () => can_mark_messages_read);
// First, test for a message list that cannot read messages.
can_mark_messages_read = false;
unread_ops.process_visible();
assert.deepEqual(channel_post_opts, undefined);
// Now flip the boolean, and get to the main thing we are testing.
can_mark_messages_read = true;
unread_ops.process_visible();
// The most important side effect of the above call is that
// we post info to the server. We can verify that the correct
// url and parameters are specified:
assert.deepEqual(channel_post_opts, {
url: "/json/messages/flags",
idempotent: true,
data: {messages: "[50]", op: "add", flag: "read"},
success: channel_post_opts.success,
});
// Simulate a successful post (which also clears the queue
// in our message_flag code).
channel_post_opts.success({messages: [message_id]});
});

View File

@ -1,790 +0,0 @@
"use strict";
// This is a general tour of how to write node tests that
// may also give you some quick insight on how the Zulip
// browser app is constructed.
// The statements below are pretty typical for most node
// tests. The reason we need these helpers will hopefully
// become clear as you keep reading.
const {strict: assert} = require("assert");
const {
mock_cjs,
mock_esm,
set_global,
unmock_module,
with_field,
zrequire,
} = require("../zjsunit/namespace");
const {make_stub} = require("../zjsunit/stub");
const {run_test} = require("../zjsunit/test");
const $ = require("../zjsunit/zjquery");
// Some quick housekeeping: Let's clear page_params, which is a data
// structure that the server sends down to us when the app starts. We
// prefer to test with a clean slate.
mock_cjs("jquery", $);
const activity = mock_esm("../../static/js/activity");
const channel = mock_esm("../../static/js/channel");
const home_msg_list = set_global("home_msg_list", {});
const message_list = mock_esm("../../static/js/message_list");
const message_live_update = mock_esm("../../static/js/message_live_update");
const message_util = mock_esm("../../static/js/message_util");
const message_viewport = mock_esm("../../static/js/message_viewport");
const notifications = mock_esm("../../static/js/notifications");
const overlays = mock_esm("../../static/js/overlays");
const pm_list = mock_esm("../../static/js/pm_list");
const resize = mock_esm("../../static/js/resize");
const settings_users = mock_esm("../../static/js/settings_users");
const topic_list = mock_esm("../../static/js/topic_list");
const unread_ui = mock_esm("../../static/js/unread_ui");
let stream_list = mock_esm("../../static/js/stream_list");
let unread_ops = mock_esm("../../static/js/unread_ops");
set_global("page_params", {});
// Let's start with testing a function from util.js.
//
// We will use our special zrequire helper to import the
// code from util. We use zrequire instead of require,
// because it has some magic to clear state when we move
// on to the next test.
//
// The most basic unit tests load up code, call functions,
// and assert truths:
const util = zrequire("util");
assert(!util.find_wildcard_mentions("boring text"));
assert(util.find_wildcard_mentions("mention @**everyone**"));
// Let's test with people.js next. We'll show this technique:
// * get a false value
// * change the data
// * get a true value
const people = zrequire("people");
const isaac = {
email: "isaac@example.com",
user_id: 30,
full_name: "Isaac Newton",
};
assert(!people.is_known_user_id(isaac.user_id));
people.add_active_user(isaac);
assert(people.is_known_user_id(isaac.user_id));
// The `people`object is a very fundamental object in the
// Zulip app. You can learn a lot more about it by reading
// the tests in people.js in the same directory as this file.
// Let's create the current user, which some future tests will
// require.
const me = {
email: "me@example.com",
user_id: 31,
full_name: "Me Myself",
};
people.add_active_user(me);
people.initialize_current_user(me.user_id);
// Let's look at stream_data next, and we will start by putting
// some data at module scope (since it may be useful for future
// tests):
const denmark_stream = {
color: "blue",
name: "Denmark",
stream_id: 101,
subscribed: false,
};
// We use both set_global and zrequire here for test isolation.
//
// We also introduce the run_test helper, which mostly just causes
// a line of output to go to the console. It does a little more than
// that, which we will see later.
const stream_data = zrequire("stream_data");
run_test("verify stream_data persists stream color", () => {
assert.equal(stream_data.get_sub_by_name("Denmark"), undefined);
stream_data.add_sub(denmark_stream);
const sub = stream_data.get_sub_by_name("Denmark");
assert.equal(sub.color, "blue");
});
// Hopefully the basic patterns for testing data-oriented modules
// are starting to become apparent. To reinforce that, we will present
// few more examples that also expose you to some of our core
// data objects. Also, we start testing some objects that have
// deeper dependencies.
const messages = {
isaac_to_denmark_stream: {
id: 400,
sender_id: isaac.user_id,
stream_id: denmark_stream.stream_id,
type: "stream",
flags: ["has_alert_word"],
topic: "copenhagen",
// note we don't have every field that a "real" message
// would have, and that can be fine
},
};
// We are going to test a core module called messages_store.js next.
// This is an example of a deep unit test, where our dependencies
// are easy to test. Start by requiring the dependencies:
const unread = zrequire("unread");
const stream_topic_history = zrequire("stream_topic_history");
const recent_topics = zrequire("recent_topics");
// And finally require the module that we will test directly:
const message_store = zrequire("message_store");
run_test("message_store", () => {
const in_message = {...messages.isaac_to_denmark_stream};
assert.equal(in_message.alerted, undefined);
message_store.set_message_booleans(in_message);
assert.equal(in_message.alerted, true);
// Let's add a message into our message_store via
// add_message_metadata.
assert.equal(message_store.get(in_message.id), undefined);
message_store.add_message_metadata(in_message);
const message = message_store.get(in_message.id);
assert.equal(message, in_message);
// There are more side effects.
const topic_names = stream_topic_history.get_recent_topic_names(denmark_stream.stream_id);
assert.deepEqual(topic_names, ["copenhagen"]);
});
// Tracking unread messages is a very fundamental part of the Zulip
// app, and we use the unread object to track unread messages.
run_test("unread", () => {
const stream_id = denmark_stream.stream_id;
const topic_name = "copenhagen";
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 0);
const in_message = {...messages.isaac_to_denmark_stream};
message_store.set_message_booleans(in_message);
unread.process_loaded_messages([in_message]);
assert.equal(unread.num_unread_for_topic(stream_id, topic_name), 1);
});
// In the Zulip app you can narrow your message stream by topic, by
// sender, by PM recipient, by search keywords, etc. We will discuss
// narrows more broadly, but first let's test out a core piece of
// code that makes things work.
// We use the second argument of zrequire to find the location of the
// Filter class.
const {Filter} = zrequire("../js/filter");
run_test("filter", () => {
const filter_terms = [
{operator: "stream", operand: "Denmark"},
{operator: "topic", operand: "copenhagen"},
];
const filter = new Filter(filter_terms);
const predicate = filter.predicate();
// We don't need full-fledged messages to test the gist of
// our filter. If there are details that are distracting from
// your test, you should not feel guilty about removing them.
assert.equal(predicate({type: "personal"}), false);
assert.equal(
predicate({
type: "stream",
stream_id: denmark_stream.stream_id,
topic: "does not match filter",
}),
false,
);
assert.equal(
predicate({
type: "stream",
stream_id: denmark_stream.stream_id,
topic: "copenhagen",
}),
true,
);
});
// We have a "narrow" abstraction that sits roughly on top of the
// "filter" abstraction. If you are in a narrow, we track the
// state with the narrow_state module.
const narrow_state = zrequire("narrow_state");
run_test("narrow_state", () => {
// As we often do, first make assertions about the starting
// state:
assert.equal(narrow_state.stream(), undefined);
// Now set the state.
const filter_terms = [
{operator: "stream", operand: "Denmark"},
{operator: "topic", operand: "copenhagen"},
];
const filter = new Filter(filter_terms);
narrow_state.set_current_filter(filter);
assert.equal(narrow_state.stream(), "Denmark");
assert.equal(narrow_state.topic(), "copenhagen");
});
/*
Let's step back and review what we've done so far.
We've used fairly straightforward testing techniques
to explore the following modules:
filter
message_store
narrow_state
people
stream_data
util
We haven't gone deep on any of these objects, but if
you are interested, all of these objects have test
suites that have 100% line coverage on the modules
that implement those objects. For example, you can look
at people.js in this directory for more tests on the
people object.
We can quickly review some testing concepts:
zrequire - bring in real code
set_global - create stubs
assert.equal - verify results
------
It's time to elaborate a bit on set_global.
First, some context. When we test certain objects,
we don't always want to test all the code they
depend on. Often we want to completely ignore the
interactions with certain objects; other times, we
will want to simulate some behavior of the objects
we depend on without bringing in all the implementation
details.
Also, our test runner runs many tests back to back.
Between each test we need to essentially reset the global
object back to its original state, so that state doesn't
leak between tests.
That's where set_global comes in. When you call
set_global, it updates the global namespace with an
object that you specify in the **test**, not real
code. Using set_global explicitly tells your test
reader what your testing boundaries are between "real"
code and "simulated" code. Finally, and perhaps most
importantly, the test runner will prevent this state
from leaking into the next test (and "zrequire" has
the same behavior attached to it as well).
------
Let's talk about our next steps.
An app is pretty useless without an actual data source.
One of the primary ways that a Zulip client gets data
is through events. (We also get data at page load, and
we can also ask the server for data, but that's not in
the scope of this conversation yet.)
Chat systems are dynamic. If an admin adds a user, or
if a user sends a messages, the server immediately sends
events to all clients so that they can reflect appropriate
changes in their UI. We're not going to discuss the entire
"full stack" mechanism here. Instead, we'll focus on
the client code, starting at the boundary where we
process events.
Let's just get started...
*/
const server_events_dispatch = zrequire("server_events_dispatch");
// We will use Bob in several tests.
const bob = {
email: "bob@example.com",
user_id: 33,
full_name: "Bob Roberts",
};
run_test("add_user_event", () => {
const event = {
type: "realm_user",
op: "add",
person: bob,
};
assert(!people.is_known_user_id(bob.user_id));
server_events_dispatch.dispatch_normal_event(event);
assert(people.is_known_user_id(bob.user_id));
});
/*
It's actually a little surprising that adding a user does
not have side effects beyond the people object. I guess
we don't immediately update the buddy list, but that's
because the buddy list gets updated on the next server
fetch.
Let's try an update next. To make this work, we will want
to put some stub objects into the global namespace (as
opposed to using the "real" code).
This is where we see a little extra benefit from the
run_test wrapper. It passes us in an object that we
can use to override data, and that works within the
scope of the function.
*/
const noop = () => {};
run_test("update_user_event", (override) => {
const new_bob = {
email: "bob@example.com",
user_id: bob.user_id,
full_name: "The Artist Formerly Known as Bob",
};
const event = {
type: "realm_user",
op: "update",
person: new_bob,
};
// We have to stub a few things:
override(activity, "redraw", noop);
override(message_live_update, "update_user_full_name", noop);
override(pm_list, "update_private_messages", noop);
override(settings_users, "update_user_data", noop);
// Dispatch the realm_user/update event, which will update
// data structures and have other side effects that are
// stubbed out above.
server_events_dispatch.dispatch_normal_event(event);
const user = people.get_by_user_id(bob.user_id);
// Verify that the code actually did its main job:
assert.equal(user.full_name, "The Artist Formerly Known as Bob");
});
/*
Our test verifies that the update events leads to a name change
inside the people object, but it obviously kind of glosses over
the other interactions.
We can go a step further and verify the sequence of operations
that happen during an event. This concept is called "mocking",
and you can find libraries to help do mocking. Here we will
just build our own lightweight mocking system, which is almost
trivially easy to do in a language like JavaScript.
*/
function test_helper() {
const events = [];
return {
redirect: (module, func_name) => {
module[func_name] = () => {
events.push([module, func_name]);
};
},
events,
};
}
/*
Our test_helper will allow us to redirect methods to an
events array, and we can then later verify that the sequence
of side effect is as predicted.
(Note that for now we don't simulate return values nor do we
inspect the arguments to these functions. We could easily
extend our helper to do more.)
The forthcoming example is a pretty extreme example, where we
are calling a pretty high level method that dispatches
a lot of its work out to other objects.
*/
const huddle_data = zrequire("huddle_data");
const message_events = zrequire("message_events");
run_test("insert_message", (override) => {
override(pm_list, "update_private_messages", noop);
const helper = test_helper();
recent_topics.is_visible = () => false;
const new_message = {
sender_id: isaac.user_id,
id: 1001,
content: "example content",
};
assert.equal(message_store.get(new_message.id), undefined);
helper.redirect(huddle_data, "process_loaded_messages");
helper.redirect(message_util, "add_new_messages");
helper.redirect(notifications, "received_messages");
helper.redirect(resize, "resize_page_components");
helper.redirect(stream_list, "update_streams_sidebar");
helper.redirect(unread_ops, "process_visible");
helper.redirect(unread_ui, "update_unread_counts");
narrow_state.reset_current_filter();
message_events.insert_new_messages([new_message]);
// Even though we have stubbed a *lot* of code, our
// tests can still verify the main "narrative" of how
// the code invokes various objects when a new message
// comes in:
assert.deepEqual(helper.events, [
[huddle_data, "process_loaded_messages"],
[message_util, "add_new_messages"],
[message_util, "add_new_messages"],
[unread_ui, "update_unread_counts"],
[resize, "resize_page_components"],
[unread_ops, "process_visible"],
[notifications, "received_messages"],
[stream_list, "update_streams_sidebar"],
]);
// Despite all of our stubbing/mocking, the call to
// insert_new_messages will have created a very important
// side effect that we can verify:
const inserted_message = message_store.get(new_message.id);
assert.equal(inserted_message.id, new_message.id);
assert.equal(inserted_message.content, "example content");
});
/*
The above example is a bit extreme. Generally we just
use the make_stub helper that comes with zjsunit.
We will step away from the actual Zulip codebase for a
second and just explore a contrived example.
*/
run_test("explore make_stub", (override) => {
// Let's say you have to test the following code.
const app = {
notify_server_of_deposit(deposit_amount) {
// simulate difficulty
throw new Error(`We cannot report this value without wifi: ${deposit_amount}`);
},
pop_up_fancy_confirmation_screen(deposit_amount, label) {
// simulate difficulty
throw new Error(`We cannot make a ${label} dialog for amount ${deposit_amount}`);
},
};
let balance = 40;
function deposit_paycheck(paycheck_amount) {
balance += paycheck_amount;
app.notify_server_of_deposit(paycheck_amount);
app.pop_up_fancy_confirmation_screen(paycheck_amount, "paycheck");
}
// Our deposit_paycheck should be easy to unit test for its
// core functionality (updating your balance), but the side
// effects get in the way. We have to override them to do
// the simple test here.
with_field(app, "notify_server_of_deposit", noop, () => {
with_field(app, "pop_up_fancy_confirmation_screen", noop, () => {
deposit_paycheck(10);
});
});
assert.equal(balance, 50);
// But we can do a little better here. Even though
// the two side-effect functions are awkward here, we can
// at least make sure they are invoked correctly. Let's
// use stubs.
const notify_stub = make_stub();
const pop_up_stub = make_stub();
// This time we'll just use our override helper to connect the
// stubs.
override(app, "notify_server_of_deposit", notify_stub.f);
override(app, "pop_up_fancy_confirmation_screen", pop_up_stub.f);
deposit_paycheck(25);
assert.equal(balance, 75);
assert.deepEqual(notify_stub.get_args("amount"), {amount: 25});
assert.deepEqual(pop_up_stub.get_args("amount", "label"), {amount: 25, label: "paycheck"});
});
/*
Let's continue to explore how we can test complex
interactions in the real code.
When a new message comes in, we update the three major
panes of the app:
* left sidebar - stream list
* middle pane - message view
* right sidebar - buddy list (aka "activity" list)
These are reflected by the following calls:
stream_list.update_streams_sidebar
message_util.add_new_messages
activity.process_loaded_messages
For now, though, let's focus on another side effect
of processing incoming messages:
unread_ops.process_visible
When new messages come in, they are often immediately
visible to users, so the app will communicate this
back to the server by calling unread_ops.process_visible.
In order to unit test this, we don't want to require
an actual server to be running. Instead, this example
will stub many of the "boundaries" to focus on the
core behavior.
*/
unmock_module("../../static/js/unread_ops");
unread_ops = zrequire("unread_ops");
run_test("unread_ops", (override) => {
(function set_up() {
const test_messages = [
{
id: 50,
type: "stream",
stream_id: denmark_stream.stream_id,
topic: "copenhagen",
unread: true,
},
];
// Make our test message appear to be unread, so that
// we then need to subsequently process them as read.
unread.process_loaded_messages(test_messages);
// Make our window appear visible.
override(notifications, "is_window_focused", () => true);
// Make our "test" message appear visible.
override(message_viewport, "bottom_message_visible", () => true);
// Make us not be in a narrow (somewhat hackily).
message_list.narrowed = undefined;
// Set current_message_list containing messages that
// can be marked read
set_global("current_msg_list", {
all_messages: () => test_messages,
can_mark_messages_read: () => true,
});
// Ignore these interactions for now:
message_list.all = {
show_message_as_read() {},
};
override(home_msg_list, "show_message_as_read", noop);
override(notifications, "close_notification", noop);
})();
// Set up a way to capture the options passed in to channel.post.
let channel_post_opts;
override(channel, "post", (opts) => {
channel_post_opts = opts;
});
// Let the real code skip over details related to active overlays.
override(overlays, "is_active", () => false);
// First, test for a message list that cannot read messages. Here
// we use with_field to limit the scope of our stub function.
with_field(
current_msg_list,
"can_mark_messages_read",
() => false,
() => {
unread_ops.process_visible();
},
);
assert.deepEqual(channel_post_opts, undefined);
with_field(
current_msg_list,
"can_mark_messages_read",
() => true,
() => {
// Do the main thing we're testing!
unread_ops.process_visible();
},
);
// The most important side effect of the above call is that
// we post info to the server. We can verify that the correct
// url and parameters are specified:
assert.deepEqual(channel_post_opts, {
url: "/json/messages/flags",
idempotent: true,
data: {messages: "[50]", op: "add", flag: "read"},
success: channel_post_opts.success,
});
});
/*
Next we will explore this function:
stream_list.update_streams_sidebar
*/
unmock_module("../../static/js/stream_list");
stream_list = zrequire("stream_list");
const social_stream = {
color: "red",
name: "Social",
stream_id: 102,
subscribed: true,
};
function make_jquery_helper() {
let appended_data;
$("#stream_filters").append = (data) => {
appended_data = data;
};
return {
verify_actions: () => {
const expected_data_to_append = [["stream stub"]];
assert.deepEqual(appended_data, expected_data_to_append);
},
};
}
function make_topic_list_helper(override) {
// We want to make sure that updating a stream_list
// closes the topic list and then rebuilds it. We don't
// care about the implementation details of topic_list for
// now, just that it is invoked properly.
override(topic_list, "active_stream_id", () => undefined);
override(topic_list, "get_stream_li", () => undefined);
let topic_list_cleared;
override(topic_list, "clear", () => {
topic_list_cleared = true;
});
let topic_list_closed;
override(topic_list, "close", () => {
topic_list_closed = true;
});
let topic_list_rebuilt;
override(topic_list, "rebuild", () => {
topic_list_rebuilt = true;
});
return {
verify_actions: () => {
assert(topic_list_cleared);
assert(topic_list_closed);
assert(topic_list_rebuilt);
},
};
}
function make_sidebar_helper() {
let updated_whether_active;
function row_widget() {
return {
update_whether_active: () => {
updated_whether_active = true;
},
get_li: () => ["stream stub"],
};
}
stream_list.stream_sidebar.set_row(social_stream.stream_id, row_widget());
return {
verify_actions: () => {
assert(updated_whether_active);
},
};
}
run_test("stream_list", (override) => {
stream_data.add_sub(social_stream);
const filter_terms = [
{operator: "stream", operand: "Social"},
{operator: "topic", operand: "lunch"},
];
const filter = new Filter(filter_terms);
override(narrow_state, "filter", () => filter);
override(narrow_state, "active", () => true);
const jquery_helper = make_jquery_helper();
const sidebar_helper = make_sidebar_helper();
const topic_list_helper = make_topic_list_helper(override);
// This is what we are testing!
with_field(stream_list, "stream_cursor", {redraw: noop}, () => {
stream_list.update_streams_sidebar();
});
jquery_helper.verify_actions();
sidebar_helper.verify_actions();
topic_list_helper.verify_actions();
});

View File

@ -93,25 +93,6 @@ exports.mock_esm = (request, obj = {}) => {
return exports.mock_cjs(request, {...obj, __esModule: true});
};
exports.unmock_module = (request) => {
const filename = Module._resolveFilename(
request,
require.cache[callsites()[1].getFileName()],
false,
);
if (!module_mocks.has(filename)) {
throw new Error(`Cannot unmock ${filename}, which was not mocked`);
}
if (!used_module_mocks.has(filename)) {
throw new Error(`You asked to mock ${filename} but we never saw it during compilation.`);
}
module_mocks.delete(filename);
used_module_mocks.delete(filename);
};
exports.set_global = function (name, val) {
if (val === null) {
throw new Error(`