Introduce subscription tests (#14472)

This commit is contained in:
Icaro Motta 2022-12-06 13:36:05 -03:00 committed by GitHub
parent da0f0d3a81
commit 6e272a96c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 357 additions and 67 deletions

View File

@ -1,14 +1,17 @@
{:lint-as {status-im.utils.views/defview clojure.core/defn {:lint-as {status-im.utils.views/defview clojure.core/defn
status-im.utils.views/letsubs clojure.core/let status-im.utils.views/letsubs clojure.core/let
reagent.core/with-let clojure.core/let reagent.core/with-let clojure.core/let
status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all
utils.re-frame/defn clj-kondo.lint-as/def-catch-all utils.re-frame/defn clj-kondo.lint-as/def-catch-all
quo.react/with-deps-check clojure.core/fn quo.react/with-deps-check clojure.core/fn
quo.previews.preview/list-comp clojure.core/for quo.previews.preview/list-comp clojure.core/for
status-im.utils.styles/def clojure.core/def status-im.utils.styles/def clojure.core/def
status-im.utils.styles/defn clojure.core/defn status-im.utils.styles/defn clojure.core/defn
taoensso.tufte/defnp clojure.core/defn} status-im.test-helpers/deftest-sub clojure.core/defn
taoensso.tufte/defnp clojure.core/defn}
:linters {:invalid-arity {:skip-args [status-im.utils.fx/defn utils.re-frame/defn]} :linters {:invalid-arity {:skip-args [status-im.utils.fx/defn utils.re-frame/defn]}
;;TODO remove number when this is fixed ;; TODO remove number when this is fixed
;;https://github.com/borkdude/clj-kondo/issues/867 ;; https://github.com/borkdude/clj-kondo/issues/867
:unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY number]}}} :unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY
number
status-im.test-helpers/restore-app-db]}}}

View File

@ -346,6 +346,53 @@ keywords and concatenating them into a single string.
(i18n/label :t/biometric-auth-error {:code error-code}) (i18n/label :t/biometric-auth-error {:code error-code})
``` ```
### Tests
#### Subscription tests
Test [layer-3 subscriptions](https://day8.github.io/re-frame/subscriptions/) by
actually subscribing to them, so reframe's signal graph gets validated too.
```clojure
;; bad
(defn user-recipes
[[current-user all-recipes location]]
...)
(re-frame/reg-sub
:user/recipes
:<- [:current-user]
:<- [:all-recipes]
:<- [:location]
user-recipes)
(deftest user-recipes-test
(testing "builds list of recipes"
(let [current-user {...}
all-recipes {...}
location [...]]
(is (= expected (recipes [current-user all-recipes location]))))))
;; good
(require '[status-im.test-helpers :as h])
(re-frame/reg-sub
:user/recipes
:<- [:current-user]
:<- [:all-recipes]
:<- [:location]
(fn [[current-user all-recipes location]]
...))
(h/deftest-sub :user/recipes
[sub-name]
(testing "builds list of recipes"
(swap! rf-db/app-db assoc
:current-user {...}
:all-recipes {...}
:location [...])
(is (= expected (rf/sub [sub-name])))))
```
## Project Structure ## Project Structure
First, the bird's-eye view with some example ClojureScript files: First, the bird's-eye view with some example ClojureScript files:

View File

@ -0,0 +1,57 @@
(ns status-im.test-helpers
(:require [clojure.spec.alpha :as s]
[clojure.string :as string]
[clojure.walk :as walk]))
(defn- subscription-name->test-name
[sub-name]
(->> [(namespace sub-name)
(name sub-name)
"test"]
(remove nil?)
(map #(string/replace % #"\." "-"))
(string/join "-")))
(defmacro ^:private testing-subscription
[description & body]
`(cljs.test/testing ~description
(restore-app-db (fn [] ~@body))))
(s/fdef deftest-sub
:args (s/cat :sub-name keyword?
:args (s/coll-of symbol? :count 1)
:body (s/* any?)))
(defmacro deftest-sub
"Defines a test based on `sub-name`, executes `body` and restores the app db.
Any usage of the `cljs.test/testing` macro inside `body` will be modified to
also make sure the app db is restored and the subscription cache is reset.
Expressions in `body` will have access to `sub-name`, which should be used to
avoid needlessly repeating the subscription name.
Example:
```clojure
(require '[status-im.test-helpers :as h])
(h/deftest-sub :wallet/sorted-tokens
[sub-name]
(testing \"sorts tokens by name, lowercased\"
;; Arrange
(swap! rf-db/app-db assoc-in [<db-path>] <value>)
;; Act and Assert
(is (= <expected> (rf/sub [sub-name])))))
```"
[sub-name args & body]
`(let [sub-name# ~sub-name]
(cljs.test/deftest ~(symbol (subscription-name->test-name sub-name))
(let [~args [sub-name#]]
(restore-app-db
(fn []
~@(clojure.walk/postwalk-replace
{'cljs.test/testing `testing-subscription
'testing `testing-subscription}
body)))))))

View File

@ -5,10 +5,12 @@
Avoid coupling this namespace with particularities of the Status' domain, thus Avoid coupling this namespace with particularities of the Status' domain, thus
prefer to use it for more general purpose concepts, such as the re-frame event prefer to use it for more general purpose concepts, such as the re-frame event
layer." layer."
(:require-macros status-im.test-helpers)
(:require [re-frame.core :as rf] (:require [re-frame.core :as rf]
[re-frame.db :as rf-db] [re-frame.db :as rf-db]
[re-frame.events :as rf-events] [re-frame.events :as rf-events]
[re-frame.registrar :as rf-registrar] [re-frame.registrar :as rf-registrar]
[re-frame.subs :as rf-subs]
[taoensso.timbre :as log])) [taoensso.timbre :as log]))
(defn db (defn db
@ -100,3 +102,15 @@
:fn (fn [{:keys [vargs level]}] :fn (fn [{:keys [vargs level]}]
(swap! logs conj {:args vargs :level level}))})] (swap! logs conj {:args vargs :level level}))})]
(f logs)))) (f logs))))
(defn restore-app-db
"Saves current app DB, calls `f` and restores the original app DB.
Always clears the subscription cache after calling `f`."
[f]
(rf-subs/clear-subscription-cache!)
(let [original-db @rf-db/app-db]
(try
(f)
(finally
(reset! rf-db/app-db original-db)))))

