単位試験のためのProxyquireまたはSononを使用しないこと


“魔法の”sinon、proxyquire、冗談などのメンテナンス、読みやすい、ドライJavaScriptユニットテスト.

免責事項


このポストに触発
物品


サムありがとう!

イントロ


私は、プロとして、私はテストはかなりシンプルで簡単に発見したことから始めてみましょう.私は私の雇い主とすべてのテスト報道の助けを借りて何百万ものアプリの数を作成!しかし、最後の年を完全なスタックを横切って作業中のユニットと統合テストの多くを書いて費やしてきた、私は自分の膨大な経験を共有する衝動を感じる.それは視点のわずかな変化を必要としますが、テストは、ビジネスロジックのハッスルと喧騒から田舎を穏やかに散歩をするような気がするはずです.

実際


それから我々のシナリオにまっすぐに飛び込んでください.単にこのURLから利用可能なすべての為替レートを取得する関数をテストしたいだけです.https://currencylayer.com/ , その後、それらのすべてをMongoDBデータベースに歴史的目的のために格納し、それらの最新のREDISデータベースにキャッシュしますが、あなたのノードのみ.jsプロセスはデータベースにアクセスできます.このAPIのユースケースは、潜在的に日常的に(毎時)実行されるServerlessな機能でありえましたその日のすべての為替レートを取得するには、為替レートの歴史的なグラフを表示し、最新の為替レートのメモリ内のデータベースにキャッシュされている.
「わぁ!」あなたは、“これはモックにたくさんです!”と思うかもしれません!実にたくさん.私は意図的にサムのオリジナルの例をはるかに複雑な現実世界のシナリオに近づくために作った.
ここで使用するパッケージを確認してみましょう.
stampit : フレキシブルオブジェクトファクトリー
ノードの取得
ユニットテストフレームワーク
ご注意ください、私たちは使用していませんproxyquire and sinon でさえchai ). なぜ?私の経験の年は、これらのライブラリが取るアプローチから離れて私たちを追い払ったので.彼らはJavaScriptの"魔法"あまりにも発生します.あなたのコードのより多くの「魔法」が、より保守的であるので、あなたはあなたのnodeoundモジュールをアップグレードするより多くの問題です.これらの問題累積的な効果は私の仕事時間の最大20 %を占めた.
最後に、私は「魔法」が悪くて、あなたのコードをより明白にするという結論に達しました.
保全可能なノードの一般的な推奨事項.JSコード:
  • より少ない魔法、より明白な.
  • 依存性が小さい.
  • もっとシンプル.
  • 以下のコード.以下の自動生成コード.
  • メインコード


    // ExRateFetcher.js
    
    const CURRENCY_API_URL = "https://api.currencylayer.com";
    const ACCESS_KEY = process.env.ACCESS_KEY;
    
    module.exports = require("stampit")({
      name: "ExRateFetcher",
    
      props: {
        fetch: require("node-fetch"),
    
        mongoose: require("mongoose"),
        CurrencyPairModel: null,
    
        redis: require("redis"),
        redisClient: null,
      },
    
      init() {
        const client = this.redis.createClient(process.env.REDIS_URL);
        client.on('ready', () => {
          this.redisClient = client;
        });
    
        this.mongoose.connect(process.env.MONGO_URL, { useNewUrlParser: true })
          .then(() => {
            const CurrencyPairSchema = new this.mongoose.Schema({
              _id: String, // currency pair as primary key
              rates: [{ date: String, rate: Number }]
            });
            this.CurrencyPairModel = this.mongoose.model(
              'CurrencyPair',
              CurrencyPairSchema
            );
          });
      },
    
      methods: {
        async _saveToMongo(rates, date) {
          const date = date.toISOString().substr(0, 10);
          for (const [pair, rate] of rates) {
            await this.CurrencyPairModel.upsert(
              { _id: pair, "rates.date": date },
              { $set: { rate } }
            );
          }
        },
    
        async _saveToRedis(rates) {
          for (const [pair, rate] of rates) {
            await this.redisClient.set(pair, rate);
          }
        },
    
        async fetchAndStoreLatest() {
          const responseBody = await this.fetch(`${CURRENCY_API_URL}/live?access_key=${ACCESS_KEY}`);
          const date = new Date(responseBody.timestamp * 1000);
          const rates = Object.entries(responseBody.quotes);
    
          if (this.CurrencyPairModel) {
            await this._saveToMongo(rates, date);
          }
          if (this.redisClient) {
            await this._saveToRedis(rates);
          }
        }
      }
    });
    
    使用方法はこちらExRateFetcher.js :
    const ExRateFetcher = require("./ExRateFetcher.js");
    ExRateFetcher().fetchAndStoreLatest();
    

    I / O依存


    いくつかのAPIは、巨大なJavaスプリングサーバーかもしれません.いくつかのAPIはあまりにも危険になるかもしれません.いくつかのAPIは高すぎることができます.いくつかのデータベースは、単位テスト目的(例えばKAFKA)のために簡単にロールアウトできません.いくつかのI/Oは、サードパーティ製のGRPC、UDP、またはWebSocketサーバーにすることができます.あなたのユニットテストを実行するためにこれらのいずれかを持つことはできません.
    実際の世界では、サードパーティのAPIと接続するデータベースは、あなたのCI/CD環境で利用できないかもしれません.私の経験では、I/O依存関係の半分(API、DBSなど)は、一般的に単体テストの目的では不可能です.このように

    INABILITY TO MOCK IS A CODE SMELL in node.js.


    当社のユニットテスト


    const assert = require("assert");
    const { describe, it } = require("mocha");
    
    // Let's stub two database dependencies with no-op code.
    const ExRateFetcher = require("./ExRateFetcher").props({
      // Attention! Mocking redis!
      redis: { createClient: () => ({ on() {} }) },
      // Attention! Mocking mongoose!
      mongoose: { connect: () => ({ then() {} }) },
    });
    
    describe("ExRateFetcher", () => {
      describe("#fetchAndStoreLatest", () => {
        it("should fetch", (done) => {
          const MockedFetcher = ExRateFetcher.props({
            // Attention! Mocking node-fetch!
            async fetch(uri) {
              assert(uri.includes("/live?access_key="));
              done();
            }
          });
    
          MockedFetcher().fetchAndStoreLatest();
        });
    
        const responseBody = {
          "timestamp": 1432400348,
          "quotes": {
            "USDEUR": 1.278342,
            "USDGBP": 0.908019,
          }
        };
    
        it("should store in Redis", () => {
          let redisSetCalled = 0;
          const MockedFetcher = ExRateFetcher.props({
            fetch: async () => responseBody,
    
            // Attention! Mocking redis!
            redis: {
              createClient() {
                return {
                  on(event, callback) {
                    assert(event === "ready");
                    assert(typeof callback === "function");
                    setTimeout(callback, 0);
                  },
                  async set(key, value) { // DB call mocking
                    assert(responseBody.quotes[key] === value);
                    redisSetCalled += 1;
                  }
                };
              }
            },
          });
    
          const fetcher = MockedFetcher();
          await new Promise(r => setTimeout(r, 1)); // wait connection
          await fetcher.fetchAndStoreLatest();
    
          assert(redisSetCalled === 2);
        });
    
        it("should store in MongoDB", () => {
          let mongoUpsertCalled = 0;
          const MockedFetcher = ExRateFetcher.props({
            fetch: async () => responseBody,
    
            // Attention! Mocking mongoose!
            mongoose: {
              connect() {
                return {
                  then(callback) {
                    assert(typeof callback === "function");
                    setTimeout(callback, 0);
                  }
                };
              },
              Schema: function () {},
              model: () => ({
                async upsert(query, command) { // DB call mocking
                  assert(command.$set.rate === responseBody.quotes[query._id]);
                  assert(query["rates.date"] === "2015-05-23");
                  mongoUpsertCalled += 1;
                }
              }),
            },
          });
    
          const fetcher = MockedFetcher();
          await new Promise(r => setTimeout(r, 1)); // wait connection
          await fetcher.fetchAndStoreLatest();
    
          assert(mongoUpsertCalled === 2);
        });
      });
    });
    
    見るとsinon コードベースでは、通常、多くの反復的なモッキングが起こります.
  • テスト1 -モックA、模擬B、模擬C
  • テスト2 -模擬A、模擬B、模擬C
  • テスト3 -模擬A、模擬B、模擬C
  • 一方、上記の単体テストコードでは、最低限だけを模擬します.我々は、モンクの上ではありません.また、あなたは行く必要はありませんsinon 何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も何度も
    私の経験では、上記のコードは非常に安定していて、文字通り何かをモックするのに非常に柔軟です.
    よくモックsetTimeout またはその他のJavaScript/ノード.jsグローバル.一方、ノードのグローバルを乱す.JSは非常に多くのエラーを起こしやすいと不安定な使用してproxyquire , sinon , jest , 上記のアプローチを使用してsetTimeout その特定のテストと他にどこにもない.このトリックだけで何年にもわたって私を救った.
    詳しく見るstampit モジュールを以下に示します:https://stampit.js.org/