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
status-im.utils.views/letsubs clojure.core/let
reagent.core/with-let clojure.core/let
status-im.utils.fx/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.previews.preview/list-comp clojure.core/for
status-im.utils.styles/def clojure.core/def
status-im.utils.styles/defn clojure.core/defn
taoensso.tufte/defnp clojure.core/defn}
{:lint-as {status-im.utils.views/defview clojure.core/defn
status-im.utils.views/letsubs clojure.core/let
reagent.core/with-let clojure.core/let
status-im.utils.fx/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.previews.preview/list-comp clojure.core/for
status-im.utils.styles/def clojure.core/def
status-im.utils.styles/defn 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]}
;;TODO remove number when this is fixed
;;https://github.com/borkdude/clj-kondo/issues/867
:unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY number]}}}
;; TODO remove number when this is fixed
;; https://github.com/borkdude/clj-kondo/issues/867
: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})
```
### 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
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
prefer to use it for more general purpose concepts, such as the re-frame event
layer."
(:require-macros status-im.test-helpers)
(:require [re-frame.core :as rf]
[re-frame.db :as rf-db]
[re-frame.events :as rf-events]
[re-frame.registrar :as rf-registrar]
[re-frame.subs :as rf-subs]
[taoensso.timbre :as log]))
(defn db
@ -100,3 +102,15 @@
:fn (fn [{:keys [vargs level]}]
(swap! logs conj {:args vargs :level level}))})]
(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
(:require [cljs.test :refer [deftest is testing]]
[status-im2.subs.communities :as subs]))
(:require [cljs.test :refer [is testing use-fixtures]]
[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
(testing "has unread messages"
(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})))))
(use-fixtures :each
{:before #(reset! rf-db/app-db {:communities/enabled? true})})
(deftest calculate-unviewed-counts-test
(let [chats [{:unviewed-messages-count 1
:unviewed-mentions-count 2}
{:unviewed-messages-count 3
:unviewed-mentions-count 0}
{:unviewed-messages-count 2
:unviewed-mentions-count 1}]]
(is (= {:unviewed-messages-count 6
:unviewed-mentions-count 3}
(subs/calculate-unviewed-counts chats)))))
(def community-id "0x1")
(h/deftest-sub :communities
[sub-name]
(testing "returns empty vector if flag is disabled"
(swap! rf-db/app-db assoc :communities/enabled? false)
(is (= [] (rf/sub [sub-name]))))
(testing "returns raw communities if flag is enabled"
(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
(:require [cljs.test :refer [deftest is testing]]
[status-im2.subs.wallet.transactions :as wallet.transactions]
[status-im2.subs.onboarding :as onboarding]
[status-im.utils.money :as money]
[status-im2.subs.wallet.wallet :as wallet]))
[status-im2.subs.onboarding :as onboarding]))
(def transactions [{:timestamp "1505912551000"}
{:timestamp "1505764322000"}
@ -40,17 +38,3 @@
{"0x1" {:keycard-pairing "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])))))