snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# Continuous integration (CI)
The Zulip server uses [GitHub Actions](https://docs.github.com/en/actions) for continuous
integration. GitHub Actions runs frontend, backend and end-to-end production
installer tests. This page documents useful tools and tips when using
GitHub Actions and debugging issues with it.
## Goals
The overall goal of our CI is to avoid regressions and minimize the
total time spent debugging Zulip. We do that by trying to catch as
many possible future bugs as possible, while minimizing both latency
and false positives, both of which can waste a lot of developer time.
There are a few implications of this overall goal:
- If a test is failing nondeterministically in CI, we consider that to
be an urgent problem.
- If the tests become a lot slower, that is also an urgent problem.
- Everything we do in CI should also have a way to run it quickly
(under 1 minute, preferably under 3 seconds), in order to iterate fast
in development. Except when working on the CI configuration itself, a
developer should never have to repeatedly wait 10 minutes for a full CI
run to iteratively debug something.
## GitHub Actions
### Useful debugging tips and tools
- GitHub Actions stores timestamps for every line in the logs. They
are hidden by default; you can see them by toggling the
`Show timestamps` option in the menu on any job's log page. (You can
get this sort of timestamp in a development environment by piping
output to `ts`).
- GitHub Actions runs on every branch you push on your Zulip fork.
This is helpful when debugging something complicated.
- You can also ssh into a container to debug failures. SSHing into
the containers can be helpful, especially in rare cases where the
tests are passing in your computer but failing in the CI. There are
various
[Actions](https://github.com/marketplace?type=actions&query=debug+ssh)
available on GitHub Marketplace to help you SSH into a container. Use
whichever you find easiest to set up.
### Suites
We run multiple jobs during a GitHub Actions build to efficiently run
Zulip's various test suites, some of them multiple times because we
support multiple versions of the base OS. See the [Actions
tabs](https://github.com/zulip/zulip/actions) for full list of Actions
that we run.
Files which define GitHub workflows live in `.github/workflows` directory.
`zulip-ci.yml` is the main file where most of the tests are run.
`production-suite.yml` builds a Zulip release tarball, which is
then installed in a fresh container. Various Nagios and other
checks are run to confirm the installation worked.
`zulip-ci.yml` is designed to run our main test suites on all of our
supported platforms. Out of them, only one of them runs the frontend
tests, since `puppeteer` is slow and unlikely to catch issues that
depend on the version of the base OS and/or Python. Similarly, only a
(different) one runs the documentation tests.
Our code for running the tests in CI lives under `tools/ci`; but that
logic is mostly thin wrappers around [Zulip's test
suites](testing.md) or production installer.
The `Legacy OS` tests are designed to ensure we give good error
messages when trying to upgrade Zulip servers running on very old base
OS versions with EOL Python versions that Zulip no longer supports.
### Configuration
The remaining details in this section are primarily relevant for doing
development on our CI system and/or provisioning process.
The first key of the job section is `docker`. The docker key specifies
the image GitHub Actions should get from [Docker Hub][docker-hub] for running
the job. Once GitHub Actions fetches the image from Docker Hub, it will spin
up a docker container. See [images](#images) section to know more about
the images we use in GitHub Actions for testing.
[docker-hub]: https://hub.docker.com/r/zulip/ci
After booting the container from the configured image, GitHub Actions will
create the directory mentioned in `working_directory` and all the
steps will be run from here.
The `steps` section describes everything: fetching the Zulip
code, provisioning, fetching caught data, running tests and uploading
coverage reports. The steps with prefix `*` reference aliases, which
are defined in the `aliases` section at the top of the file.
### Images
GitHub Actions tests are run in containers that are spun off from the
images maintained by Zulip team. The Docker images can be generated by
running `tools/ci/build-docker-images`; see instructions at the top of
`tools/ci/Dockerfile` for more information.
### Performance optimizations
#### Caching
An important element of making GitHub Actions perform effectively is caching
between jobs the various caches that live under `/srv/` in a Zulip
development or production environment. In particular, we cache the
following:
- Python virtualenvs
- node_modules directories
This has a huge impact on the performance of running tests in GitHub Actions
CI; without these caches, the average test time would be several times
longer.
We have designed these caches carefully (they are also used in
production and the Zulip development environment) to ensure that each
is named by a hash of its dependencies and ubuntu distribution name,
so Zulip should always be using the same version of dependencies it
would have used had the cache not existed. In practice, bugs are
always possible, so be mindful of this possibility.
A consequence of this caching is that test jobs for branches which
modify `package.json`, `pyproject.toml`, and other key dependencies
will be significantly slower than normal, because they won't get to
benefit from the cache.

18
docs/testing/index.md Normal file
View File

@@ -0,0 +1,18 @@
# Code testing
```{toctree}
---
maxdepth: 3
---
testing
linters
testing-with-django
testing-with-node
testing-with-puppeteer
mypy
typescript
continuous-integration
manual-testing
philosophy
```

262
docs/testing/linters.md Normal file
View File

@@ -0,0 +1,262 @@
# Linters
A linter is a program that runs on the source code of software and
reports potentially problematic code to the user. Linters help enforce
coding guidelines, from minor details like whitespace formatting or
capitalization patterns for strings to preventing problematic coding
patterns that can lead to security bugs.
## Overview
Zulip does extensive linting of much of its source code, including
Python/JavaScript/TypeScript files, HTML templates (Django/Handlebars), CSS files,
JSON fixtures, Markdown documents, puppet manifests, and shell scripts.
For some files we simply check for small things like trailing whitespace,
but for other files, we are quite thorough about checking semantic
correctness.
Obviously, a large reason for linting code is to enforce the [Zulip
coding standards](../contributing/code-style.md). But we also use the linters to
prevent common coding errors.
We borrow some open source tools for much of our linting, and the links
below will direct you to the official documentation for these projects.
- [ESLint](https://eslint.org)
- [mypy](http://mypy-lang.org/)
- [Prettier](https://prettier.io/)
- [Puppet](https://puppet.com/) (puppet provides its own mechanism for
validating manifests)
- [Ruff](https://docs.astral.sh/ruff/)
- [stylelint](https://github.com/stylelint/stylelint)
Zulip also uses some home-grown code to perform tasks like validating
indentation in template files, enforcing coding standards that are unique
to Zulip, allowing certain errors from third party linters to pass through,
and exempting legacy files from lint checks.
## Running the linters
If you run `./tools/test-all`, it will automatically run the linters.
You can also run them individually or pass specific files:
```bash
./tools/lint
./tools/lint web/src/compose.ts
./tools/lint web/src/
```
`./tools/lint` has many useful options; you can read about them in its
internal documentation using `./tools/lint --help`. Of particular
note are:
- `--fix`: Several of our linters support automatically fixing basic
issues; this option will ask `tools/lint` to run those.
- `--verbose`: Provides detailed information on how to fix many common
linter errors not covered by `--fix`.
- `--skip` and `--only`: Only run certain linters.
- `-m`: Only check modified files.
Finally, you can rely on our continuous integration setup to run linters for you,
but it is good practice to run lint checks locally.
:::{important}
We provide a
[Git pre-commit hook](../git/zulip-tools.md#set-up-git-repo-script)
that can automatically run `tools/lint` on just the files that
changed (in a few 100ms) whenever you make a commit. This can save
you a lot of time, by automatically detecting linter errors as you
make them.
:::
**Note:** The linters only check files that Git tracks. Remember to `git add`
new files before running lint checks.
Our linting tools generally support the ability to lint files
individually--with some caveats--and those options will be described
later in this document.
## General considerations
Once you have read the [Zulip coding guidelines](../contributing/code-style.md), you can
be pretty confident that 99% of the code that you write will pass through
the linters fine, as long as you are thorough about keeping your code clean.
And, of course, for minor oversights, `lint` is your friend, not your foe.
Occasionally, our linters will complain about things that are more of
an artifact of the linter limitations than any actual problem with your
code. There is usually a mechanism where you can bypass the linter in
extreme cases, but often it can be a simple matter of writing your code
in a slightly different style to appease the linter. If you have
problems getting something to lint, you can submit an unfinished PR
and ask the reviewer to help you work through the lint problem, or you
can find other people in the [Zulip Community](https://zulip.com/development-community/)
to help you.
Also, bear in mind that 100% of the lint code is open source, so if you
find limitations in either the Zulip home-grown stuff or our third party
tools, feedback will be highly appreciated.
Finally, one way to clean up your code is to thoroughly exercise it
with tests. The [Zulip test documentation](testing.md)
describes our test system in detail.
## Lint checks
Most of our lint checks get performed by `./tools/lint`. These include the
following checks:
- Check Python code with Ruff.
- Check Python formatting with Ruff.
- Check JavaScript and TypeScript code with ESLint.
- Check CSS, JavaScript, TypeScript, and YAML formatting with Prettier.
- Check Python code for custom Zulip rules.
- Check non-Python code for custom Zulip rules.
- Check Puppet manifests with the Puppet validator.
- Check HTML templates for matching tags and indentations.
- Check CSS for parsability and formatting.
- Check JavaScript code for addClass calls.
- Running `mypy` to check static types in Python code. Our
[documentation on using mypy](mypy.md) covers mypy in
more detail.
- Running `tsc` to compile TypeScript code. Our [documentation on
TypeScript](typescript.md) covers TypeScript in more detail.
The rest of this document pertains to the checks that occur in `./tools/lint`.
## lint
Zulip has a script called `lint` that lives in our "tools" directory.
It is the workhorse of our linting system, although in some cases it
dispatches the heavy lifting to other components such as Ruff,
eslint, and other home grown tools.
You can find the source code [here](https://github.com/zulip/zulip/blob/main/tools/lint).
In order for our entire lint suite to run in a timely fashion, the `lint`
script performs several lint checks in parallel by forking out subprocesses.
Note that our project does custom regex-based checks on the code. The code for these
types of checks mostly lives [here](https://github.com/zulip/zulip/tree/main/tools/linter_lib).
### Special options
You can use the `-h` option for `lint` to see its usage. One particular
flag to take note of is the `--modified` flag, which enables you to only run
lint checks against files that are modified in your Git repo. Most of the
"sub-linters" respect this flag, but some will continue to process all the files.
Generally, a good workflow is to run with `--modified` when you are iterating on
the code, and then run without that option right before committing new code.
If you need to troubleshoot the linters, there is a `--verbose-timing`
option that can give you clues about which linters may be running
slow, for example.
### Lint checks
The next part of this document describes the lint checks that we apply to
various file types.
#### Generic source code checks
We check almost our entire codebase for trailing whitespace. Also, we
disallow tab (\t) characters in all but two files.
We also have custom regex-based checks that apply to specific file types.
For relatively minor files like Markdown files and JSON fixtures, this
is the extent of our checking.
Finally, we're checking line length in Python code (and hope to extend
this to other parts of the codebase soon). You can use
`#ignorelinelength` for special cases where a very long line makes
sense (e.g., a link in a comment to an extremely long URL).
#### Python code
Our Python code is formatted using
[Ruff](https://docs.astral.sh/ruff/) (using the options in the
`[tool.ruff]` section of `pyproject.toml`). The `lint` script enforces
this by running Ruff in check mode, or in write mode with `--fix`.
The bulk of our Python linting also gets outsourced to Ruff.
Zulip also has custom regex-based rules that it applies to Python code.
Look for `python_rules` in the source code for `lint`. Note that we
provide a mechanism to exclude certain lines of codes from these checks.
Often, it is simply the case that our regex approach is too crude to
correctly exonerate certain valid constructs. In other cases, the code
that we exempt may be deemed not worthwhile to fix.
#### JavaScript code
We check our JavaScript code in a few different ways:
- We run eslint.
- We check code formatting with Prettier.
- We perform custom Zulip regex checks on the code.
#### Puppet manifests
We use Puppet as our tool to manage configuration files, using
Puppet "manifests." To lint Puppet manifests, we use the "parser validate"
option of Puppet.
#### HTML templates
Zulip uses two HTML templating systems:
- [Django templates](https://docs.djangoproject.com/en/5.0/topics/templates/)
- [handlebars](https://handlebarsjs.com/)
Zulip has an internal tool that validates both types of templates for
correct indentation and matching tags. You can find the code here:
- driver: [check-templates](https://github.com/zulip/zulip/blob/main/tools/check-templates)
- engine: [lib/template_parser.py](https://github.com/zulip/zulip/blob/main/tools/lib/template_parser.py)
We exempt some legacy files from indentation checks, but we are hoping to
clean those files up eventually.
#### CSS
Zulip uses [stylelint](https://github.com/stylelint/stylelint) to lint
its CSS; see our
[configuration](https://github.com/zulip/zulip/blob/main/stylelint.config.js)
for the rules we currently enforce.
#### Shell scripts
Zulip uses [shellcheck](https://github.com/koalaman/shellcheck) to
lint our shell scripts. We recommend the
[shellcheck gallery of bad code][shellcheck-bad-code] as a resource on
how to not write bad shell.
[shellcheck-bad-code]: https://github.com/koalaman/shellcheck/blob/master/README.md#user-content-gallery-of-bad-code
#### Markdown, shell scripts, JSON fixtures
We mostly validate miscellaneous source files like `.json`, and `.md`
files for whitespace issues.
## Philosophy
If you want to help improve Zulip's system for linting, here are some
considerations.
#### Speed
We want our linters to be fast enough that most developers
will feel comfortable running them in a pre-commit hook, so we run
our linters in parallel and support incremental checks.
#### Accuracy
We try to catch as many common mistakes as possible, either via a
linter or an automated test.
#### Completeness
Our goal is to have most common style issues by caught by the linters, so new
contributors to the codebase can efficiently fix produce code with correct
style without needing to go back-and-forth with a reviewer.

View File

@@ -0,0 +1,563 @@
# Manual testing
As a general rule, we like to have automated tests for everything that
can be practically tested. However, there are certain types of bugs
that are best caught with old fashioned manual testing (also called
manual QA). Manual testing not only catches bugs, but it also helps
developers learn more about the system and think about the existing
semantics of a feature they're working on.
This doc assumes you know how to set up a local development server
and open the Zulip app in the browser. It also assumes a basic
knowledge of how to use Zulip.
## Basic stuff
When testing Zulip manually, here are things to focus on:
- The best bugs to catch are security/permissions bugs.
- Don't rush manual testing. Look for small details like
display glitches.
- Always test with multiple users (you can use incognito windows
to facilitate this).
- Always keep the inspector console open and watch for warnings
or errors.
- Be methodical about collecting information on bugs. (You will
eventually want to create tickets, but you may want to consolidate
your own notes before filing tickets.)
You generally want to test with Cordelia as the primary user,
and use Hamlet as her primary conversation partner. Use Iago
when you need to test administrative functions. Send messages
to Othello or Prospero if you want to verify things such as
Cordelia not being able to receive messages not intended for her.
The rest of this document groups tasks into basic areas of
functionality of the system. If you have multiple people testing
at once, you can divvy up QA tasks by these sections in the doc.
### Message view
We mostly test the message view as part of testing everything
else, but there are few things to specially test here.
Try using all the navigation hotkeys:
- Up/k
- Down/j
- PgUp/K
- PgDn/J/Spacebar
- End (or fn-right-arrow on OSX)
- also try scrolling aggressively with the mouse
Try narrowing from the message view:
- Hotkeys
- use a to go to Combined feed
- use s to narrow to a channel (select message first
and verify in sidebar)
- use S to narrow to the topic (and verify in sidebar)
- use v to navigate to direct messages
- Click on the recipient bar
- narrow to a channel
- narrow to a topic
- narrow to direct messages with one user
- narrow to a group direct message
- Click on the Zulip logo
- narrow to a topic
- click on the Zulip logo (and verify you're in the Recent conversations view)
### Messagebox
With messagebox we want to test mainly the functioning of all
all the keyboard shortcuts and click handlers.
Apart from that there are three views of a message box, we want
to test their appearance too:
- Message that includes sender
- Message without the sender
- "/me" message
Here's how we're going to test the message appearances:
- narrow to a new topic and send a message (this message will include sender)
- edit the message ("EDITED" label should appear beside sender name)
- send another message (will not include sender)
- edit the message ("EDITED" label should appear in the left column, where the avatar is)
- send a "/me" message (`/me test message`)
- message should appear alongside sender name
- edit the message ("EDITED" label should appear beside the message)
For all the three cases, we need to test the click handlers and
the hotkeys too:
- Sender popover:
- click on the avatar and sender name for messages which include sender
- press 'u' to open the sender popover for all messages
- Message reply:
- click on message to reply
- use 'r' or Return hotkey to reply
- use '>' to quote message
- use '@' to mention and reply
- Reactions:
- click on the reactions button to open menu
- use ':' to open the reactions menu
- react to a message
- Chevron
- click on chevron to open menu
- use 'i' to open chevron menu
- Message edit
- click on message edit/view source button
- use 'i' + Return to edit/view source message
- click on the 'copy and close' option in view source and verify positioning of 'Copied!' label.
- Star a message:
- click on the star button in the right column
- use 'Ctrl + S' to star a message
- Message length
- send a long message and see if 'Show more' button appears
- click on the 'more' or 'collapse' link
- use i to collapse/expand a message irrespective of message length
- use 'v' to show all images in the thread
- use 'M' to mute the thread
Play with the screen size to check if the messagebox appears
fine in different screens too.
### Message editing
With message editing we mainly want to exercise topic changes.
Here are some tasks:
- Do lots of editing
- send a message to the topic "original"
- edit the message content
- send two messages to the "original" channel
- start to edit a message but then cancel
- change the topic for the first message to "change1" (just
this message)
- narrow back to "original"
- send one more message to the channel
- change the topic for the last two messages to "change2"
- narrow back to "original"
- send two more messages to the channel
- edit the 2nd message on topic and change all messages to
"change3"
- Test UI entry points
- hit "i" then down arrow to edit with the popup
- use the popup using the mouse
- enter edit mode using the pencil icon
### Narrowing
Zulip uses the term "narrowing" to refer to opening different views
of your messages, whether by clicking on sidebar options, recipient
bars, or by using search. The main focus of these tasks should
be watching unread counts. Of course, you also want to see messages
show up in the message pane. And, finally, you should make sure
that no messages outside the narrow show up in Cordelia's view.
:::{important}
Make sure that Cordelia is subscribed to Verona but not
subscribed to Denmark; if not, you should use different channels
for your testing.
:::
When testing narrows, you want to have Hamlet send the same message
several times in a row, while cycling Cordelia through various narrows.
Here are the main tasks for Hamlet (and each message gets sent several
times):
- Send Cordelia/Othello a direct message.
- Send Cordelia a direct message.
- Send Othello a direct message.
- Post to Verona/foo.
- Post to Verona/bar.
- Post to Denmark/foo.
- Post to Denmark/foo and mention Cordelia.
For each of the above types of messages, you will want to cycle
through the following views for Cordelia (and have Hamlet send new
messages after each narrow):
- Go to Combined feed view.
- Go to Direct message feed view.
- Go to Direct messages w/Hamlet.
- Go to Direct messages w/Hamlet and Othello.
- Go to Verona view.
- Go to Verona/bar view.
- Go to Verona/foo view.
- Go to Denmark view.
- Go to Denmark/foo view.
There are 56 things to test here. If you can get into a rhythm
where you can test each case in about 30 seconds, then the whole
exercise is about 30 minutes, assuming no bugs.
### Composing messages
We have pretty good automated tests for our Markdown processor, so
manual testing is targeted more to other interactions. For composing
a message, pay attention to details like what is automatically
populated and where the focus is placed.
- Hotkeys
- use r to reply to a channel message
- use r to reply to a direct message
- use R to reply to the author of a direct message
- use R to reply to the author of a direct message channel
- use c to compose a channel message
- use x to compose a new direct message
- Buttons
- Narrow to a channel and click on "New topic"
- Narrow to "Direct message feed" and click on "New topic"
- Narrow to a channel and click on "New direct message"
- Narrow to "Direct message feed" and click on "New direct message"
- Topics
- Compose/send a message to a channel with no topic.
- Compose/send a message to a channel with a new topic.
- Compose/send a message to a channel with autocomplete.
- Compose/send a message to a channel manually typing an
existing topic.
- Formatting stuff
- Use the "A" icon to get Markdown help.
- Use the eyeball icon to show a preview and send from preview mode.
- Toggle in and out of preview before sending a message.
- Use @-mention to mention Hamlet (and send him a message).
- Use `#**devel**` syntax and send to Hamlet, then follow the link.
- Create a bulleted list.
- Use the emoji icon to find an emoji in the picker.
- Attachments
- Send a message with an attachment using the paperclip icon.
- Send a message with multiple attachments.
- Copy an image from the clipboard.
- Use drag/drop from the desktop to upload an image.
- Drafts
- Start composing a message then click outside the compose box.
- Use "restore drafts" to restore the draft.
- Start composing then use "Esc" to abort the message.
- Use "restore drafts" to restore the draft.
- Start composing a channel message and then abort using
the little "x" icon in the compose box.
- Click on "New direct message" and restore the draft. (You
should now be sending to a channel.)
- Click to send
- Turn off Enter-to-send.
- Send a two-paragraph message using Tab and Enter.
- Send a two-paragraph message using Ctrl-Enter or Cmd-Enter.
- Turn on Enter-to-send.
- Hit Enter to send.
### Popover menus
For this task you just want to go through all of our popover menus
and exercise them. The main nuance here is that you occasionally want
to click somewhere on the UI outside of an existing popover to see if
the popover menu is "too sticky." Also, occasionally actions will be
somewhat jarring; for example, if you mute a message in the current view,
then the message will disappear from the view.
Here are the things to test:
- Channel sidebar menus (click ellipsis when hovering over channel filters)
- Channel settings (just make sure it goes there)
- Narrow (and then have Hamlet send a message)
- Pin/unpin (do both)
- Compose (send a message to the channel)
- Mark as read (scroll back and then have Hamlet send you a message)
- Mute/unmute (do both)
- Unsubscribe (and then go to Channel settings in the gear menu to resubscribe)
- Choose custom color (play around with this)
- Topic sidebar menus (click ellipsis when hovering over topics)
- Narrow (and then have Hamlet send a message)
- Mute/unmute (try both)
- Mark as read (scroll back and then have Hamlet send you a message)
- Left-message-pane menus (click on person's name)
- Verify email
- Verify date message sent
- Send a direct message (make sure compose box is filled out ok)
- Narrow to direct messages with
- Narrow to direct messages sent by
- Right-pane-pane menus (click on chevron when hovering)
- use "i" hotkey to open the menu
- Edit a message you sent (using the down-arrow key to navigate the popup)
- View Source for somebody else's message (make sure
it's not editable)
- Reply (send a message)
- Collapse/uncollapse (try both)
- Mute/unmute (try both, watch left sidebar)
- Link to this conversation
- Buddy list menus (click ellipsis when hovering over users)
- Narrow to direct messages with
- Narrow to message sent by
- Compose a message to
### Sidebar filtering
This is a fairly quick task where we test the search filters on the left sidebar
and the buddy list. If Cordelia is not subscribed to Denmark, subscribe her to
that channel.
- Channels filtering
- Use "w" hotkey to open the search.
- Filter on "d".
- Pin/unpin Denmark.
- Clear filter.
- Use "A" and "D" hotkeys to cycle through the channels.
- Filter again and then click somewhere else.
- Buddy list filtering
- Use "q" hotkey to open the search.
- Filter for Hamlet, Prospero, Othello, etc.
- Log on Hamlet and log off Hamlet while filtering for Hamlet.
- Log on/log off Hamlet while filtering for Othello.
- Log on/log off Hamlet while not filtering at all.
- Filter again and then click somewhere else.
### Channel permissions
This is an important category to test, because we obviously do not
want to have bugs where people can read messages on channels they
should not have access to.
The general flow here is for Hamlet to create the channels and verify
that Cordelia has the correct visibility to them.
First, we start off with "positive" tests.
- Positive tests
- Have Hamlet create a public channel w/Cordelia subscribed and
have him post a message to the channel.
- Have Hamlet create a public channel without Cordelia and then...
- Have Hamlet post to the channel.
- Have Cordelia subscribe to the channel.
- Verify Cordelia can see the previous message.
- Have Cordelia post a message to the channel.
- Have Hamlet create a private channel with Cordelia
invited and test a two-way conversation between the two
users.
For negative tests, we want to dig a little deeper to find back
doors for Cordelia to access the channel. Here are some techniques
to try:
- Try to have her compose a message to the channel by
circumventing autocomplete.
- Try to have her narrow to the channel using channel:foo
in search.
- Go to channel settings and see if the channel shows up.
For public channels, it's ok for Cordelia to know the channel exists,
and she can subsequently subscribe. For private channels, she should
not even know they exist (until she's invited, of course).
- Negative tests
- Have Hamlet create a public channel without inviting Cordelia.
- Verify Cordelia can see the channel in her settings.
- Verify Cordelia can't compose a message to the channel.
- Verify that Cordelia sees nothing when Hamlet posts to
the channel.
- Have Hamlet create a public channel with Cordelia, but then
have Iago revoke her subscription using the admin page.
- Verify that the channel appears in Cordelia's left sidebar
and then goes away.
- Try to have Cordelia view the channel using a sneaky
search along the lines of `channel:foo`.
- Have Hamlet create a private channel without inviting Cordelia.
- Verify Cordelia can't compose a message to the channel.
### Search
The main task for testing search is to play around with search
suggestions (autocomplete). Once you select an option, verify the
message view is consistent with the search and that the left sidebar
reflects the current narrow. If a search comes up legitimately
empty, have Hamlet send a message that matches the search.
Here are searches you should be able to do with autocomplete:
- channel:design
- channel:Verona topic:Verona1
- channel:Verona keyword
- sent by me
- @-mentions
- starred messages
- messages sent by Hamlet
- direct messages with Hamlet
- direct messages with Hamlet matching keyword "foo"
There are some things you can try that don't come up in autocomplete:
- -channel:Verona (exclude Verona)
- channel:Verona channel:devel (should return no results)
Miscellaneous:
- Use the "/" hotkey to start a search.
- Use the "x" icon to clear a search.
- Use the "Esc" hotkey to clear a search.
### Channel settings
Test various UI entry points into channel settings:
- Use small gear menu in left sidebar, then filter to "devel".
- Use popover menu in left sidebar next to "devel".
- Use gear menu above buddy list and filter to "devel".
- Use gear menu and click on "devel."
- Use gear menu and then click on chevron menu next to "devel."
(I'm not sure why we still have the chevron at this writing.)
Create new public channel "public1" and add Hamlet:
- Type "public1" in the text box and then click "Create new channel."
- Select "People must be invited" and then verify you can't
select "Announce new channel in #[announcement channel]".
- Select "Anyone can join" again to make it be public.
- Check the checkbox for Hamlet.
- Hit the "Create" button.
Test subscribe/unsubscribe:
- Log in as Hamlet and go to his channel settings.
- As Cordelia, unsubscribe from "public1" using the checkmark in the
channel settings page.
- Verify that Hamlet sees that Cordelia has unsubscribed (and the
subscriber count should decrement).
- As Cordelia, resubscribe to "public1."
- Verify Hamlet sees that change.
As Cordelia, exercise different options in **Create Channel**
dialog by creating channels s1, s2, s3, etc.:
- s1: anyone can join, announce it, and add Hamlet using filter feature
- s2: people must be invited
- s3: anyone can join, don't announce
- s4: check all, then uncheck all, then invite only Hamlet
- s5: invite everybody but Hamlet
- s6:
- create the channel as public, but don't subscribe anybody initially
- then click on channel options to add Hamlet using "Add" button
Test per-channel options:
- Use "devel" channel and send a message to it
- Do mute and unmute, have Hamlet send messages
- Test notifications on/off, have Hamlet send messages
- Test pin and unpin, view left sidebar
- Change channel color, and then view the left sidebar and the All
messages view
- Verify channel subscriber counts in the main channel view
### User settings
You can modify per-user settings by choosing "Settings" in the gear menu.
Do these tasks as Cordelia.
- Your account
- Change full name (Hamlet should see the name change)
- Customize profile picture
- Deactivate account (and then log in as Iago to re-activate Cordelia)
- Preferences
- Right now, these unfortunately require reloads to take effect.
- Default language (change to Spanish)
- 24-hour time (and then test going back to AM/PM)
- Notifications
- Channel message
- turn off notifications at user level
- create a new channel
- have Hamlet send a message
- turn on notifications at user level
- create a new channel
- have Hamlet send a message
- then turn off notifications for that channel
- have Hamlet send another message
- Direct messages and @-mentions
- Test Desktop/Audible options
- You can ignore other stuff for now
- Bots/API key
- Create a bot with a generic avatar and send it a direct message
- Create a bot with a custom avatar and send it a direct message
- Change your API key
- Alert words
- Create an alert word
- Have Hamlet send you a message that includes the alert word
- Zulip labs
- Turn on auto-scroll to new messages (and have Hamlet send you one)
- Turn on/off "Enable desktop notifications for new channels" and test.
(We may eliminate this option soon.)
### Keyboard shortcuts
We mostly test keyboard shortcuts as part of other tasks.
Here are the tasks for this section:
- Use the "?" hotkey to open the keyboard help
- Proofread the dialog for typos.
- Close the dialog.
- Re-open the keyboard help using the gear menu.
- Find a hotkey that you don't frequently use and experiment with its
usage.
### Miscellaneous menu options
Make sure that these options launch appropriate help screens:
- Proofread and try a couple random options:
- Message formatting
- Search filters
- Make sure help launches in a separate browser tab:
- Desktop and mobile apps
- Integrations
- API documentation
### Inviting users/tutorial
Here are the tasks:
- Invite ignore@zulip.com using the link beneath the buddy list but
then don't take further action.
- Fully invite foo@zulip.com using the gear menu.
- Go to the development console to get the login link for foo@zulip.com.
- Go through the signup flow.
- Follow the tutorial.
- Use the gear menu to log out.
- Log back in as Cordelia (admittedly, this step doesn't really QA
much of our production code, since the login flow is customized for
the development environment).
### To be continued...
This document does not cover settings/admin options yet. The main
things to do when testing the settings system are:
- Verify that changes are synced to other users.
- Verify error messages appear if you do something wrong and look right.
- For organization settings, verify that they look right in read-only
mode (i.e. when not logged into an administrator account).

561
docs/testing/mypy.md Normal file
View File

@@ -0,0 +1,561 @@
# Python static type checker (mypy)
[mypy](http://mypy-lang.org/) is a compile-time static type checker
for Python, allowing optional, gradual typing of Python code. Zulip
was fully annotated with mypy's Python 2 syntax in 2016, before our
migration to Python 3 in late 2017. In 2018 and 2020, we migrated
essentially the entire codebase to the nice PEP 484 (Python 3 only)
and PEP 526 (Python 3.6) syntax for static types:
```python
user_dict: Dict[str, UserProfile] = {}
def get_user(email: str, realm: Realm) -> UserProfile:
... # Actual code of the function here
```
You can learn more about it at:
- The
[mypy cheat sheet for Python 3](https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html)
is the best resource for quickly understanding how to write the PEP
484 type annotations used by mypy correctly.
- The
[Python type annotation spec in PEP 484](https://www.python.org/dev/peps/pep-0484/).
- Our [blog post on being an early adopter of mypy][mypy-blog-post] from 2016.
- Our [best practices](#best-practices) section below.
The mypy type checker is run automatically as part of Zulip's Travis
CI testing process in the `backend` build.
[mypy-blog-post]: https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/
## Installing mypy
mypy is installed by default in the Zulip development environment.
## Running mypy on Zulip's code locally
To run mypy on Zulip's python code, you can run the command:
```bash
tools/run-mypy
```
Mypy outputs errors in the same style as a compiler would. For
example, if your code has a type error like this:
```python
foo = 1
foo = '1'
```
you'll get an error like this:
```console
test.py: note: In function "test":
test.py:200: error: Incompatible types in assignment (expression has type "str", variable has type "int")
```
## Mypy is there to find bugs in Zulip before they impact users
For the purposes of Zulip development, you can treat `mypy` like a
much more powerful linter that can catch a wide range of bugs. If,
after running `tools/run-mypy` on your Zulip branch, you get mypy
errors, it's important to get to the bottom of the issue, not just do
something quick to silence the warnings, before we merge the changes.
Possible explanations include:
- A bug in any new type annotations you added.
- A bug in the existing type annotations.
- A bug in Zulip!
- Some Zulip code is correct but confusingly reuses variables with
different types.
- A bug in mypy (though this is increasingly rare as mypy is now
fairly mature as a project).
Each explanation has its own solution, but in every case the result
should be solving the mypy warning in a way that makes the Zulip
codebase better. If you're having trouble, silence the warning with
an `Any` or `# type: ignore[code]` so you're not blocked waiting for help,
add a `# TODO: ` comment so it doesn't get forgotten in code review,
and ask for help in chat.zulip.org.
## Mypy stubs for third-party modules
For the Python standard library and some popular third-party modules,
the [typeshed project](https://github.com/python/typeshed) has
[stubs](https://github.com/python/mypy/wiki/Creating-Stubs-For-Python-Modules),
basically the equivalent of C header files defining the types used in
these Python APIs.
For other third-party modules that we call from Zulip, one either
needs to add an `ignore_missing_imports` entry in `pyproject.toml` in the
root of the project, letting `mypy` know that it's third-party code,
or add type stubs to the `stubs/` directory, which has type stubs that
mypy can use to type-check calls into that third-party module.
It's easy to add new stubs! Just read the docs, look at some of
existing examples to see how they work, and remember to remove the
`ignore_missing_imports` entry in `pyproject.toml` when you add them.
For any third-party modules that don't have stubs, `mypy` treats
everything in the third-party module as an `Any`, which is the right
model (one certainly wouldn't want to need stubs for everything just
to use `mypy`!), but means the code can't be fully type-checked.
## Working with types from django-stubs
For features that are difficult to be expressed with static type
annotations, type analysis is supplemented with mypy plugins. Zulip's
Python codebases uses the Django web framework, and such a plugin is
required in order for `mypy` to correctly infer the types of most code
interacting with Django model classes (i.e. code that accesses the
database).
We use the `mypy_django_plugin` plugin from the
[django-stubs](https://github.com/typeddjango/django-stubs) project,
which supports accurate type inference for classes like
`QuerySet`. For example, `Stream.objects.filter(realm=realm)` is
simple Django code to fetch all the channels in a realm. With this
plugin, mypy will correctly determine its type is `QuerySet[Stream]`,
aka a standard, lazily evaluated Django query object that can be
iterated through to access `Stream` objects, without the developer
needing to do an explicit annotation.
When declaring the types for functions that accept a `QuerySet`
object, you should always supply the model type that it accepts as the
first type parameter.
```python
def foo(user: QuerySet[UserProfile]) -> None:
...
```
For `.values_list`, supply the type of the column as the second type
parameter.
```python
def get_book_page_counts() -> QuerySet[Book, int]:
return Book.objects.filter().values_list("page_count", flat=True)
```
For `.values`, we prefer to define a `TypedDict` containing the
key-value pairs for the columns.
```python
class BookMetadata(TypedDict):
id: int
name: str
def get_book_meta_data(
book_ids: List[int],
) -> QuerySet[Book, BookMetadata]:
return Book.objects.filter(id__in=book_ids).values("name", "id")
```
When writing a helper function that returns the response from a test
client, it should be typed as `TestHttpResponse` instead of
`HttpResponse`. This type is only defined in the Django stubs, so it
has to be conditionally imported only when type
checking. Conventionally, we alias it as `TestHttpResponse`, which is
internally named `_MonkeyPatchedWSGIResponse` within django-stubs.
```python
from typing import TYPE_CHECKING
from zerver.lib.test_classes import ZulipTestCase
if TYPE_CHECKING:
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
class FooTestCase(ZulipTestCase):
def helper(self) -> "TestHttpResponse":
return self.client_get("/bar")
```
We sometimes encounter inaccurate type annotations in the Django
stubs project. We prefer to address these by [submitting a pull
request](https://github.com/typeddjango/django-stubs/pulls) to fix the
issue in the upstream project, just like we do with `typeshed` bugs.
## Using @overload to accurately describe variations
Sometimes, a function's type is most precisely expressed as a few
possibilities, and which possibility can be determined by looking at
the arguments. You can express that idea in a way mypy understands
using `@overload`. For example, `check_list` returns a `Validator`
function that verifies that an object is a list, raising an exception
if it isn't.
It supports being passed a `sub_validator`, which will verify that
each element in the list has a given type as well. One can express
the idea "If `sub_validator` validates that something is a `ResultT`,
`check_list(sub_validator)` validates that something is a
`List[ResultT]` as follows:
```python
@overload
def check_list(sub_validator: None, length: Optional[int]=None) -> Validator[List[object]]:
...
@overload
def check_list(sub_validator: Validator[ResultT],
length: Optional[int]=None) -> Validator[List[ResultT]]:
...
def check_list(sub_validator: Optional[Validator[ResultT]]=None,
length: Optional[int]=None) -> Validator[List[ResultT]]:
```
The first overload expresses the types for the case where no
`sub_validator` is passed, in which case all we know is that it
returns a `Validator[List[object]]`; whereas the second defines the
type logic for the case where we are passed a `sub_validator`.
**Warning:** Mypy only checks the body of an overloaded function
against the final signature and not against the more restrictive
`@overload` signatures. This allows some type errors to evade
detection by mypy:
```python
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> int: ... # oops
def f(x: Union[int, str]) -> Union[int, str]:
return x
x: int = f("three!!")
```
Due to this potential for unsafety, we discourage overloading unless
it's absolutely necessary. Consider writing multiple functions with
different names instead.
See the [mypy overloading documentation][mypy-overloads] for more details.
[mypy-overloads]: https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading
## Best practices
### When is a type annotation justified?
Usually in fully typed code, mypy will protect you from writing a type
annotation that isn't justified by the surrounding code. But when you
need to write annotations at the border between untyped and typed
code, keep in mind that **a type annotation should always represent a
guarantee,** not an aspiration. If you have validated that some value
is an `int`, it can go in an `int` annotated variable. If you are
going to validate it later, it should not. When in doubt, an `object`
annotation is always safe.
Mypy understands many Python constructs like `assert`, `if`,
`isinstance`, and logical operators, and uses them to automatically
narrow the type of validated objects in many cases.
```python
def f(x: object, y: Optional[str]) -> None:
if isinstance(x, int):
# Within this if block, mypy infers that x: int
print(x + 1)
assert y is not None
# After that assert statement, mypy infers that y: str
print(y.strip())
```
It won't be able do this narrowing if the validation is hidden behind
a function call, so sometimes it's helpful for a validation function
to return the type-narrowed value back to the caller even though the
caller already has it. (The validators in `zerver/lib/validator.py`
are examples of this pattern.)
### Avoid the `Any` type
Mypy provides the [`Any`
type](https://mypy.readthedocs.io/en/stable/dynamic_typing.html) for
interoperability with untyped code, but it is completely unchecked.
You can put an value of an arbitrary type into an expression of type
`Any`, and get an value of an arbitrary type out, and mypy will make
no effort to check that the input and output types match. So using
`Any` defeats the type safety that mypy would otherwise provide.
```python
x: Any = 5
y: str = x # oops
print(y.strip()) # runtime error
```
If you think you need to use `Any`, consider the following safer
alternatives first:
- To annotate a dictionary where different keys correspond to values
of different types, instead of writing `Dict[str, Any]`, try
declaring a
[**`dataclass`**](https://mypy.readthedocs.io/en/stable/additional_features.html#dataclasses)
or a
[**`TypedDict`**](https://mypy.readthedocs.io/en/stable/more_types.html#typeddict).
- If you're annotating a class or function that might be used with
different data types at different call sites, similar to the builtin
`List` type or the `sorted` function, [**generic
types**](https://mypy.readthedocs.io/en/stable/generics.html) with
`TypeVar` might be what you need.
- If you need to accept data of several specific possible types at a
single site, you may want a [**`Union`
type**](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#union-types).
`Union` is checked: before using `value: Union[str, int]` as a
`str`, mypy requires that you validate it with an
`instance(value, str)` test.
- If you really have no information about the type of a value, use the
**`object` type**. Since every type is a subtype of `object`, you
can correctly annotate any value as `object`. The [difference
between `Any` and
`object`](https://mypy.readthedocs.io/en/stable/dynamic_typing.html#any-vs-object)
is that mypy will check that you safely validate an `object` with
`isinstance` before using it in a way that expects a more specific
type.
- A common way for `Any` annotations to sneak into your code is the
interaction with untyped third-party libraries. Mypy treats any
value imported from an untyped library as annotated with `Any`, and
treats any type imported from an untyped library as equivalent to
`Any`. Consider providing real type annotations for the library by
[**writing a stub file**](#mypy-stubs-for-third-party-modules).
### Avoid `cast()`
The [`cast`
function](https://mypy.readthedocs.io/en/stable/type_narrowing.html#casts) lets you
provide an annotation that Mypy will not verify. Obviously, this is
completely unsafe in general.
```python
x = cast(str, 5) # oops
print(x.strip()) # runtime error
```
Instead of using `cast`:
- You can use a [variable
annotation](https://mypy.readthedocs.io/en/stable/type_inference_and_annotations.html#explicit-types-for-variables)
to be explicit or to disambiguate types that mypy can check but
cannot infer.
```python
l: List[int] = []
```
- You can use an [`isinstance`
test](https://mypy.readthedocs.io/en/stable/common_issues.html#complex-type-tests)
to safely verify that a value really has the type you expect.
### Avoid `# type: ignore` comments
Mypy allows you to ignore any type checking error with a
[`# type: ignore`
comment](https://mypy.readthedocs.io/en/stable/common_issues.html#spurious-errors-and-locally-silencing-the-checker),
but you should avoid this in the absence of a very good reason, such
as a bug in mypy itself. If there are no safe options for dealing
with the error, prefer an unchecked `cast`, since its unsafety is
somewhat more localized.
Our linter requires all `# type: ignore` comments to be [scoped to the
specific error
code](https://mypy.readthedocs.io/en/stable/error_codes.html) being
ignored, and followed by an explanation such as a link to a GitHub
issue.
### Avoid other unchecked constructs
- As mentioned
[above](#using-overload-to-accurately-describe-variations), we
**discourage writing overloaded functions** because their bodies are
not checked against the `@overload` signatures.
- **Avoid `Callable[..., T]`** (with literal ellipsis `...`), since
mypy cannot check the types of arguments passed to it. Provide the
specific argument types (`Callable[[int, str], T]`) in simple cases,
or use [callback
protocols](https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols)
in more complex cases.
### Use `Optional` and `None` correctly
The [`Optional`
type](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#built-in-types)
is for optional values, which are values that could be `None`. For
example, `Optional[int]` is equivalent to `Union[int, None]`.
The `Optional` type is **not for optional parameters** (unless they
are also optional values as above). This signature does not use the
`Optional` type:
```python
def func(flag: bool = False) -> str:
...
```
A collection such as `List` should only be `Optional` if `None` would
have a different meaning than the natural meaning of an empty
collection. For example:
- An include list where the default is to include everything should be
`Optional` with default `None`.
- An exclude list where the default is to exclude nothing should be
non-`Optional` with default `[]`.
Don't test an `Optional` value using truthiness (`if value:`,
`not value`, `value or default_value`), especially when the type might
have falsy values other than `None`.
```python
s: Optional[str]
if not s: # bad: are we checking for None or ""?
...
if s is None: # good
...
```
### Read-only types
The basic Python collections
[`List`](https://docs.python.org/3/library/typing.html#typing.List),
[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict),
and [`Set`](https://docs.python.org/3/library/typing.html#typing.Set)
are mutable, but it's confusing for a function to mutate a collection
that was passed to it as an argument, especially by accident. To
avoid this, prefer annotating function parameters with read-only
types:
- [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)
instead of `List`,
- [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)
instead of `Dict`,
- [`AbstractSet`](https://docs.python.org/3/library/typing.html#typing.AbstractSet)
instead of `Set`.
This is especially important for parameters with default arguments,
since a mutable default argument is confusingly shared between all
calls to the function.
```python
def f(items: Sequence[int] = []) -> int:
items.append(1) # mypy catches this mistake
return sum(items)
```
In some cases the more general
[`Collection`](https://docs.python.org/3/library/typing.html#typing.Collection)
or
[`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)
types might be appropriate. (But don’t use `Iterable` for a value
that might be iterated multiple times, since a one-use iterator is
`Iterable` too.)
For example, if a function gets called with either a `list` or a `QuerySet`,
and it only iterates the object once, the parameter can be typed as `Iterable`.
```python
def f(items: Iterable[Realm]) -> None:
for item in items:
...
realms_list: List[Realm] = [zulip, analytics]
realms_queryset: QuerySet[Realm] = Realm.objects.all()
f(realms_list) # OK
f(realms_queryset) # Also OK
```
A function's return type can be mutable if the return value is always
a freshly created collection, since the caller ends up with the only
reference to the value and can freely mutate it without risk of
confusion. But a read-only return type might be more appropriate for
a function that returns a reference to an existing collection.
Read-only types have the additional advantage of being [covariant
rather than
invariant](https://mypy.readthedocs.io/en/latest/common_issues.html#invariance-vs-covariance):
if `B` is a subtype of `A`, then `List[B]` may not be converted to
`List[A]`, but `Sequence[B]` may be converted to `Sequence[A]`.
### Typing decorators
A simple decorator that operates on functions of a fixed signature
works with no issues:
```python
def fancy(func: Callable[[str], str]) -> Callable[[int], str]:
def wrapped_func(n: int) -> str:
print("so fancy")
return func(str(n))
return wrapped_func
@fancy
def f(s: str) -> str:
return s
```
A decorator with an argument also works:
```python
def fancy(message: str) -> Callable[[Callable[[str], str]], Callable[[int], str]]:
def wrapper(func: Callable[[str], str]) -> Callable[[int], str]:
def wrapped_func(n: int) -> str:
print(message)
return func(str(n))
return wrapped_func
return wrapper
@fancy("so fancy")
def f(s: str) -> str:
return s
```
And a [generic
decorator](https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators)
that operates on functions of arbitrary signatures can be written
[with a `cast`](https://github.com/python/mypy/issues/1927) if the
output signature is always the same as the input signature:
```python
FuncT = TypeVar("FuncT", bound=Callable[..., object])
def fancy(func: FuncT) -> FuncT:
def wrapped_func(*args: object, **kwargs: object) -> object:
print("so fancy")
return func(*args, **kwargs)
return cast(FuncT, wrapped_func) # https://github.com/python/mypy/issues/1927
@fancy
def f(s: str) -> str:
return s
```
(A generic decorator with an argument would return
`Callable[[FuncT], FuncT]`.)
But Mypy doesn't yet support the advanced type annotations that would
be needed to correctly type generic signature-changing decorators,
such as `zerver.decorator.authenticated_json_view`, which passes an
extra argument to the inner function. For these decorators we must
unfortunately give up some type safety by falling back to
`Callable[..., T]`.
## Troubleshooting advice
All of our linters, including mypy, are designed to only check files
that have been added in Git (this is by design, since it means you
have untracked files in your Zulip checkout safely). So if you get a
`mypy` error like this after adding a new file that is referenced by
the existing codebase:
```console
mypy | zerver/models.py:1234: note: Import of 'zerver.lib.markdown_wrappers' ignored
mypy | zerver/models.py:1234: note: (Using --follow-imports=error, module not passed on command line)
```
The problem is that you need to `git add` the new file.

251
docs/testing/philosophy.md Normal file
View File

@@ -0,0 +1,251 @@
# Testing philosophy
Zulip's automated tests are a huge part of what makes the project able
to make progress. This page records some of the key principles behind
how we have designed our automated test suites.
Greg's talk at FlutterCon 2025,
["Writing better tests with less work"](https://www.youtube.com/watch?v=nf_XYpfsZNg),
is another fantastic resource, in video format.
## Effective testing allows us to move quickly
Zulip's engineering strategy can be summarized as "move quickly
without breaking things". Despite reviewing many code submissions
from new contributors without deep expertise in the code they are
changing, Zulip's maintainers spend most of the time they spend
integrating changes on product decisions and code
structure/readability questions, not on correctness, style, or
lower-level issues.
This is possible because we have spent years systematically investing
in testing, tooling, code structure, documentation, and development
practices to help ensure that our contributors write code that needs
relatively few changes before it can be merged. The testing element
of this is to have reliable, extensive, easily extended test suites
that cover most classes of bugs. Our testing systems have been
designed to minimize the time spent manually testing or otherwise
investigating whether changes are correct.
For example, our [infrastructure for testing
authentication](../development/authentication.md) allows using a mock
LDAP database in both automated tests and the development environment,
making it much easier now to refactor and improve this important part of
the product than it was when you needed to set up an LDAP server and
populate it with some test data in order to test LDAP authentication.
While not every part of Zulip has a great test suite, many components
do, and for those components, the tests mean that new contributors can
often make substantive changes and have them be
more or less correct by the time they share the
changes for code review. More importantly, it means that maintainers
save most of the time that would otherwise be spent verifying that the
changes are simply correct, and instead focus on making sure that the
codebase remains readable, well-structured, and well-tested.
## Test suite performance and reliability are critical
When automated test suites are slow or unreliable, developers will
avoid running them, and furthermore, avoid working on improving them
(both the system and individual tests). Because changes that make
tests slow or unreliable are often unintentional side effects of other
development, problems in this area tend to accumulate as a codebase
grows. As a result, barring focused effort to prevent this outcome,
any large software project will eventually have its test suite rot
into one that is slow, unreliable, untrustworthy, and hated. We aim
for Zulip to avoid that fate.
So we consider it essential to maintaining every automated test suite
setup in a way where it is fast and reliable (tests pass both in CI
and locally if there are no problems with the developer's changes).
Concretely, our performance goals are for the full backend suite
(`test-backend`) to complete in about a minute, and our full frontend
suite (`test-js-with-node`) to run in under 10 seconds.
It'd be a long blog post to summarize everything we do to help achieve
these goals, but a few techniques are worth highlighting:
- Our test suites are designed to not access the Internet, since the
Internet might be down or unreliable in the test environment. Where
outgoing HTTP requests are required to test something, we mock the
responses with libraries like `responses`.
- We carefully avoid the potential for contamination of data inside
services like PostgreSQL, Redis, and memcached from different tests.
- Every test case prepends a unique random prefix to all keys it
uses when accessing Redis and memcached.
- Every test case runs inside a database transaction, which is
aborted after the test completes. Each test process interacts
only with a fresh copy of a special template database used for
server tests that is destroyed after the process completes.
- We rigorously investigate non-deterministically failing tests as though
they were priority bugs in the product.
## Integration testing or unit testing?
Developers frequently ask whether they should write "integration
tests" or "unit tests". Our view is that tests should be written
against interfaces that you're already counting on keeping stable, or
already promising people you'll keep stable. In other words,
interfaces that you or other people are already counting on mostly not
changing except in compatible ways.
So writing tests for the Zulip server against Zulip's end-to-end API
is a great example of that: the API is something that people have
written lots of code against, which means all that code is counting on
the API generally continuing to work for the ways they're using it.
The same would be true even if the only users of the API were our own
project's clients like the mobile apps -- because there are a bunch of
already-installed copies of our mobile apps out there, and they're
counting on the API not suddenly changing incompatibly.
One big reason for this principle is that when you write tests against
an interface, those tests become a cost you pay any time you change
that interface -- you have to go update a bunch of tests.
So in a big codebase if you have a lot of "unit tests" that are for
tiny internal functions, then any time you refactor something and
change the internal interfaces -- even though you just made them up,
and they're completely internal to that codebase so there's nothing
that will break if you change them at will -- you have to go deal with
editing a bunch of tests to match the new interfaces. That's
especially a lot of work if you try to take the tests seriously,
because you have to think through whether the tests breaking are
telling you something you should actually listen to.
In some big codebases, this can lead to tests feeling a lot like
busywork... and it's because a lot of those tests really are
busywork. And that leads to developers not being committed to
maintaining and expanding the test suite in a thoughtful way.
But if your tests are written against an external API, and you make
some refactoring change and a bunch of tests break... now that's
telling you something very real! You can always edit the tests... but
the tests are stand-ins for real users and real code out there beyond
your reach, which will break the same way.
So you can still make the change... but you have to deal with figuring
out an appropriate migration or backwards-compatibility strategy for
all those real users out there. Updating the tests is one of the easy
parts. And those changes to the tests are a nice reminder to code
reviewers that you've changed an interface, and the reviewer should
think carefully about whether those interface changes will be a
problem for any existing clients and whether they're properly reflected
in any documentation for that interface.
Some examples of this philosophy:
- If you have a web service that's mainly an API, you want to write
your tests for that API.
- If you have a CLI program, you want to write your tests against the
CLI.
- If you have a compiler, an interpreter, etc., you want essentially
all your tests to be example programs, with a bit of metadata for
things like "should give an error at this line" or "should build and
run, and produce this output".
In the Zulip context:
- Zulip uses the same API for our web app as for our mobile clients and
third-party API clients, and most of our server tests are written
against the Zulip API.
- The tests for Zulip's incoming webhooks work by sending actual
payloads captured from the real third-party service to the webhook
endpoints, and verifies that the webhook produces the expected Zulip
message as output, to test the actual interface.
So, to summarize our approach to integration vs. unit testing:
- While we aim to achieve test coverage of every significant code path
in the Zulip server, which is commonly associated with unit testing,
most of our tests are integration tests in the sense of sending a
complete HTTP API query to the Zulip server and checking that the
HTTP response and the internal state of the server following the request
are both correct.
- Following the end-to-end principle in system design, where possible
we write tests that execute a complete flow (e.g., registering a new
Zulip account) rather than testing the implementations of individual
functions.
- We invest in the performance of Zulip in part to give users a great
experience, but just as much to make our test suite fast enough
that we can write our tests this way.
## Avoid duplicating code with security impact
Developing secure software with few security bugs is extremely
difficult. An important part of our strategy for avoiding security
logic bugs is to design patterns for how all of our code that
processes untrusted user input can be well tested without either
writing (and reviewing!) endless tests or requiring every developer to
be good at thinking about security corner cases.
Our strategy for this is to write a small number of carefully-designed
functions like `access_stream_by_id` that we test carefully, and then
use linting and other coding conventions to require that all access to
data from code paths that might share that data with users be mediated
through those functions. So rather than having each view function do
it own security checks for whether the user can access a given channel,
and needing to test each of those copies of the logic, we only need to
do that work once for each major type of data structure and level of
access.
These `access_*_by_*` functions are written in a special style, with each
conditional on its own line (so our test coverage tooling helps verify
that every case is tested), detailed comments, and carefully
considered error-handling to avoid leaking information such as whether
the channel ID requested exists or not.
We will typically also write tests for a given view verifying that it
provides the appropriate errors when improper access is attempted, but
these tests are defense in depth; the main way we prevent invalid
access to channels is not offering developers a way to get a `Stream`
object in server code except as mediated through these security check
functions.
## Share test setup code
It's very common to need to write tests for permission checking or
error handling code. When doing this, it's best to share the test
setup code between success and failure tests.
For example, when testing a function that returns a boolean (as
opposed to an exception with a specific error messages), it's often
better to write a single test function, `test_foo`, that calls the
function several times and verifies its output for each value of the
test conditions.
The benefit of this strategy is that you guarantee that the test setup
only differs as intended: Done well, it helps avoid the otherwise
extremely common failure mode where a `test_foo_failure` test passes
for the wrong reason. (e.g., the action fails not because of the
permission check, but because a required HTTP parameter was only added
to an adjacent `test_foo_success`).
## What isn't tested probably doesn't work
Even the very best programmers make mistakes constantly. Further, it's
impossible to do large codebase refactors (which are important to
having a readable, happy, correct codebase) if doing so has a high
risk of creating subtle bugs.
As a result, it's important to test every change. For business logic,
the best option is usually a high-quality automated test, that is
designed to be robust to future refactoring.
But for some things, like documentation and CSS, the only way to test
is to view the element in a browser and try things that might not
work. What to test will vary with what is likely to break. For
example, after a significant change to Zulip's Markdown documentation,
if you haven't verified every special bit of formatting visually and
clicked every new link, there's a good chance that you've introduced a
bug.
Manual testing not only catches bugs, but it also helps developers
learn more about the system and think about the existing semantics of
a feature they're working on.
When submitting a pull request that affects UI, it's extremely helpful
to show a screencast of your feature working, because that allows a
reviewer to save time that would otherwise be spent manually testing
your changes.

View File

@@ -0,0 +1,499 @@
# Backend Django tests
## Overview
Zulip uses the Django framework for its Python backend. We
use the testing framework from
[django.test](https://docs.djangoproject.com/en/5.0/topics/testing/)
to test our code. We have thousands of automated tests that verify that
our backend works as expected.
All changes to the Zulip backend code should be supported by tests. We
enforce our testing culture during code review, and we also use
coverage tools to measure how well we test our code. We mostly use
tests to prevent regressions in our code, but the tests can have
ancillary benefits such as documenting interfaces and influencing
the design of our software.
If you have worked on other Django projects that use unit testing, you
will probably find familiar patterns in Zulip's code. This document
describes how to write tests for the Zulip backend, with a particular
emphasis on areas where we have either wrapped Django's test framework
or just done things that are kind of unique in Zulip.
## Running tests
Our tests live in `zerver/tests/`. You can run them with
`./tools/test-backend`. The tests run in parallel using multiple
threads in your development environment, and can finish in under 30s
on a fast machine. When you are in iterative mode, you can run
individual tests or individual modules, following the dotted.test.name
convention below:
```bash
cd /srv/zulip
./tools/test-backend zerver.tests.test_queue_worker.WorkerTest
```
There are many command line options for running Zulip tests, such
as a `--verbose` option. The
best way to learn the options is to use the online help:
```bash
./tools/test-backend --help
```
We also have ways to instrument our tests for finding code coverage,
URL coverage, and slow tests. Use the `-h` option to discover these
features. We also have a `--profile` option to facilitate profiling
tests.
By default, `test-backend` will run all requested tests, and report
all failures at the end. You can configure it to stop after the first
error with the `--stop` option (or `-x`).
Another useful option is `--rerun`, which will rerun just the tests
that failed in the last test run.
**Webhook integrations**. For performance, `test-backend` with no
arguments will not run webhook integration tests (`zerver/webhooks/`),
which would otherwise account for about 25% of the total runtime.
When working on webhooks, we recommend instead running
`test-backend zerver/webhooks` manually (or better, the direction for
the specific webhooks you're working on). And of course our CI is
configured to always use `test-backend --include-webhooks` and run all
of the tests.
## Writing tests
Before you write your first tests of Zulip, it is worthwhile to read
the rest of this document.
To get a hang of commonly used testing techniques, read
[zerver/tests/test_example.py](https://github.com/zulip/zulip/blob/main/zerver/tests/test_example.py).
You can also read some of the existing tests in `zerver/tests`
to get a feel for other patterns we use.
A good practice is to get a "failing test" before you start to implement
your feature. First, it is a useful exercise to understand what needs to happen
in your tests before you write the code, as it can help drive out simple
design or help you make incremental progress on a large feature. Second,
you want to avoid introducing tests that give false positives. Ensuring
that a test fails before you implement the feature ensures that if somebody
accidentally regresses the feature in the future, the test will catch
the regression.
Another important files to skim are
[zerver/lib/test_helpers.py](https://github.com/zulip/zulip/blob/main/zerver/lib/test_helpers.py),
which contains test helpers.
[zerver/lib/test_classes.py](https://github.com/zulip/zulip/blob/main/zerver/lib/test_classes.py),
which contains our `ZulipTestCase` and `WebhookTestCase` classes.
### Setting up data for tests
All tests start with the same fixture data. (The tests themselves
update the database, but they do so inside a transaction that gets
rolled back after each of the tests complete. For more details on how the
fixture data gets set up, refer to `tools/setup/generate-fixtures`.)
The fixture data includes a few users that are named after
Shakesepeare characters, and they are part of the "zulip.com" realm.
Generally, you will also do some explicit data setup of your own. Here
are a couple useful methods in ZulipTestCase:
- make_stream
- subscribe
- unsubscribe
- send_stream_message
- send_personal_message
More typically, you will use methods directly from the backend code.
(This ensures more end-to-end testing, and avoids false positives from
tests that might not consider ancillary parts of data setup that could
influence tests results.)
Here are some example action methods that tests may use for data setup:
- check_send_message
- do_change_user_role
- do_create_user
- do_make_stream_private
### Testing code that accesses the filesystem
Some tests need to access the filesystem (e.g., `test_upload.py` tests
for `LocalUploadBackend` and the data import tests). Doing
this correctly requires care to avoid problems like:
- Leaking files after every test (which are clutter and can eventually
run the development environment out of disk) or
- Interacting with other parallel processes of this `test-backend` run
(or another `test-backend` run), or with later tests run by this
process.
To avoid these problems, you can do the following:
- Use a subdirectory of `settings.TEST_WORKER_DIR`; this is a
subdirectory of `/var/<uuid>/test-backend` that is unique to the
test worker thread and will be automatically deleted when the
relevant `test-backend` process finishes.
- Delete any files created by the test in the test class's `tearDown`
method (which runs even if the test fails); this is valuable to
avoid conflicts with other tests run later by the same test process.
Our common testing infrastructure handles some of this for you,
e.g., it replaces `settings.LOCAL_UPLOADS_DIR` for each test process
with a unique path under `/var/<uuid>/test-backend`. And
`UploadSerializeMixin` manages some of the cleanup work for
`test_upload.py`.
### Testing with mocks
This section is a beginner's guide to mocking with Python's
`unittest.mock` library. It will give you answers to the most common
questions around mocking, and a selection of commonly used mocking
techniques.
#### What is mocking?
When writing tests, _mocks allow you to replace methods or objects with fake entities
suiting your testing requirements_. Once an object is mocked, **its original code does not
get executed anymore**.
Rather, you can think of a mocked object as an initially empty shell:
Calling it won't do anything, but you can fill your shell with custom code, return values, etc.
Additionally, you can observe any calls made to your mocked object.
#### Why is mocking useful?
When writing tests, it often occurs that you make calls to functions
taking complex arguments. Creating a real instance of such an argument
would require the use of various different libraries, a lot of
boilerplate code, etc. Another scenario is that the tested code
accesses files or objects that don't exist at testing time. Finally,
it is good practice to keep tests independent from others. Mocks help
you to isolate test cases by simulating objects and methods irrelevant
to a test's goal.
In all of these cases, you can "mock out" the function calls / objects
and replace them with fake instances that only implement a limited
interface. On top of that, these fake instances can be easily
analyzed.
Say you have a module `greetings` defining the following functions:
```python
def fetch_database(key: str) -> str:
# ...
# Do some look-ups in a database
return data
def greet(name_key: str) -> str:
name = fetch_database(name_key)
return "Hello" + name
```
- You want to test `greet()`.
- In your test, you want to call `greet("Mario")` and verify that it returns the correct greeting:
```python
from greetings import greet
def test_greet() -> str:
greeting = greet("Mario")
assert greeting == "Hello Mr. Mario Mario"
```
-> **You have a problem**: `greet()` calls `fetch_database()`. `fetch_database()` does some look-ups in
a database. _You haven't created that database for your tests, so your test would fail, even though
the code is correct._
- Luckily, you know that `fetch_database("Mario")` should return "Mr. Mario Mario".
- _Hint_: Sometimes, you might not know the exact return value, but one that is equally valid and works
with the rest of the code. In that case, just use this one.
-> **Solution**: You mock `fetch_database()`. This is also referred to as "mocking out" `fetch_database()`.
```python
from unittest.mock import patch
def test_greet() -> None:
# Mock `fetch_database()` with an object that acts like a shell: It still accepts calls like `fetch_database()`,
# but doesn't do any database lookup. We "fill" the shell with a return value; This value will be returned on every
# call to `fetch_database()`.
with patch("greetings.fetch_database", return_value="Mr. Mario Mario"):
greeting = greetings.greet("Mario")
assert greeting == "Hello Mr. Mario Mario"
```
That's all. Note that **this mock is suitable for testing `greet()`, but not for testing `fetch_database()`**.
More generally, you should only mock those functions you explicitly don't want to test.
#### How does mocking work under the hood?
Since Python 3.3, the standard mocking library is `unittest.mock`. `unittest.mock` implements the basic mocking class `Mock`.
It also implements `MagicMock`, which is the same as `Mock`, but contains many default magic methods (in Python,
those are the ones starting with a dunder `__`). From the docs:
> In most of these examples the Mock and MagicMock classes are interchangeable. As the MagicMock is the more capable class
> it makes a sensible one to use by default.
`Mock` itself is a class that principally accepts and records any and all calls. A piece of code like
```python
from unittest import mock
foo = mock.Mock()
foo.bar('quux')
foo.baz
foo.qux = 42
```
is _not_ going to throw any errors. Our mock silently accepts all these calls and records them.
`Mock` also implements methods for us to access and assert its records, e.g.,
```python
foo.bar.assert_called_with('quux')
```
Finally, `unittest.mock` also provides a method to mock objects only within a scope: `patch()`. We can use `patch()` either
as a decorator or as a context manager. In both cases, the mock created by `patch()` will apply for the scope of the decorator /
context manager. `patch()` takes only one required argument `target`. `target` is a string in dot notation that _refers to
the name of the object you want to mock_. It will then assign a `MagicMock()` to that object.
As an example, look at the following code:
```python
from unittest import mock
from os import urandom
with mock.patch('__main__.urandom', return_value=42):
print(urandom(1))
print(urandom(1)) # No matter what value we plug in for urandom, it will always return 42.
print(urandom(1)) # We exited the context manager, so the mock doesn't apply anymore. Will return a random byte.
```
_Note that calling `mock.patch('os.urandom', return_value=42)` wouldn't work here_: `os.urandom` would be the name of our patched
object. However, we imported `urandom` with `from os import urandom`; hence, we bound the `urandom` name to our current module
`__main__`.
On the other hand, if we had used `import os.urandom`, we would need to call `mock.patch('os.urandom', return_value=42)` instead.
#### Boilerplate code
- Including the Python mocking library:
```python
from unittest import mock
```
- Mocking a class with a context manager:
```python
with mock.patch('module.ClassName', foo=42, return_value='I am a mock') as my_mock:
# In here, 'module.ClassName' is mocked with a MagicMock() object my_mock.
# my_mock has an attribute named foo with the value 42.
# var = module.ClassName() will assign 'I am a mock' to var.
```
- Mocking a class with a decorator:
```python
@mock.patch('module.ClassName', foo=42, return_value='I am a mock')
def my_function(my_mock):
# ...
# In here, 'module.ClassName' will behave as in the previous example.
```
- Mocking a class attribute:
```python
with mock.patch.object(module.ClassName, 'class_method', return_value=42)
# In here, 'module.ClassName' has the same properties as before, except for 'class_method'
# Calling module.ClassName.class_method() will now return 42.
```
Note the missing quotes around module.ClassName in the patch.object() call.
#### Zulip mocking practices
For mocking we generally use the "mock" library and use `mock.patch` as
a context manager or decorator. We also take advantage of some context managers
from Django as well as our own custom helpers. Here is an example:
```python
with self.settings(RATE_LIMITING=True):
with mock.patch('zerver.decorator.rate_limit_user') as rate_limit_mock:
api_result = my_webhook(request)
self.assertTrue(rate_limit_mock.called)
```
Follow [this link](../subsystems/settings.md#testing-non-default-settings) for more
information on the "settings" context manager.
Zulip has several features, like outgoing webhooks or social
authentication, that made outgoing HTTP requests to external
servers. We test those features using the excellent
[responses](https://pypi.org/project/responses/) library, which has a
nice interface for mocking `requests` calls to return whatever HTTP
response from the external server we need for the test. you can find
examples with `git grep responses.add`. Zulip's own `HostRequestMock`
class should be used only for low-level tests for code that expects to
receive Django HttpRequest object.
## Zulip testing philosophy
If there is one word to describe Zulip's philosophy for writing tests,
it is probably "flexible." (Hopefully "thorough" goes without saying.)
When in doubt, unless speed concerns are prohibitive,
you usually want your tests to be somewhat end-to-end, particularly
for testing endpoints.
These are some of the testing strategies that you will see in the Zulip
test suite...
### Endpoint tests
We strive to test all of our URL endpoints. The vast majority of Zulip
endpoints support a JSON interface. Regardless of the interface, an
endpoint test generally follows this pattern:
- Set up the data.
- Log in with `self.login()` or set up an API key.
- Use a Zulip test helper to hit the endpoint.
- Assert that the result was either a success or failure.
- Check the data that comes back from the endpoint.
Generally, if you are doing endpoint tests, you will want to create a
test class that is a subclass of `ZulipTestCase`, which will provide
you helper methods like the following:
- api_auth
- assert_json_error
- assert_json_success
- client_get
- client_post
- get_api_key
- get_streams
- login
- send_message
### Library tests
For certain Zulip library functions, especially the ones that are
not intrinsically tied to Django, we use a classic unit testing
approach of calling the function and inspecting the results.
For these types of tests, you will often use methods like
`self.assertEqual()`, `self.assertTrue()`, etc., which come with
[unittest](https://docs.python.org/3/library/unittest.html#unittest.TestCase)
via Django.
### Fixture-driven tests
Particularly for testing Zulip's integrations with third party systems,
we strive to have a highly data-driven approach to testing. To give a
specific example, when we test our GitHub integration, the test code
reads a bunch of sample inputs from a JSON fixture file, feeds them
to our GitHub integration code, and then verifies the output against
expected values from the same JSON fixture file.
Our fixtures live in `zerver/tests/fixtures`.
### Mocks and stubs
We use mocks and stubs for all the typical reasons:
- to more precisely test the target code
- to stub out calls to third-party services
- to make it so that you can [run the Zulip tests on the airplane without wifi][no-internet]
[no-internet]: testing.md#internet-access-inside-test-suites
A detailed description of mocks, along with useful coded snippets, can be found in the section
[Testing with mocks](#testing-with-mocks).
### Template tests
In [zerver/tests/test_templates.py](https://github.com/zulip/zulip/blob/main/zerver/tests/test_templates.py)
we have a test that renders all of our backend templates with
a "dummy" context, to make sure the templates don't have obvious
errors. (These tests won't catch all types of errors; they are
just a first line of defense.)
### SQL performance tests
A common class of bug with Django systems is to handle bulk data in
an inefficient way, where the backend populates objects for join tables
with a series of individual queries that give O(N) latency. (The
remedy is often just to call `select_related()`, but sometimes it
requires a more subtle restructuring of the code.)
We try to prevent these bugs in our tests by using a context manager
called `queries_captured()` that captures the SQL queries used by
the backend during a particular operation. We make assertions about
those queries, often simply by using the `assert_database_query_count`
that checks the number of queries.
### Event-based tests
The Zulip backend has a mechanism where it will fetch initial data
for a client from the database, and then it will subsequently apply
some queued up events to that data to the data structure before notifying
the client. The `BaseAction.do_test()` helper helps tests
verify that the application of those events via apply_events() produces
the same data structure as performing an action that generates said event.
This is a bit esoteric, but if you read the tests, you will see some of
the patterns. You can also learn more about our event system in the
[new feature tutorial](../tutorials/new-feature-tutorial.md#handle-database-interactions).
### Negative tests
It is important to verify error handling paths for endpoints, particularly
situations where we need to ensure that we don't return results to clients
with improper authentication or with limited authorization. A typical test
will call the endpoint with either a non-logged in client, an invalid API
key, or missing input fields. Then the test will call `assert_json_error()`
to verify that the endpoint is properly failing.
## Testing considerations
Here are some things to consider when writing new tests:
- **Duplication** We try to avoid excessive duplication in tests.
If you have several tests repeating the same type of test setup,
consider making a setUp() method or a test helper.
- **Network independence** Our tests should still work if you don't
have an internet connection. For third party clients, you can simulate
their behavior using fixture data. For third party servers, you can
typically simulate their behavior using mocks.
- **Coverage** We have 100% line coverage on several of our backend
modules. You can use the `--coverage` option to generate coverage
reports, and new code should have 100% coverage, which generally
requires testing not only the "happy path" but also error handling
code and edge cases. It will generate a nice HTML report that you can
view right from your browser (the tool prints the URL where the report
is exposed in your development environment).
The HTML report also displays which tests executed each line, which
can be handy for finding existing tests for a code path you're
working on.
- **Console output** A properly written test should print nothing to
the console; use `with self.assertLogs` to capture and verify any
logging output. Note that we reconfigure various loggers in
`zproject/test_extra_settings.py` where the output is unlikely to be
interesting when running our test suite.
`test-backend --ban-console-output` checks for stray print statements.
Note that `test-backend --coverage` will assert that
various specific files in the project have 100% test coverage and
throw an error if their coverage has fallen. One of our project goals
is to expand that checking to ever-larger parts of the codebase.

View File

@@ -0,0 +1,298 @@
# 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.
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

View File

@@ -0,0 +1,167 @@
# Web frontend black-box Puppeteer tests
While our [node test suite](testing-with-node.md) is the
preferred way to test most frontend code because they are easy to
write and maintain, some code is best tested in a real browser, either
because of navigation (e.g., login) or because we want to verify the
interaction between Zulip logic and browser behavior (e.g., copy/paste,
keyboard shortcuts, etc.).
## Running tests
You can run this test suite as follows:
```bash
tools/test-js-with-puppeteer
```
See `tools/test-js-with-puppeteer --help` for useful options,
especially running specific subsets of the tests to save time when
debugging.
The test files live in `web/e2e-tests` and make use
of various useful helper functions defined in
`web/e2e-tests/lib/common.ts`.
## How Puppeteer tests work
The Puppeteer tests use a real Chromium browser (powered by
[puppeteer](https://github.com/puppeteer/puppeteer)), connected to a
real Zulip development server. These are black-box tests: Steps in a
Puppeteer test are largely things one might do as a user of the Zulip
web app, like "Type this key", "Wait until this HTML element
appears/disappears", or "Click on this HTML element".
For example, this function might test the `x` keyboard shortcut to
open the compose box for a new direct message:
```js
async function test_private_message_compose_shortcut(page) {
await page.keyboard.press("KeyX");
await page.waitForSelector("#private_message_recipient", {visible: true});
await common.pm_recipient.expect(page, "");
await close_compose_box(page);
}
```
The test function presses the `x` key, waits for the
`#private_message_recipient` input element to appear, verifies its
content is empty, and then closes the compose box. The
`waitForSelector` step here (and in most tests) is critical; tests
that don't wait properly often fail nonderministically, because the
test will work or not depending on whether the browser updates the UI
before or after executing the next step in the test.
Black-box tests are fantastic for ensuring the overall health of the
project, but are also slow, costly to maintain, and require care to
avoid nondeterministic failures, so we usually prefer to write a Node
test instead when both are options.
They also can be a bit tricky to understand for contributors not
familiar with [async/await][learn-async-await].
## Debugging Puppeteer tests
The following questions are useful when debugging Puppeteer test
failures you might see in [continuous
integration](continuous-integration.md):
- Does the flow being tested work properly in the Zulip browser UI?
Test failures can reflect real bugs, and often it's easier and more
interactive to debug an issue in the normal Zulip development
environment than in the Puppeteer test suite.
- Does the change being tested adjust the HTML structure in a way that
affects any of the selectors used in the tests? If so, the test may
just need to be updated for your changes.
- Does the test fail deterministically when you run it locally using
e.g., `./tools/test-js-with-puppeteer compose.ts`? If so, you can
iteratively debug to see the failure.
- Does the test fail nondeterministically? If so, the problem is
likely that a `waitForSelector` statement is either missing or not
waiting for the right thing. Tests fail nondeterministically much
more often on very slow systems like those used for Continuous
Integration (CI) services because small races are amplified in those
environments; this often explains failures in CI that cannot be
easily reproduced locally.
- Does the test fail when you are typing (filling the form) on a modal
or other just-opened UI widget? Puppeteer starts typing after focusing on
the text field, sending keystrokes one after another. So, if
application code explicitly focuses the modal after it
appears/animates, this could cause the text field that Puppeteer is
trying to type into to lose focus, resulting in partially typed data.
The recommended fix for this is to wait until the modal is focused before
starting typing, like this:
```JavaScript
await page.waitForFunction(":focus").attr("id") === modal_id);
```
These tools/features are often useful when debugging:
- You can use `console.log` statements both in Puppeteer tests and the
code being tested to print-debug.
- Zulip's Puppeteer tests are configured to generate screenshots of
the state of the test browser when an assert statement fails; these
are stored under `var/puppeteer/*.png` and are extremely helpful for
debugging test failures.
- TODO: Mention how to access Puppeteer screenshots in CI.
- TODO: Add an option for using the `headless: false` debugging mode
of Puppeteer so you can watch what's happening, and document how to
make that work with Vagrant.
- TODO: Document `--interactive`.
- TODO: Document how to run 100x in CI to check for nondeterministic
failures.
- TODO: Document any other techniques/ideas that were helpful when porting
the Casper suite.
- The Zulip server powering these tests is just `run-dev` with some
extra [Django settings](../subsystems/settings.md) from
`zproject/test_extra_settings.py` to configure an isolated database
so that the tests will not interfere/interact with a normal
development environment. The console output while running the tests
includes the console output for the server; any Python exceptions
are likely actual bugs in the changes being tested.
See also [Puppeteer upstream's debugging
tips](https://github.com/puppeteer/puppeteer#debugging-tips); some
tips may require temporary patches to functions like `run_test` or
`ensure_browser` in `web/e2e-tests/lib/common.ts`.
## Writing Puppeteer tests
Probably the easiest way to learn how to write Puppeteer tests is to
study some of the existing test files. There are a few tips that can
be useful for writing Puppeteer tests in addition to the debugging
notes above:
- Run just the file containing your new tests as described above to
have a fast debugging cycle.
- When you're done writing a test, run it 100 times in a loop to
verify it does not fail nondeterministically (see above for notes on
how to get CI to do it for you); this is important to avoid
introducing extremely annoying nondeterministic failures into
`main`.
- With black-box browser tests like these, it's very important to write your code
to wait for browser's UI to update before taking any action that
assumes the last step was processed by the browser (e.g., after you
click on a user's avatar, you need an explicit wait for the profile
popover to appear before you can try to click on a menu item in that
popover). This means that before essentially every action in your
Puppeteer tests, you'll want to use `waitForSelector` or similar
wait function to make sure the page or element is ready before you
interact with it. The [puppeteer docs site](https://pptr.dev/) is a
useful reference for the available wait functions.
- When using `waitForSelector`, you always want to use the
`{visible: true}` option; otherwise the test will stop waiting as
soon as the target selector is present in the DOM even if it's
hidden. For the common UI pattern of having an element always be
present in the DOM whose presence is managed via show/hide rather
than adding/removing it from the DOM, `waitForSelector` without
`visible: true` won't wait at all.
- The test suite uses a smaller set of default user accounts and other
data initialized in the database than the normal development
environment; specifically, it uses the same setup as the [backend
tests](testing-with-django.md). To see what differs from
the development environment, check out the conditions on
`options["test_suite"]` in
`zilencer/management/commands/populate_db.py`.
[learn-async-await]: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await

154
docs/testing/testing.md Normal file
View File

@@ -0,0 +1,154 @@
# Testing overview
Zulip takes pride in its extensive, carefully designed test suites.
For example, `test-backend` runs a complete test suite (~98% test
coverage; 100% on core code) for the Zulip server in under a minute on
a fast laptop; very few web apps of similar scope can say something
similar.
This page focused on the mechanics of running automated tests in a
[development environment](../development/overview.md); you may also
want to read about our [testing philosophy](philosophy.md)
and [continuous integration
setup](continuous-integration.md).
Manual testing with a web browser is primarily discussed in the docs
on [using the development environment](../development/using.md).
## Running tests
Zulip tests must be run inside a Zulip development environment; if
you're using Vagrant, you may need to enter it with `vagrant ssh`.
You can run all of the test suites (similar to our continuous integration)
as follows:
```bash
./tools/test-all
```
However, you will rarely want to do this while actively developing,
because it takes a long time. Instead, your edit/refresh cycle will
typically involve running subsets of the tests with commands like these:
```bash
./tools/lint zerver/models/__init__.py # Lint the file you just changed
./tools/test-backend zerver.tests.test_markdown.MarkdownEmbedsTest.test_inline_youtube
./tools/test-backend MarkdownEmbedsTest # Run `test-backend --help` for more options
./tools/test-js-with-node util
# etc.
```
The commands above will all run in just a few seconds. Many more
useful options are discussed in each tool's documentation (e.g.,
`./tools/test-backend --help`).
## Major test suites
Zulip has a handful of major tests suite that every developer will
eventually work with, each with its own page detailing how it works:
- [Linters](linters.md): Our dozen or so linters run in parallel.
- [Django](testing-with-django.md): Server/backend Python tests.
- [Node](testing-with-node.md): JavaScript tests for the
frontend run via node.js.
- [Puppeteer](testing-with-puppeteer.md): End-to-end
UI tests run via a Chromium browser.
## Other test suites
Additionally, Zulip also has about a dozen smaller tests suites:
- `tools/test-migrations`: Checks whether the `zerver/migrations`
migration content the models defined in `zerver/models/*.py`. See our
[schema migration documentation](../subsystems/schema-migrations.md)
for details on how to do database migrations correctly.
- `tools/test-documentation`: Checks for broken links in this
ReadTheDocs documentation site.
- `tools/test-help-documentation`: Checks for broken links in the
`/help/` help center documentation, and related pages.
- `tools/test-api`: Tests that the API documentation at `/api`
actually works; the actual code for this is defined in
`zerver/openapi/python_examples.py`.
- `tools/check-capitalization`: Checks whether translated strings (aka
user-facing strings) correctly follow Zulip's capitalization
conventions. This requires some maintenance of an exclude list
(`tools.lib.capitalization.IGNORED_PHRASES`) of proper nouns
mentioned in the Zulip project, but helps a lot in avoiding new
strings being added that don't match our style.
- `tools/check-frontend-i18n`: Checks for a common bug in Handlebars
templates, of using the wrong syntax for translating blocks
containing variables.
- `./tools/test-run-dev`: Checks that `run-dev` starts properly;
this helps prevent bugs that break the development environment.
- `./tools/test-queue-worker-reload`: Verifies that Zulip's queue
processors properly reload themselves after code changes.
- `./tools/setup/optimize-svg`: Checks whether all integration logo SVG
graphics are optimized.
logos are properly optimized for size (since we're not going to edit
third-party logos, this helps keep the Zulip codebase from getting huge).
- `./tools/test-tools`: Automated tests for various parts of our
development tooling (mostly various linters) that are not used in
production.
Each of these has a reason (usually, performance or a need to do messy
things to the environment) why they are not part of the handful of
major test suites like `test-backend`, but they all contribute
something valuable to helping keep Zulip bug-free.
## Internet access inside test suites
As a policy matter, the Zulip test suites should never make outgoing
HTTP or other network requests. This is important for 2 major
reasons:
- Tests that make outgoing Internet requests will fail when the user
isn't on the Internet.
- Tests that make outgoing Internet requests often have a hidden
dependency on the uptime of a third-party service, and will fail
nondeterministically if that service has a temporary outage.
Nondeterministically failing tests can be a big waste of
developer time, and we try to avoid them wherever possible.
As a result, Zulip's major test suites should never access the
Internet directly. Since code in Zulip does need to access the
Internet (e.g., to access various third-party APIs), this means that
the Zulip tests use mocking to basically hardcode (for the purposes of
the test) what responses should be used for any outgoing Internet
requests that Zulip would make in the code path being tested.
This is easy to do using test fixtures (a fancy word for fixed data
used in tests) and the `mock.patch` function to specify what HTTP
response should be used by the tests for every outgoing HTTP (or other
network) request. Consult
[our guide on mocking](testing-with-django.md#zulip-mocking-practices) to
learn how to mock network requests easily; there are also a number of
examples throughout the codebase.
We partially enforce this policy in the main Django/backend test suite
by overriding certain library functions that are used in outgoing HTTP
code paths (`httplib2.Http().request`, `requests.request`, etc.) to
throw an exception in the backend tests. While this is enforcement is
not complete (there a lot of other ways to use the Internet from
Python), it is easy to do and catches most common cases of new code
depending on Internet access.
This enforcement code results in the following exception:
```pytb
File "tools/test-backend", line 120, in internet_guard
raise Exception("Outgoing network requests are not allowed in the Zulip tests."
Exception: Outgoing network requests are not allowed in the Zulip tests.
...
```
#### Documentation tests
The one exception to this policy is our documentation tests, which
will attempt to verify that the links included in our documentation
aren't broken. Those tests end up failing nondeterministically fairly
often, which is unfortunate, but there's simply no other correct way
to verify links other than attempting to access them. The compromise
we've implemented is that in CI, these tests only verify links to
websites controlled by the Zulip project (zulip.com, our GitHub,
our ReadTheDocs), and not links to third-party websites.

101
docs/testing/typescript.md Normal file
View File

@@ -0,0 +1,101 @@
# TypeScript static types
Zulip is early in the process of migrating our codebase to use
[TypeScript](https://www.typescriptlang.org/), the leading static type
system for JavaScript. It works as an extension of the ES6 JavaScript
standard, and provides similar benefits to our use of
[the mypy static type system for Python](mypy.md).
We expect to eventually migrate the entire JavaScript codebase to
TypeScript, though our current focus is on getting the tooling and
migration process right, not on actually migrating the codebase.
As a result, the details in this document are preliminary ideas for
discussion and very much subject to change.
A typical piece of TypeScript code looks like this:
```ts
setdefault(key: K, value: V): V {
const mapping = this._items[this._munge(key)];
if (mapping === undefined) {
return this.set(key, value);
}
return mapping.v;
}
```
The following resources are valuable for learning TypeScript:
- The main documentation on [TypeScript syntax][typescript-handbook].
## Type checking
TypeScript types are checked by the TypeScript compiler, `tsc`, which
is run as part of our [lint checks](linters.md). You can run the
compiler yourself with `tools/run-tsc`, which will check all the
TypeScript files once, or `tools/run-tsc --watch`, which will
continually recheck the files as you edit them.
## Linting and style
We use the ESLint plugin for TypeScript to lint TypeScript code, just
like we do for JavaScript. Our long-term goal is to use an idiomatic
TypeScript style for our TypeScript codebase.
However, because we are migrating an established JavaScript codebase,
we plan to start with a style that is closer to the existing
JavaScript code, so that we can easily migrate individual modules
without too much code churn. A few examples:
- TypeScript generally prefers explicit `return undefined;`, whereas
our existing JavaScript style uses just `return;`.
- With TypeScript, we expect to make heavy use of `let` and `const`
rather than `var`.
- With TypeScript/ES6, we may no longer need to use `_.each()` as our
recommended way to do loop iteration.
For each of the details, we will either want to bulk-migrate the
existing JavaScript codebase before the migration or plan to do it
after JS->TS migration for a given file, so that we don't need to
modify these details as part of converting a file from JavaScript to
TypeScript.
A possibly useful technique for this will be setting some eslint
override rules at the top of individual files in the first commit that
converts them from JS to TS.
## Migration strategy
Our plan is to order which modules we migrate carefully, starting with
those that:
- Appear frequently as reverse dependencies of other modules
(e.g., `people.js`). These are most valuable to do first because
then we have types on the data being interacted with by other
modules when we migrate those.
- Don't have large open pull requests (to avoid merge conflicts); one
can scan for these using [TinglingGit](https://github.com/zulip/TinglingGit).
- Have good unit test coverage, which limits the risk of breaking
correctness through refactoring. Use
`tools/test-js-with-node --coverage` to get a coverage report.
When migrating a module, we want to be especially thoughtful about
putting together a commit structure that makes mistakes unlikely and
the changes easy to verify. For example:
- First a commit that just converts the language to TypeScript adding
types. The result may potentially have some violations of the
long-term style we want (e.g., not using `const`). Depending on how
we're handling linting, we set some override eslint rules at the top
of the module at this stage so CI still passes.
- Then a commit just migrating use of `var` to `const/let` without
other changes (other than removing any relevant linter overrides).
- Etc.
With this approach, we should be able to produce a bunch of really
simple commits that can be merged the same day they're written without
significant risk of introducing regressions from typos, refactors that
don't quite work how they were expected to, etc.
[typescript-handbook]: https://www.typescriptlang.org/docs/handbook/basic-types.html