View File

@ -0,0 +1,16 @@
(ns status-im2.subs.activity-center-test
(:require [cljs.test :refer [is testing]]
[re-frame.db :as rf-db]
[status-im.test-helpers :as h]
status-im2.subs.activity-center
[utils.re-frame :as rf]))
(h/deftest-sub :activity-center/filter-status-unread-enabled?
[sub-name]
(testing "returns true when filter status is unread"
(swap! rf-db/app-db assoc-in [:activity-center :filter :status] :unread)
(is (true? (rf/sub [sub-name]))))
(testing "returns false when filter status is not unread"
(swap! rf-db/app-db assoc-in [:activity-center :filter :status] :all)
(is (false? (rf/sub [sub-name])))))

View File

@ -1,40 +1,76 @@
(ns status-im2.subs.communities-test (ns status-im2.subs.communities-test
(:require [cljs.test :refer [deftest is testing]] (:require [cljs.test :refer [is testing use-fixtures]]
[status-im2.subs.communities :as subs])) [re-frame.db :as rf-db]
[status-im.test-helpers :as h]
status-im2.subs.communities
[utils.re-frame :as rf]))
(deftest community->home-item-test (use-fixtures :each
(testing "has unread messages" {:before #(reset! rf-db/app-db {:communities/enabled? true})})
(is (= {:name "name-1"
:muted? true
:unread-messages? true
:unread-mentions-count 5
:community-icon "icon-1"}
(subs/community->home-item
{:name "name-1"
:muted true
:images {:thumbnail {:uri "icon-1"}}}
{:unviewed-messages-count 1
:unviewed-mentions-count 5}))))
(testing "no unread messages"
(is (= {:name "name-2"
:muted? false
:unread-messages? false
:unread-mentions-count 5
:community-icon "icon-2"}
(subs/community->home-item
{:name "name-2"
:muted false
:images {:thumbnail {:uri "icon-2"}}}
{:unviewed-messages-count 0
:unviewed-mentions-count 5})))))
(deftest calculate-unviewed-counts-test (def community-id "0x1")
(let [chats [{:unviewed-messages-count 1
:unviewed-mentions-count 2} (h/deftest-sub :communities
{:unviewed-messages-count 3 [sub-name]
:unviewed-mentions-count 0} (testing "returns empty vector if flag is disabled"
{:unviewed-messages-count 2 (swap! rf-db/app-db assoc :communities/enabled? false)
:unviewed-mentions-count 1}]] (is (= [] (rf/sub [sub-name]))))
(is (= {:unviewed-messages-count 6
:unviewed-mentions-count 3} (testing "returns raw communities if flag is enabled"
(subs/calculate-unviewed-counts chats))))) (let [raw-communities {"0x1" {:id "0x1"}}]
(swap! rf-db/app-db assoc
:communities/enabled? true
:communities raw-communities)
(is (= raw-communities (rf/sub [sub-name]))))))
(h/deftest-sub :communities/section-list
[sub-name]
(testing "builds sections using the first community name char (uppercased)"
(swap! rf-db/app-db assoc :communities
{"0x1" {:name "civilized monkeys"}
"0x2" {:name "Civilized rats"}})
(is (= [{:title "C"
:data [{:name "civilized monkeys"}
{:name "Civilized rats"}]}]
(rf/sub [sub-name]))))
(testing "sorts by section ascending"
(swap! rf-db/app-db assoc :communities
{"0x3" {:name "Memorable"}
"0x1" {:name "Civilized monkeys"}})
(is (= [{:title "C" :data [{:name "Civilized monkeys"}]}
{:title "M" :data [{:name "Memorable"}]}]
(rf/sub [sub-name]))))
(testing "builds default section for communities without a name"
(swap! rf-db/app-db assoc :communities
{"0x2" {:id "0x2"}
"0x1" {:id "0x1"}})
(is (= [{:title ""
:data [{:id "0x2"}
{:id "0x1"}]}]
(rf/sub [sub-name])))))
(h/deftest-sub :communities/unviewed-counts
[sub-name]
(testing "sums counts for a particular community"
(swap! rf-db/app-db assoc :chats
{"0x100" {:community-id community-id
:unviewed-mentions-count 3
:unviewed-messages-count 2}
"0x101" {:community-id "0x2"
:unviewed-mentions-count 7
:unviewed-messages-count 9}
"0x102" {:community-id community-id
:unviewed-mentions-count 5
:unviewed-messages-count 1}})
(is (= {:unviewed-messages-count 3
:unviewed-mentions-count 8}
(rf/sub [sub-name community-id]))))
(testing "defaults to zero when count keys are not present"
(swap! rf-db/app-db assoc :chats
{"0x100" {:community-id community-id}})
(is (= {:unviewed-messages-count 0
:unviewed-mentions-count 0}
(rf/sub [sub-name community-id])))))

View File

@ -1,9 +1,7 @@
(ns status-im2.subs.subs-test (ns status-im2.subs.subs-test
(:require [cljs.test :refer [deftest is testing]] (:require [cljs.test :refer [deftest is testing]]
[status-im2.subs.wallet.transactions :as wallet.transactions] [status-im2.subs.wallet.transactions :as wallet.transactions]
[status-im2.subs.onboarding :as onboarding] [status-im2.subs.onboarding :as onboarding]))
[status-im.utils.money :as money]
[status-im2.subs.wallet.wallet :as wallet]))
(def transactions [{:timestamp "1505912551000"} (def transactions [{:timestamp "1505912551000"}
{:timestamp "1505764322000"} {:timestamp "1505764322000"}
@ -40,17 +38,3 @@
{"0x1" {:keycard-pairing "keycard-pairing-code"}}} {"0x1" {:keycard-pairing "keycard-pairing-code"}}}
{})] {})]
(is (= res "keycard-pairing-code"))))) (is (= res "keycard-pairing-code")))))
(deftest test-balance-total-value
(is (= (wallet/get-balance-total-value
{:ETH (money/bignumber 1000000000000000000)
:SNT (money/bignumber 100000000000000000000)
:AST (money/bignumber 10000)}
{:ETH {:USD {:from "ETH", :to "USD", :price 677.91, :last-day 658.688}}
:SNT {:USD {:from "SNT", :to "USD", :price 0.1562, :last-day 0.15}}
:AST {:USD {:from "AST", :to "USD", :price 4, :last-day 3}}}
:USD
{:ETH 18
:SNT 18
:AST 4})
697.53)))

