Skip to content

Commit

Permalink
docs: Add guide for writing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
balamurali27 authored May 29, 2023
1 parent 126a691 commit 2503e52
Showing 1 changed file with 182 additions and 0 deletions.
182 changes: 182 additions & 0 deletions guide-to-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Prerequisites

Before we get into writing tests, please make sure you have pre-commit hook for
styling tools setup so CI won't fail from these

Instructions [here](https://github.com/frappe/press/issues/424#issuecomment-1193375098)

# Writing Tests for Press

Writing tests involve running tests locally (duh). So let's get that setup. (You'll only have to do this once)

## Make a test site

Tests can leave fake records. This will pollute your local setup. So, get
yourself a test site. You can get these commands from the CI workflow file too,
but I'll save you some time. You can name the site and set password to whatever.

```sh
bench new-site --db-root-password admin --admin-password admin test_site
bench --site test_site install-app press
bench --site test_site add-to-hosts # in case you wanna call APIs
bench --site test_site set-config allow_tests true
```

Finally, you need to start bench as some of the tests may want to trigger
background jobs, which would fail if background workers aren't there

```sh
bench start
```

As you write tests you'll occasionally want to remove all test data
in your test site from time to time. So, here ya go:

```sh
bench --site test_site reinstall --yes
```

## Writing tests

This is the hard part. Because of Press's dependency with outside world, it's
hard to isolate unit tests to this project. Regardless it's still possible with
plain old python's built in libraries.

Majority of this is done with the help of python's `unittest.mock` library. We
use this library to mock parts of code when referencing things that are out of
Press's control.

Eg: We can mock all Agent Job creation calls by decorating the TestCase class like so

```python
@patch.object(AgentJob, "enqueue_http_request", new=Mock())
class TestSite(unittest.TestCase):
```

We use `patch.object` decorator here so that every instance of `AgentJob`
object will have it's `enqueue_http_request` method be replaced by whatever we
pass in the new argument, which in this case is `Mock()` which does nothing.
You can think of it as a `pass`. But it has other uses as you'll find if you
keep reading.

> Note: Class decorators aren't inherited, so you'll have to do this on all
> classes you want to mock http request creation for Agent Job
Now that we've learned to mock the external things, we can go about mocking
internal things, which forms the basis of testing, which is

1. Make test records
2. Perform operation (i.e Run code that will on production)
3. Test the test records for results

### Making test records

Making test records is also kind of a pain as we have validations all around
code that will need to be passed every time you create a doc. This is too much
cognition. Therefore, we can create utility functions (with sensible defaults)
to make test record of the corresponding Doctype in their own corresponding
test files (for organization reasons). These functions will be doing the bare
minimum to make a valid document of that doctype.

Eg: `create_test_bench` in `test_bench.py` can be imported and used whenever
you need a valid bench (which itself has dependencies on many other doctypes)

You can also add default args to these utility functions as you come across the
need. Just append to end so you won't have to rewrite pre-existing tests.

### Rerunnability

Not a real word, but I like to be able to re-run my tests without having to
nuke the database. Leaving the database in an "empty state" after every test is
a very easy way to achieve this. This also makes testing for things like count
of docs super easy. Lucky for us there's a method in `TestCase` that's run
after every individual test in the class. It's called `tearDown`.

We can easily do

```python
def tearDown(self):
frappe.db.rollback()
```

And every doc you create (in foreground at least) will not be committed into the database.

> Note: If the code you're testing calls frappe.db.commit, be sure to mock it
> cuz otherwise docs will get committed till that point regardless.
You can mock certain lines while testing a piece of code with the `patch` decorator too. Eg:

```python
from unittest.mock import MagicMock, patch

# this will mock all the frappe.db.commit calls in server.py while in this test suite
@patch("press.press.doctype.server.server.frappe.db.commit", new=MagicMock)
class TestBench(unittest.TestCase):
```

You can also use the patch decorator on test methods too. Eg:

https://github.com/frappe/press/blob/6dd6b2c8193b04f1aec1601d52ba09ce9dca8dfe/press/tests/test_cleanup.py#L280-L290
The decorator passes the mocked function (which is a `Mock()` object) along as
an argument, so you can later do asserts on it (if you want to).

You can even use the decorator as context manager if you don't want to mock
things for the entirety of the test.

https://github.com/frappe/press/blob/6dd6b2c8193b04f1aec1601d52ba09ce9dca8dfe/press/tests/test_audit.py#L97-L102

Here, we're actually faking the output of the function which usually calls a
remote endpoint that's out of our control by adding the `new` argument to the
method.

That's pretty much all you need to write safe, rerunnable tests for Press. You
can checkout https://docs.python.org/3/library/unittest.mock.html for more
things you can do with the standard python libraries. If your editor and
plugins are setup configured nicely, you can even do TDD with ease.

> Protip: When you have test records you want across a TestCase, then you can
> simply use the create the test record in `setUp` method of the same. The test
> records can be assigned to member variables. Eg:
```python
def setUp(self):
self.team = create_test_team()
```

## Running tests

You can run all of the tests with the following command.

```sh
bench --site test_site run-tests --app press
```

But you'll never have to. That's what CI is for. Instead, you'll mostly want to use:

```sh
bench --site test_site run-tests --app press --module press.press.doctype.some_doctype.test_some_doctype
```

This is because while writing bugs, your changes will mostly affect that one
module only and since we don't have many tests to begin with, it won't take
very long to run a module's test by itself anyway. Give your eyes a break while this happens.

You can also run individual test with:

```sh
bench --site test_site run-tests --module press.press.doctype.some_doctype.test_some_doctype --test test_very_specific_thing
```

You most likely won't enjoy running commands manually like this. So you'd want
to check out [this vim plugin](https://github.com/ankush/frappe_test.vim/) or
[this vscode plugin](https://marketplace.visualstudio.com/items?itemName=AnkushMenat.frappe-test-runner)

> Note: frappe_test plugin doesn't populate vim's quickfix list yet. Though
> Frappe's test runner output isn't very pyunit errorformat friendly, you can
> still make it work with a [custom errorformat](https://github.com/balamurali27/dotfiles/blob/85dc18a/.config/nvim/after/plugin/frappe.vim#LL10C1-L10C128) and some hacks to [set makeprg](https://github.com/balamurali27/dotfiles/blob/0bcd6270770d0b67b63fc0ea308e6834fefda5a6/.config/nvim/init.vim#L150C7-L163)
# References

- https://frappeframework.com/docs/v14/user/en/testing
- https://docs.python.org/3/library/unittest.mock.html
- https://learnvim.irian.to/basics/compile

0 comments on commit 2503e52

Please sign in to comment.