Merge pull request #229 from penpot/hiru-doc-unit-tests

Document new unit tests
This commit is contained in:
Andrey Antukh 2024-06-05 13:19:11 +02:00 committed by GitHub
commit 3e9068b272
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 256 additions and 15 deletions

View File

@ -228,3 +228,258 @@ This macro enables you to have assertions on production code, that
generate runtime exceptions when failed (make sure you handle them
appropriately).
## Unit tests
We expect all Penpot code (either in frontend, backend or common subsystems) to
have unit tests, i.e. the ones that test a single unit of code, in isolation
from other blocks. Currently we are quite far from that objective, but we are
working to improve this.
### Running tests with kaocha
Unit tests are executed inside the [development environment](/technical-guide/developer/devenv).
We can use [kaocha test runner](https://cljdoc.org/d/lambdaisland/kaocha/), and
we have prepared, for convenience, some aliases in `deps.edn` files. To run
them, just go to `backend`, `frontend` or `common` and execute:
```bash
# To run all tests once
clojure -M:dev:test
# To run all tests and keep watching for changes
clojure -M:dev:test --watch
# To run a single tests module
clojure -M:dev:test --focus common-tests.logic.comp-sync-test
# To run a single test
clojure -M:dev:test --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute
```
Watch mode runs all tests when some file changes, except if some tests failed
previously. In this case it only runs the failed tests. When they pass, then
runs all of them again.
You can also mark tests in the code by adding metadata:
```clojure
;; To skip a test, for example when is not working or too slow
(deftest ^:kaocha/skip bad-test
(is (= 2 1)))
;; To skip it but warn you during test run, so you don't forget it
(deftest ^:kaocha/pending bad-test
(is (= 2 1)))
```
Please refer to the [kaocha manual](https://cljdoc.org/d/lambdaisland/kaocha/1.91.1392/doc/6-focusing-and-skipping)
for how to define custom metadata and other ways of selecting tests.
**NOTE**: in `frontend` we still can't use kaocha to run the tests. We are on
it, but for now we use shadow-cljs with `package.json` scripts:
```bash
yarn run test
yarn run test:watch
```
#### Test output
The default kaocha reporter outputs a summary for the test run. There is a pair
of brackets `[ ]` for each suite, a pair of parentheses `( )` for each test,
and a dot `.` for each assertion `t/is` inside tests.
```bash
penpot@c261c95d4623:~/penpot/common$ clojure -M:dev:test
[(...)(............................................................
.............................)(....................................
..)(..........)(.................................)(.)(.............
.......................................................)(..........
.....)(......)(.)(......)(.........................................
..............................................)(............)]
190 tests, 3434 assertions, 0 failures.
```
All standard output from the tests is captured and hidden, except if some test
fails. In this case, the output for the failing test is shown in a box:
```bash
FAIL in sample-test/stdout-fail-test (sample_test.clj:10)
Expected:
:same
Actual:
-:same +:not-same
╭───── Test output ───────────────────────────────────────────────────────
│ Can you see this?
╰─────────────────────────────────────────────────────────────────────────
2 tests, 2 assertions, 1 failures.
```
You can bypass the capture with the command line:
```bash
clojure -M:dev:test --no-capture-output
```
Or for some specific output:
```clojure
(ns sample-test
(:require [clojure.test :refer :all]
[kaocha.plugin.capture-output :as capture]))
(deftest stdout-pass-test
(capture/bypass
(println "This message should be displayed"))
(is (= :same :same)))
```
### Running tests in the REPL
An alternative way of running tests is to do it from inside the REPL you can
use in the backend and common apps in the [development environment](/technical-guide/developer/devenv).
We have a helper function `(run-tests)` that refresh the environment (to avoid
having [stale tests](https://practical.li/clojure/testing/unit-testing/#command-line-test-runners))
and runs all tests or a selection. It is defined in `backend/dev/user.clj` and
`common/dev/user.clj`, so it's available in the REPL without importing anything.
```clojure
;; To run all tests
(run-tests)
;; To run all tests in one namespace
(run-tests 'some.namespace)
;; To run a single test
(run-tests 'some.namespace/some-test)
;; To run all tests in one or several namespaces,
;; selected by a regular expression
(run-tests #"^backend-tests.rpc.*")
```
### Writing unit tests
We write tests using the standard [Clojure test
API](https://clojure.github.io/clojure/clojure.test-api.html). You can find a
[guide to writing unit tests](https://practical.li/clojure/testing/unit-testing) at Practicalli
Clojure, that we follow as much as possible.
#### Sample files helpers
An important issue when writing tests in Penpot is to have files with the
specific configurations we need to test. For this, we have defined a namespace
of helpers to easily create files and its elements with sample data.
To make handling of uuids more convenient, those functions have a uuid
registry. Whenever you create an object, you may give a `:label`, and the id of
the object will be stored in the registry associated with this label, so you
can easily recover it later.
You have functions to create files, pages and shapes, to connect them and
specify their attributes, having all of them default values if not set.
Files also store in metadata the **current page**, so you can control in what
page the `add-` and `get-` functions will operate.
```clojure
(ns common-tests.sample-helpers-test
(:require
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[clojure.test :as t]))
(t/deftest test-create-file
(let [;; Create a file with one page
f1 (thf/sample-file :file1)
;; Same but define the label of the page, to retrieve it later
f2 (thf/sample-file :file2 :page-label :page1)
;; Set the :name attribute of the created file
f3 (thf/sample-file :file3 :name "testing file")
;; Create an isolated page
p2 (thf/sample-page :page2 :name "testing page")
;; Create a second page and add to the file
f4 (-> (thf/sample-file :file4 :page-label :page3)
(thf/add-sample-page :page4 :name "other testing page"))
;; Create an isolated shape
p2 (thf/sample-shape :shape1 :type :rect :name "testing shape")
;; Add a couple of shapes to a previous file, in different pages
f5 (-> f4
(ths/add-sample-shape :shape2)
(thf/switch-to-page :page4)
(ths/add-sample-shape :shape3 :name "other testing shape"
:width 100))
;; Retrieve created shapes
s1 (ths/get-shape f4 :shape1)
s2 (ths/get-shape f5 :shape2 :page-label :page3)
s3 (ths/get-shape f5 :shape3)]
;; Check some values
(t/is (= (:name f1) "Test file"))
(t/is (= (:name f3) "testing file"))
(t/is (= (:id f2) (thi/id :file2)))
(t/is (= (:id (thf/current-page f2)) (thi/id :page1)))
(t/is (= (:id s1) (thi/id :shape1)))
(t/is (= (:name s1) "Rectangle"))
(t/is (= (:name s3) "testing shape"))
(t/is (= (:width s3) 100))
(t/is (= (:width (:selrect s3)) 100))))
```
Also there are functions to make some transformations, like creating a
component, instantiating it or swapping a copy.
```clojure
(ns app.common-tests.sample-components-test
(:require
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.shapes :as ths]))
(t/deftest test-create-component
(let [;; Create a file with one component
f1 (-> (thf/sample-file :file1)
(ths/add-sample-shape :frame1 :type :frame)
(ths/add-sample-shape :rect1 :type :rect
:parent-label :frame1)
(thc/make-component :component1 :frame1))]))
```
Finally, there are composition helpers, to build typical structures with a
single line of code. And the files module has some functions to display the
contents of a file, in a way similar to `debug/dump-tree` but showing labels
instead of ids:
```clojure
(ns app.common-tests.sample-compositions-test
(:require
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]))
(t/deftest test-create-composition
(let [f1 (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main-root
:main-child
:copy-root))]
(ctf/dump-file f1 :show-refs? true)))
;; {:main-root} [:name Frame1] # [Component :component1]
;; :main-child [:name Rect1]
;;
;; :copy-root [:name Frame1] #--> [Component :component1] :main-root
;; <no-label> [:name Rect1] ---> :main-child
```
You can see more examples of usage by looking at the existing unit tests.

View File

@ -296,20 +296,6 @@ msgstr[1] "%s projects"
;; => "1 project"
```
## Unit Tests
Unit tests have to be compiled first, and then run with node.
```bash
npx shadow-cljs compile tests && node target/tests.js
```
Or run the watch (that automatically runs the test):
```bash
npx shadow-cljs watch tests
```
## Integration tests
### Setup
@ -330,7 +316,7 @@ Ensure your development environment docker image is up to date.
./manage.sh start-devenv
```
**NOTE** You can learn more about how to set up, start and stop our development environment [here](http://localhost:8080/technical-guide/developer/devenv/#getting-started)
**NOTE** You can learn more about how to set up, start and stop our development environment [here](/technical-guide/developer/devenv)
### Running the integration tests