View File

@ -0,0 +1,133 @@
(ns status-im2.subs.wallet.wallet-test
(:require [cljs.test :refer [deftest is testing]]
[re-frame.db :as rf-db]
[status-im.test-helpers :as h]
[status-im.utils.money :as money]
[status-im2.subs.wallet.wallet :as wallet]
[utils.re-frame :as rf]))
(def money-zero (money/bignumber 0))
(def money-eth (money/bignumber 8000000000000000000))
(def money-snt (money/bignumber 756000000000000000000))
(def main-account-id "0x0Fbd")
(def accounts
[{:address "0x0Fbd"
:name "Main account"
:hidden false
:removed false}
{:address "0x5B03"
:name "Secondary account"
:hidden false
:removed false}])
(def wallet
{:accounts {main-account-id
{:balance {:ETH money-eth :SNT money-snt}
:transactions {}
:max-block 0}
"0x5B03"
{:balance {:ETH money-eth :SNT money-snt}
:transactions {}
:max-block 10}}})
(def prices
{:ETH {:USD {:from "ETH"
:to "USD"
:price 1282.23}}
:SNT {:USD {:from "SNT"
:to "USD"
:price 0.0232}}})
(def tokens
{"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
{:address "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
:name "Ether"
:symbol :ETH
:decimals 18
:chainId 1}
"0x744d70fdbe2ba4cf95131626614a1763df805b9e"
{:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e"
:name "Status Network Token"
:symbol :SNT
:decimals 18
:chainId 1}})
(h/deftest-sub :balances
[sub-name]
(swap! rf-db/app-db assoc
:multiaccount/accounts accounts
:wallet wallet)
(is (= [{:ETH money-eth
:SNT money-snt}
{:ETH money-eth
:SNT money-snt}]
(rf/sub [sub-name]))))
(h/deftest-sub :wallet/token->decimals
[sub-name]
(swap! rf-db/app-db assoc :wallet/all-tokens tokens)
(is (= {:SNT 18 :ETH 18}
(rf/sub [sub-name]))))
(deftest get-balance-total-value-test
(is (= 697.53
(wallet/get-balance-total-value
{:ETH (money/bignumber 1000000000000000000)
:SNT (money/bignumber 100000000000000000000)
:AST (money/bignumber 10000)}
{:ETH {:USD {:from "ETH" :to "USD" :price 677.91 :last-day 658.688}}
:SNT {:USD {:from "SNT" :to "USD" :price 0.1562 :last-day 0.15}}
:AST {:USD {:from "AST" :to "USD" :price 4 :last-day 3}}}
:USD
{:ETH 18
:SNT 18
:AST 4}))))
(h/deftest-sub :portfolio-value
[sub-name]
(testing "returns fallback value when balances and prices are not available"
(is (= "..." (rf/sub [sub-name]))))
(testing "returns zero when balance is not positive"
(let [empty-wallet {:accounts {main-account-id
{:balance {:ETH money-zero
:SNT money-zero}}}}]
(swap! rf-db/app-db assoc
:multiaccount/accounts accounts
:prices prices
:wallet empty-wallet
:wallet/all-tokens tokens)
(is (= "0" (rf/sub [sub-name])))))
(testing "returns formatted value in the default USD currency"
(swap! rf-db/app-db assoc
:multiaccount/accounts accounts
:prices prices
:wallet wallet
:wallet/all-tokens tokens)
(is (= "20,550.76" (rf/sub [sub-name])))))
(h/deftest-sub :account-portfolio-value
[sub-name]
(testing "returns fallback value when balances and prices are not available"
(is (= "..." (rf/sub [sub-name]))))
(testing "returns zero when balance is not positive"
(let [empty-wallet {:accounts {main-account-id
{:balance {:ETH money-zero
:SNT money-zero}}}}]
(swap! rf-db/app-db assoc
:multiaccount/accounts accounts
:prices prices
:wallet empty-wallet
:wallet/all-tokens tokens)
(is (= "0" (rf/sub [sub-name main-account-id])))))
(testing "returns formatted value in the default USD currency"
(swap! rf-db/app-db assoc
:multiaccount/accounts accounts
:prices prices
:wallet wallet
:wallet/all-tokens tokens)
(is (= "10,275.38" (rf/sub [sub-name main-account-id])))))