Skip to content

Latest commit

 

History

History
2001 lines (1287 loc) · 124 KB

readme-jp.md

File metadata and controls

2001 lines (1287 loc) · 124 KB


👇 このガイドを読むとテストスキルが向上する理由


📗 非常にわかりやすく、網羅的な46以上のベストプラクティス

これは JavaScript & Node.js の信頼性のためのA-Zなガイドです。
本ガイドは、沢山の素晴らしいブログ記事、書籍などの世にある様々なツールから内容をキュレーションし、要約して作られています。

🚢 基礎なんて10000マイル以上置いてきぼりにするアドバンスドな内容

基礎はもちろんのこと、多くのアドバンスドなトピック(本番環境でのテスト・ミューテーションテスト・property-basedテスト・戦略的でプロフェッショナルなツールについてなど)まで学べる旅に出ましょう! このガイドを隅々まで読みこめば、あなたのテストスキルは並のレベルを大きく凌駕することでしょう。

🌐 フロントエンド、バックエンド、CI、その他なんでもフルスタックに

まずは、どんなアプリケーションにとっても根幹となる普遍的なテストの習慣を理解するところから始めましょう。
そして、フロントエンド/UI、バックエンド、CI、あるいはなんならその全てでも、自分の興味のある分野を探求していきましょう。


著者: Yoni Goldberg について


翻訳 - 自分の言語で読んでください



目次

他をインスパイアするたったひとつのアドバイス(1発の特別な弾丸)

綺麗なテストを構築するための土台 (12発の弾丸)

バックエンドおよびマイクロサービスのテストを効果的に書く(8発の弾丸)

コンポーネントテストやE2Eテストなどを含むWeb UIのテストを書く(11発の弾丸)

監視員を監視する - テスト品質を測る (4発の弾丸)

JSの世界におけるCIのガイドライン (9発の弾丸)



Section 0️⃣: 黄金律


⚪️ 0 黄金律: リーンなテストのためのデザイン

Do: テストコードは本番コードとは違います - 非常にシンプルで、短く、抽象さを排除し、フラットで、リーンで、書きたくなるようなものにしましょう。誰でも一目見て意図がすぐに伝わるようなテストを心がけましょう。

私たちの頭はいつもメインの本番コードのことでいっぱいなので、余計にややこしいものを追加するような「脳内のスペース」なんてありません。もしも追加で難しいコードを我々のちっぽけな脳に押し込もうなんてしようものなら、チームを遅滞させますし、それはそもそもテストを書きたかった理由と逆行していて本末転倒です。実際のところ、これが多くのチームがテストを諦めてしまう理由です。

テストとは、ある別のことのための機会だと捉えましょう。それは、一緒に協業して楽しいような、友好的で笑顔に満ち溢れたアシスタントであり、小さな投資で大きなリターンをもたらすものなのです。
科学的に人間には2つの脳のシステムがあります: システム1は、たとえばガラガラの道路を車を運転するような努力のいらない活動のために使われ、システム2は、たとえば数式を解くような複雑で意識的な操作のために使われます。
システム1で処理できるようなテストをデザインしましょう。テストコードと向き合う時には、まるでHTML文書を編集するかのような気楽さを感じられるべきであって、2X(17 × 24)という数式を解く時のようであってはいけません。

その達成のためには、テクニック、ツール、費用対効果が高いテスト対象を選択的に取捨選択するとよいでしょう。必要なものだけをテストし、敏捷性を保つことを心がけましょう。時には、信頼性を敏捷性と簡潔さと天秤にかけ、いくつかのテストを捨てることも有効です。

alt text

ここから先のアドバイスのほとんどは、この原則から派生したものです。

準備はいいですか?



Section 1: テスト解剖図


⚪ ️ 1.1 テスト名には3つの要点を含める

こうしましょう: テストレポートとは、現在のアプリケーションの変更が要件を満たせているかどうかを伝えるものでなければなりません。その要件とはコードベースに詳しいとは限らない人たちにとってのものであり、それは、テスターやデプロイをするDevOpsエンジニアや2年後のあなたです。 そのためには、テスト自体が要件レベルで話し、3つの要点を含んでいるとよいでしょう。

(1) 何がテストされているのか? たとえば、ProductsService.addNewProduct というメソッド

(2) どのような状況とシナリオを想定しているか? たとえば、 priceがメソッドに渡されなかった時

(3) どんなテスト結果を予期しているか? たとえば、 新しいproductが承認されないこと


さもなくば: デプロイが失敗し、"Add product"という名前のテストが落ちています。これで何が不具合を起こしているか正確に分かると言えますか?


👇 Note: それぞれの弾丸にはコード例がついていて、時にはイラストもあります。クリックして広げてください。

コード例

👏 正しい例: 3つの要点を満たしたテスト名

//1. テスト対象のユニット
describe('Products Service', function() {
  describe('productを追加する', function() {
    //2. シナリオ and 3. 期待する結果
    it('priceが指定されていない時、 productのステータスが承認待ちであること', ()=> {
      const newProduct = new ProductService().add(...);
      expect(newProduct.status).to.equal('pendingApproval');
    });
  });
});

👏 正しい例: 3つの要点を満たしたテスト名

alt text


© クレジット & もっと読む 1. Roy Osherove - Naming standards for unit tests



⚪ ️ 1.2 AAAパターンでテストを構成する

こうしましょう: Arrange(準備する), Act(動かす), Assert(確認する)という3つの工程でテストを構成しましょう。こうすることで、コードを読む人がテストの方針を理解するために脳内CPUを費やさずに済みます。

1つ目のA - Arrange(準備する): テストがシミュレートしたい状況をセットアップするためのコードです。これには、テストしたい対象をインスタンス化する、DBレコードを追加する、特定のオブジェクトをモック/スタブすることなどが含まれます。

2つ目のA - Act(動かす): テスト対象を動かします。 大抵は1行で済みます。

3つ目のA - Assert(確認する): 返り値が期待している結果となっているかどうかを確認します。大抵は1行で済みます。


さもなくば: メインのコードを理解するのに何時間もかかってしまうばかりか、1日のタスクの中で本来は最も簡単であるはずのテストを書くという行為で脳がくたくたになってしまいます。


コード例

👏 正しい例: AAAパターンで構成されたテスト

describe("Customer classifier", () => {
  test("カスタマーが500$費やした時, プレミアムとして識別されること", () => {
    //Arrange(準備する)
    const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
    const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });

    //Act(動かす)
    const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);

    //Assert(確認する)
    expect(receivedClassification).toMatch("premium");
  });
});

👎 アンチパターン例: 区切りがなく、ひとかたまりで、分かりにくい

test("プレミアムとして識別されること", () => {
  const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
  const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });
  const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);
  expect(receivedClassification).toMatch("premium");
});



⚪ ️1.3 プロダクト由来の言葉で期待する振る舞いを記述する: BDDスタイルのアサーションを使う

こうしましょう: 宣言的なスタイルでテストを書くことは、読者に少しも脳内CPUを使わせずに概観を掴ませる助けとなります。沢山の条件ロジックを含むような命令的なコードを書くと、読者は脳内CPUを沢山使うことを強制されてしまいます。
なので、宣言的なBDDスタイルで、 expectshould などを用い、お手製を避けつつ、人間的な言葉で期待する結果を書きましょう。
もしも、ChaiやJestが欲しいアサーションメソッドをもっておらず、そしてその欲しいアサーションメソッドが何度も使いうるものであれば、Jestのマッチャーを拡張することカスタムChaiプラグインを書くことを検討してみてください。

さもなくば: チームがテストを書かなくなり、面倒なテストを.skip()で飛ばすようになります。


コード例

👎 アンチパターン例: 読者がテストの方針を知るために、命令的なコードをそれなりの量確認しなければならない

test("アドミンを取得する時、 アドミンのみが取得結果に含まれること", () => {
  //"admin1"、 "admin2" というアドミンと、"user1"というユーザーを追加してあると仮定する
  const allAdmins = getUsers({ adminOnly: true });

  let admin1Found,
    adming2Found = false;

  allAdmins.forEach(aSingleUser => {
    if (aSingleUser === "user1") {
      assert.notEqual(aSingleUser, "user1", "A user was found and not admin");
    }
    if (aSingleUser === "admin1") {
      admin1Found = true;
    }
    if (aSingleUser === "admin2") {
      admin2Found = true;
    }
  });

  if (!admin1Found || !admin2Found) {
    throw new Error("Not all admins were returned");
  }
});

👏 正しい例: 宣言的なテストを俯瞰するのは容易いことです

it("アドミンを取得する時、 アドミンのみが取得結果に含まれること", () => { 
  // 2人アドミンを追加してあると仮定する
  const allAdmins = getUsers({ adminOnly: true });

  expect(allAdmins)
    .to.include.ordered.members(["admin1", "admin2"])
    .but.not.include.ordered.members(["user1"]);
});



⚪ ️ 1.4 ブラックボックステストを守る: パブリックメソッドのみをテストする

こうしましょう: 内部実装をテストしても大きなオーバーヘッドの割に、何も得られません。もしコードやAPIが正しい結果を返しているのなら、3時間もかけて"どのように"それが達成されたかをテストし、更にそのような壊れやすいテストをメンテしていく必要がありますか?
公開されている振る舞いがテストされている時は、常に内部実装も暗黙的にテストされていて、そのテストが壊れる時というのは何か特定の問題があった時だけです(たとえば、出力が間違っているなど)。 このようなアプローチはbehavioral testingと呼ばれます。 逆に、内部実装をテストする場合(ホワイトボックス的アプローチ) - フォーカスがコンポーネントの出力から核心的な詳細に移ります。小さなリファクタリングによって、たとえ出力結果が問題なかったとしても、テストが壊れるかもしれません。- これはメンテナンスコストを著しく向上させてしまいます。


さもなくば: テストがオオカミ少年になります: (例えば、プライベート変数の名前が変わったことでテストが壊れたなどの理由で)嘘の叫びをあげます。開発者たちがCIの通知を無視し続けてある日本当のバグが無視されてしまうようになるのも、全く驚くことではありません。


コード例

👎 アンチパターン例: 特に理由もなく内部実装をテストしている

class ProductService {
  // このメソッドは内部でしか使われていない
  // このメソッド名を変更するとテストが壊れる
  calculateVATAdd(priceWithoutVAT) {
    return { finalPrice: priceWithoutVAT * 1.2 };
    // 上記の返り値の形やキー名を変えるとテストが壊れる
  }
  //public method
  getPrice(productId) {
    const desiredProduct = DB.getProduct(productId);
    finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice;
    return finalPrice;
  }
}

it("ホワイトボックステスト: 内部メソッドが0 vatを受け取る時, 0を返す", async () => {
  // ユーザーがVATを計算できるようにしなければいけない要件はなく、最終金額が示せれば良い。それにも関わらず、クラス内部をテストすることに固執してしまっている。
  expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0);
});



⚪ ️ ️1.5 正しいテストダブルを選択する: スタブやスパイの代わりにモックを使わない

こうしましょう: テストダブルはアプリケーションの内部に結合しているため必要悪ですが、時には大きな価値をもたらします。 (テストダブルって何のことか忘れてしまった人はこちらを読みましょう: モック vs スタブ vs スパイ).

テストダブルを使う前に簡単な自問をしましょう: 私がテストしようとしている機能は仕様書に書いてある、あるいは今後書かれうることか?もし違うなら、それはホワイトボックステストになってしまっている可能性があります。

たとえば、もし決済サービスが落ちた時にどんな風にアプリケーションが振る舞うのかテストしたい時、決済サービスをスタブして'No Response'を返却させることでテスト対象が正しい値を返しているか確認することでしょう。これは特定のシナリオ下におけるアプリケーションの振る舞い、応答、結果をチェックします。あるいは、決済サービスが落ちている時にメールが送信されることをスパイを使って確認することでしょう。 - これも仕様書におそらく書かれていることの振る舞い確認です。("決済に失敗したらメールを送信する")
一方で、決済サービスをモックしてそれが正しいJavaScriptの型で呼び出されていることを確認する場合 - アプリケーションの機能とは関係のない内部のことにテストがフォーカスしてしまい、頻繁に更新しなければならないでしょう。


さもなくば: どんなリファクタリングをするにも、コード上で使われている全てのモックを探して更新することが必要になってしまいます。すると、テストは頼りがいのある親友ではなく、重荷になってしまいます。


コード例

👎 アンチパターン例: モックの関心が内部実装にある

it("有効なプロダクトが削除される時, データアクセス用のDALが正しいプロダクトと正しいコンフィグで1度だけ呼ばれること", async () => {
  //既にプロダクトを追加してあるとする
  const dataAccessMock = sinon.mock(DAL); 
  //う〜ん、よくないですね: 内部実装をテストすることがゴールになってしまっていて、ただの副作用ではなくなってしまっています。
  dataAccessMock
    .expects("deleteProduct")
    .once()
    .withArgs(DBConfig, theProductWeJustAdded, true, false);
  new ProductService().deletePrice(theProductWeJustAdded);
  dataAccessMock.verify();
});

👏正しいコード例: スパイの関心が要件をテストすることにあり、副作用は結果として内部実装に触れている

it("有効なプロダクトが削除される時, メールが送信されること", async () => {
  //既にプロダクトを追加してあるとする
  const spy = sinon.spy(Emailer.prototype, "sendEmail");
  new ProductService().deletePrice(theProductWeJustAdded);
  //う〜ん、OK: これも内部実装じゃないのかって? そうですね、でもメールを送信するという要件をテストする上での副作用としてです
  expect(spy.calledOnce).to.be.true;
});



📗 これらのプラクティスを動画で学びたいですか?

私のオンラインコースをチェックしてみてください Testing Node.js & JavaScript From A To Z



⚪ ️1.6 "foo"ではなく、リアルな入力データを使う

こうしましょう: 時にプロダクションのバグは予期せぬ非常に限定的な入力値によってもたらされます - テストの入力値がリアルであるほど、バグを早期に発見できる可能性が高まります。Fakerのような専用のライブラリを使うことで、擬似的にリアルで、プロダクションの様々な状態に似せたデータを生成しましょう。たとえば、そういうライブラリを使うとリアルっぽい電話番号、ユーザー名、クレジットカード情報、会社名、あるいは'lorem ipsum'テキストまで生成できます。fakerのデータをランダムにしてテスト対象ユニットを拡張するようなテストを作ることもできますし(通常のテストに加えてです、代わりにではなく)、あるいは実際のプロダクション環境からデータをインポートすることもできます。もっと高いレベルをみたいですか?次の弾丸をみてください(property-basedテスト)


さもなくば: "Foo"のような人工的な入力値を使っていると、開発時のテストでは誤ってグリーンになってしまうかもしれませんが、本番環境でハッカーが“@3e2ddsf . ##’ 1 fdsfds . fds432 AAAA”のような薄汚い文字列を渡してきたら、レッドになってしまうかもしれません。


コード例

👎 アンチパターン例: 人工的なデータのせいでテストスイートが通ってしまう

const addProduct = (name, price) => {
  const productNameRegexNoSpace = /^\S*$/; //空白文字列を許容しない

  if (!productNameRegexNoSpace.test(name)) return false; //入力値が簡易的なせいで、このパスには到達しない

  //なにかここにロジックがあるとする
  return true;
};

test("ダメな例: 有効なプロパティでproductを追加する時、確認に成功する", async () => {
  //"Foo"という文字列が全てのテストで使われ、永遠にfalseな結果を引き起こさない
  const addProductResult = addProduct("Foo", 5);
  expect(addProductResult).toBe(true);
  //誤った成功: 空白の入った長い文字列で試さなかったので成功してしまった
});

👏 正しい例: ランダムにリアルな入力値を使う

it("良い例: 有効なproductを追加する時、確認に成功する", async () => {
  const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
  //生成されたランダムな入力値: {'Sleek Cotton Computer',  85481}
  expect(addProductResult).to.be.true;
  //ランダムな入力値のおかげで、予期していなかったコードパスに到達してテストが失敗した。
  //バグを早期発見できた!
});



⚪ ️ 1.7 Property-basedテストで多様な入力値の組み合わせをテストする

こうしましょう: 開発者は往々にしてわずかな入力値のパターンでテストをしてしまいがちです。入力値のフォーマットが現実のデータに近い時ですら(’fooを使うな’の項を読んでください)、開発者は method(‘’, true, 1), method(“string” , false , 0) のような限られた入力値の組合せしかカバーしません。

しかし本番環境では、5個のパラメーターを持つAPIは何千もの組合せで呼び出されますし、その中の1つがプロセスを落としてしまうかもしれません。(Fuzz Testingを読んでください)。 もし、1個のテストで1000種もの入力値を自動で試し、どの入力値が正しいレスポンスを返せなかったかを把握できる、と言ったらどうですか?
Property-basedテストという技術はまさにそれをやってくれます: ありえる全ての入力値のパターンをテスト対象に流し込むことで、バグを発見する機運を高めてくれるのです。 たとえば、addNewProduct(id, name, isDiscount) というメソッドがあるとしましょう。このメソッドを使うライブラリは、(1, “iPhone”, false), (2, “Galaxy”, true) のような、(number, string, boolean)のあらゆる組合せで呼び出します。 js-verifytestcheck (こちらの方がドキュメントが断然良いです)のようなライブラリを使うと、MochaやJestなどお好みのテストランナーライブラリでproperty-basedテストを実行することができます。 Update: Nicolas Dubienさんが下記のコメントでfast-checkを見てみてくださいと提案してくれましたが、こちらのライブラリはさらにフィーチャーを提供していて、アクティブにメンテナンスされているようです。

さもなくば: ちゃんと動くコードパスしか網羅しないテスト入力値を無意識で選択してしまいます。残念ながらこれでは、バグを表出させるための相棒であるテストの効率を下げてしまいます。


コード例

👏 正しい例: "fast-check"を使って多様な入力値の組み合わせをテスト

import fc from "fast-check";

describe("Product service", () => {
  describe("Adding new", () => {
    //これはランダムなプロパティで100回走る
    it("有効な範囲内でランダムなプロパティでproductを追加する時、常に成功すること", () =>
      fc.assert(
        fc.property(fc.integer(), fc.string(), (id, name) => {
          expect(addNewProduct(id, name).status).toEqual("approved");
        })
      ));
  });
});



⚪ ️ 1.8 If needed, use only short & inline snapshots

Do: When there is a need for snapshot testing, use only short and focused snapshots (i.e. 3-7 lines) that are included as part of the test (Inline Snapshot) and not within external files. Keeping this guideline will ensure your tests remain self-explanatory and less fragile.

On the other hand, ‘classic snapshots’ tutorials and tools encourage to store big files (e.g. component rendering markup, API JSON result) over some external medium and ensure each time when the test run to compare the received result with the saved version. This, for example, can implicitly couple our test to 1000 lines with 3000 data values that the test writer never read and reasoned about. Why is this wrong? By doing so, there are 1000 reasons for your test to fail - it’s enough for a single line to change for the snapshot to get invalid and this is likely to happen a lot. How frequently? for every space, comment or minor CSS/HTML change. Not only this, the test name wouldn’t give a clue about the failure as it just checks that 1000 lines didn’t change, also it encourages to the test writer to accept as the desired true a long document he couldn’t inspect and verify. All of these are symptoms of obscure and eager test that is not focused and aims to achieve too much

It’s worth noting that there are few cases where long & external snapshots are acceptable - when asserting on schema and not data (extracting out values and focusing on fields) or when the received document rarely changes

Otherwise: A UI test fails. The code seems right, the screen renders perfect pixels, what happened? your snapshot testing just found a difference from the origin document to current received one - a single space character was added to the markdown...


Code Examples

👎 Anti-Pattern Example: Coupling our test to unseen 2000 lines of code

it("TestJavaScript.com is renderd correctly", () => {
  //Arrange

  //Act
  const receivedPage = renderer
    .create(<DisplayPage page="http://www.testjavascript.com"> Test JavaScript </DisplayPage>)
    .toJSON();

  //Assert
  expect(receivedPage).toMatchSnapshot();
  //We now implicitly maintain a 2000 lines long document
  //every additional line break or comment - will break this test
});

👏 Doing It Right Example: Expectations are visible and focused

it("When visiting TestJavaScript.com home page, a menu is displayed", () => {
  //Arrange

  //Act
  const receivedPage = renderer
    .create(<DisplayPage page="http://www.testjavascript.com"> Test JavaScript </DisplayPage>)
    .toJSON();

  //Assert

  const menu = receivedPage.content.menu;
  expect(menu).toMatchInlineSnapshot(`
<ul>
<li>Home</li>
<li> About </li>
<li> Contact </li>
</ul>
`);
});



⚪ ️1.9 Avoid global test fixtures and seeds, add data per-test

Do: Going by the golden rule (bullet 0), each test should add and act on its own set of DB rows to prevent coupling and easily reason about the test flow. In reality, this is often violated by testers who seed the DB with data before running the tests (also known as ‘test fixture’) for the sake of performance improvement. While performance is indeed a valid concern — it can be mitigated (see “Component testing” bullet), however, test complexity is a much painful sorrow that should govern other considerations most of the time. Practically, make each test case explicitly add the DB records it needs and act only on those records. If performance becomes a critical concern — a balanced compromise might come in the form of seeding the only suite of tests that are not mutating data (e.g. queries)

Otherwise: Few tests fail, a deployment is aborted, our team is going to spend precious time now, do we have a bug? let’s investigate, oh no — it seems that two tests were mutating the same seed data


Code Examples

👎 Anti-Pattern Example: tests are not independent and rely on some global hook to feed global DB data

before(async () => {
  //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework
  await DB.AddSeedDataFromJson('seed.json');
});
it("When updating site name, get successful confirmation", async () => {
  //I know that site name "portal" exists - I saw it in the seed files
  const siteToUpdate = await SiteService.getSiteByName("Portal");
  const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
  expect(updateNameResult).to.be(true);
});
it("When querying by site name, get the right site", async () => {
  //I know that site name "portal" exists - I saw it in the seed files
  const siteToCheck = await SiteService.getSiteByName("Portal");
  expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[
});

👏 Doing It Right Example: We can stay within the test, each test acts on its own set of data

it("When updating site name, get successful confirmation", async () => {
  //test is adding a fresh new records and acting on the records only
  const siteUnderTest = await SiteService.addSite({
    name: "siteForUpdateTest"
  });

  const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");

  expect(updateNameResult).to.be(true);
});

⚪ ️ 1.10 Don’t catch errors, expect them

Do: When trying to assert that some input triggers an error, it might look right to use try-catch-finally and asserts that the catch clause was entered. The result is an awkward and verbose test case (example below) that hides the simple test intent and the result expectations

A more elegant alternative is the using the one-line dedicated Chai assertion: expect(method).to.throw (or in Jest: expect(method).toThrow()). It’s absolutely mandatory to also ensure the exception contains a property that tells the error type, otherwise given just a generic error the application won’t be able to do much rather than show a disappointing message to the user

Otherwise: It will be challenging to infer from the test reports (e.g. CI reports) what went wrong


Code Examples

👎 Anti-pattern Example: A long test case that tries to assert the existence of error with try-catch

it("When no product name, it throws error 400", async () => {
  let errorWeExceptFor = null;
  try {
    const result = await addNewProduct({});
  } catch (error) {
    expect(error.code).to.equal("InvalidInput");
    errorWeExceptFor = error;
  }
  expect(errorWeExceptFor).not.to.be.null;
  //if this assertion fails, the tests results/reports will only show
  //that some value is null, there won't be a word about a missing Exception
});

👏 Doing It Right Example: A human-readable expectation that could be understood easily, maybe even by QA or technical PM

it("When no product name, it throws error 400", async () => {
  await expect(addNewProduct({}))
    .to.eventually.throw(AppError)
    .with.property("code", "InvalidInput");
});



⚪ ️ 1.11 Tag your tests

Do: Different tests must run on different scenarios: quick smoke, IO-less, tests should run when a developer saves or commits a file, full end-to-end tests usually run when a new pull request is submitted, etc. This can be achieved by tagging tests with keywords like #cold #api #sanity so you can grep with your testing harness and invoke the desired subset. For example, this is how you would invoke only the sanity test group with Mocha: mocha — grep ‘sanity’

Otherwise: Running all the tests, including tests that perform dozens of DB queries, any time a developer makes a small change can be extremely slow and keeps developers away from running tests


Code Examples

👏 Doing It Right Example: Tagging tests as ‘#cold-test’ allows the test runner to execute only fast tests (Cold===quick tests that are doing no IO and can be executed frequently even as the developer is typing)

//this test is fast (no DB) and we're tagging it correspondigly
//now the user/CI can run it frequently
describe("Order service", function() {
  describe("Add new order #cold-test #sanity", function() {
    test("Scenario - no currency was supplied. Expectation - Use the default currency #sanity", function() {
      //code logic here
    });
  });
});



⚪ ️ 1.12 Categorize tests under at least 2 levels

Do: Apply some structure to your test suite so an occasional visitor could easily understand the requirements (tests are the best documentation) and the various scenarios that are being tested. A common method for this is by placing at least 2 'describe' blocks above your tests: the 1st is for the name of the unit under test and the 2nd for additional level of categorization like the scenario or custom categories (see code examples and print screen below). Doing so will also greatly improve the test reports: The reader will easily infer the tests categories, delve into the desired section and correlate failing tests. In addition, it will get much easier for a developer to navigate through the code of a suite with many tests. There are multiple alternative structures for test suite that you may consider like given-when-then and RITE


Otherwise: When looking at a report with flat and long list of tests, the reader have to skim-read through long texts to conclude the major scenarios and correlate the commonality of failing tests. Consider the following case: When 7/100 tests fail, looking at a flat list will demand reading the failing tests text to see how they relate to each other. However, in a hierarchical report all of them could be under the same flow or category and the reader will quickly infer what or at least where is the root failure cause


Code Examples

👏 Doing It Right Example: Structuring suite with the name of unit under test and scenarios will lead to the convenient report that is shown below

// Unit under test
describe("Transfer service", () => {
  //Scenario
  describe("When no credit", () => {
    //Expectation
    test("Then the response status should decline", () => {});

    //Expectation
    test("Then it should send email to admin", () => {});
  });
});

alt text


👎 Anti-pattern Example: A flat list of tests will make it harder for the reader to identify the user stories and correlate failing tests

test("Then the response status should decline", () => {});

test("Then it should send email", () => {});

test("Then there should not be a new transfer record", () => {});

alt text




⚪ ️1.13 Other generic good testing hygiene

Do: This post is focused on testing advice that is related to, or at least can be exemplified with Node JS. This bullet, however, groups few non-Node related tips that are well-known

Learn and practice TDD principles — they are extremely valuable for many but don’t get intimidated if they don’t fit your style, you’re not the only one. Consider writing the tests before the code in a red-green-refactor style, ensure each test checks exactly one thing, when you find a bug — before fixing write a test that will detect this bug in the future, let each test fail at least once before turning green, start a module by writing a quick and simplistic code that satisfies the test - then refactor gradually and take it to a production grade level, avoid any dependency on the environment (paths, OS, etc)

Otherwise: You‘ll miss pearls of wisdom that were collected for decades



Section 2️⃣: Backend Testing

⚪ ️2.1 Enrich your testing portfolio: Look beyond unit tests and the pyramid

Do: The testing pyramid, though 10> years old, is a great and relevant model that suggests three testing types and influences most developers’ testing strategy. At the same time, more than a handful of shiny new testing techniques emerged and are hiding in the shadows of the testing pyramid. Given all the dramatic changes that we’ve seen in the recent 10 years (Microservices, cloud, serverless), is it even possible that one quite-old model will suit all types of applications? shouldn’t the testing world consider welcoming new testing techniques?

Don’t get me wrong, in 2019 the testing pyramid, TDD and unit tests are still a powerful technique and are probably the best match for many applications. Only like any other model, despite its usefulness, it must be wrong sometimes. For example, consider an IoT application that ingests many events into a message-bus like Kafka/RabbitMQ, which then flow into some data-warehouse and are eventually queried by some analytics UI. Should we really spend 50% of our testing budget on writing unit tests for an application that is integration-centric and has almost no logic? As the diversity of application types increase (bots, crypto, Alexa-skills) greater are the chances to find scenarios where the testing pyramid is not the best match.

It’s time to enrich your testing portfolio and become familiar with more testing types (the next bullets suggest few ideas), mind models like the testing pyramid but also match testing types to real-world problems that you’re facing (‘Hey, our API is broken, let’s write consumer-driven contract testing!’), diversify your tests like an investor that build a portfolio based on risk analysis — assess where problems might arise and match some prevention measures to mitigate those potential risks

A word of caution: the TDD argument in the software world takes a typical false-dichotomy face, some preach to use it everywhere, others think it’s the devil. Everyone who speaks in absolutes is wrong :]


Otherwise: You’re going to miss some tools with amazing ROI, some like Fuzz, lint, and mutation can provide value in 10 minutes


Code Examples

👏 Doing It Right Example: Cindy Sridharan suggests a rich testing portfolio in her amazing post ‘Testing Microservices — the same way’

alt text

☺️Example: YouTube: “Beyond Unit Tests: 5 Shiny Node.JS Test Types (2018)” (Yoni Goldberg)


alt text



⚪ ️2.2 Component testing might be your best affair

Do: Each unit test covers a tiny portion of the application and it’s expensive to cover the whole, whereas end-to-end testing easily covers a lot of ground but is flaky and slower, why not apply a balanced approach and write tests that are bigger than unit tests but smaller than end-to-end testing? Component testing is the unsung song of the testing world — they provide the best from both worlds: reasonable performance and a possibility to apply TDD patterns + realistic and great coverage.

Component tests focus on the Microservice ‘unit’, they work against the API, don’t mock anything which belongs to the Microservice itself (e.g. real DB, or at least the in-memory version of that DB) but stub anything that is external like calls to other Microservices. By doing so, we test what we deploy, approach the app from outwards to inwards and gain great confidence in a reasonable amount of time.

Otherwise: You may spend long days on writing unit tests to find out that you got only 20% system coverage


Code Examples

👏 Doing It Right Example: Supertest allows approaching Express API in-process (fast and cover many layers)

alt text



⚪ ️2.3 Ensure new releases don’t break the API using contract tests

Do: So your Microservice has multiple clients, and you run multiple versions of the service for compatibility reasons (keeping everyone happy). Then you change some field and ‘boom!’, some important client who relies on this field is angry. This is the Catch-22 of the integration world: It’s very challenging for the server side to consider all the multiple client expectations — On the other hand, the clients can’t perform any testing because the server controls the release dates. Consumer-driven contracts and the framework PACT were born to formalize this process with a very disruptive approach — not the server defines the test plan of itself rather the client defines the tests of the… server! PACT can record the client expectation and put in a shared location, “broker”, so the server can pull the expectations and run on every build using PACT library to detect broken contracts — a client expectation that is not met. By doing so, all the server-client API mismatches are caught early during build/CI and might save you a great deal of frustration

Otherwise: The alternatives are exhausting manual testing or deployment fear


Code Examples

👏 Doing It Right Example:

alt text



⚪ ️ 2.4 Test your middlewares in isolation

Do: Many avoid Middleware testing because they represent a small portion of the system and require a live Express server. Both reasons are wrong — Middlewares are small but affect all or most of the requests and can be tested easily as pure functions that get {req,res} JS objects. To test a middleware function one should just invoke it and spy (using Sinon for example) on the interaction with the {req,res} objects to ensure the function performed the right action. The library node-mock-http takes it even further and factors the {req,res} objects along with spying on their behavior. For example, it can assert whether the http status that was set on the res object matches the expectation (See example below)

Otherwise: A bug in Express middleware === a bug in all or most requests


Code Examples

👏Doing It Right Example: Testing middleware in isolation without issuing network calls and waking-up the entire Express machine

//the middleware we want to test
const unitUnderTest = require("./middleware");
const httpMocks = require("node-mocks-http");
//Jest syntax, equivelant to describe() & it() in Mocha
test("A request without authentication header, should return http status 403", () => {
  const request = httpMocks.createRequest({
    method: "GET",
    url: "/user/42",
    headers: {
      authentication: ""
    }
  });
  const response = httpMocks.createResponse();
  unitUnderTest(request, response);
  expect(response.statusCode).toBe(403);
});



⚪ ️2.5 Measure and refactor using static analysis tools

Do: Using static analysis tools helps by giving objective ways to improve code quality and keep your code maintainable. You can add static analysis tools to your CI build to abort when it finds code smells. Its main selling points over plain linting are the ability to inspect quality in the context of multiple files (e.g. detect duplications), perform advanced analysis (e.g. code complexity) and follow the history and progress of code issues. Two examples of tools you can use are SonarQube (4,900+ stars) and Code Climate (2,000+ stars)

Credit: Keith Holliday


Otherwise: With poor code quality, bugs and performance will always be an issue that no shiny new library or state of the art features can fix


Code Examples

👏 Doing It Right Example: CodeClimate, a commercial tool that can identify complex methods:

alt text



⚪ ️ 2.6 Check your readiness for Node-related chaos

Do: Weirdly, most software testings are about logic & data only, but some of the worst things that happen (and are really hard to mitigate) are infrastructural issues. For example, did you ever test what happens when your process memory is overloaded, or when the server/process dies, or does your monitoring system realizes when the API becomes 50% slower?. To test and mitigate these type of bad things — Chaos engineering was born by Netflix. It aims to provide awareness, frameworks and tools for testing our app resiliency for chaotic issues. For example, one of its famous tools, the chaos monkey, randomly kills servers to ensure that our service can still serve users and not relying on a single server (there is also a Kubernetes version, kube-monkey, that kills pods). All these tools work on the hosting/platform level, but what if you wish to test and generate pure Node chaos like check how your Node process copes with uncaught errors, unhandled promise rejection, v8 memory overloaded with the max allowed of 1.7GB or whether your UX remains satisfactory when the event loop gets blocked often? to address this I’ve written, node-chaos (alpha) which provides all sort of Node-related chaotic acts

Otherwise: No escape here, Murphy’s law will hit your production without mercy


Code Examples

👏 Doing It Right Example: : Node-chaos can generate all sort of Node.js pranks so you can test how resilience is your app to chaos

alt text


⚪ ️2.7 Avoid global test fixtures and seeds, add data per-test

Do: Going by the golden rule (bullet 0), each test should add and act on its own set of DB rows to prevent coupling and easily reason about the test flow. In reality, this is often violated by testers who seed the DB with data before running the tests (also known as ‘test fixture’) for the sake of performance improvement. While performance is indeed a valid concern — it can be mitigated (see “Component testing” bullet), however, test complexity is a much painful sorrow that should govern other considerations most of the time. Practically, make each test case explicitly add the DB records it needs and act only on those records. If performance becomes a critical concern — a balanced compromise might come in the form of seeding the only suite of tests that are not mutating data (e.g. queries)

Otherwise: Few tests fail, a deployment is aborted, our team is going to spend precious time now, do we have a bug? let’s investigate, oh no — it seems that two tests were mutating the same seed data


Code Examples

👎 Anti-Pattern Example: tests are not independent and rely on some global hook to feed global DB data

before(async () => {
  //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework
  await DB.AddSeedDataFromJson('seed.json');
});
it("When updating site name, get successful confirmation", async () => {
  //I know that site name "portal" exists - I saw it in the seed files
  const siteToUpdate = await SiteService.getSiteByName("Portal");
  const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
  expect(updateNameResult).to.be(true);
});
it("When querying by site name, get the right site", async () => {
  //I know that site name "portal" exists - I saw it in the seed files
  const siteToCheck = await SiteService.getSiteByName("Portal");
  expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[
});

👏 Doing It Right Example: We can stay within the test, each test acts on its own set of data

it("When updating site name, get successful confirmation", async () => {
  //test is adding a fresh new records and acting on the records only
  const siteUnderTest = await SiteService.addSite({
    name: "siteForUpdateTest"
  });
  const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");
  expect(updateNameResult).to.be(true);
});



Section 3️⃣: Frontend Testing

⚪ ️ 3.1 Separate UI from functionality

Do: When focusing on testing component logic, UI details become a noise that should be extracted, so your tests can focus on pure data. Practically, extract the desired data from the markup in an abstract way that is not too coupled to the graphic implementation, assert only on pure data (vs HTML/CSS graphic details) and disable animations that slow down. You might get tempted to avoid rendering and test only the back part of the UI (e.g. services, actions, store) but this will result in fictional tests that don't resemble the reality and won't reveal cases where the right data doesn't even arrive in the UI


Otherwise: The pure calculated data of your test might be ready in 10ms, but then the whole test will last 500ms (100 tests = 1 min) due to some fancy and irrelevant animation


Code Examples

👏 Doing It Right Example: Separating out the UI details

test("When users-list is flagged to show only VIP, should display only VIP members", () => {
  // Arrange
  const allUsers = [{ id: 1, name: "Yoni Goldberg", vip: false }, { id: 2, name: "John Doe", vip: true }];

  // Act
  const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true} />);

  // Assert - Extract the data from the UI first
  const allRenderedUsers = getAllByTestId("user").map(uiElement => uiElement.textContent);
  const allRealVIPUsers = allUsers.filter(user => user.vip).map(user => user.name);
  expect(allRenderedUsers).toEqual(allRealVIPUsers); //compare data with data, no UI here
});

👎 Anti-Pattern Example: Assertion mix UI details and data

test("When flagging to show only VIP, should display only VIP members", () => {
  // Arrange
  const allUsers = [{ id: 1, name: "Yoni Goldberg", vip: false }, { id: 2, name: "John Doe", vip: true }];

  // Act
  const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true} />);

  // Assert - Mix UI & data in assertion
  expect(getAllByTestId("user")).toEqual('[<li data-test-id="user">John Doe</li>]');
});



⚪ ️ 3.2 Query HTML elements based on attributes that are unlikely to change

Do: Query HTML elements based on attributes that are likely to survive graphic changes unlike CSS selectors and like form labels. If the designated element doesn't have such attributes, create a dedicated test attribute like 'test-id-submit-button'. Going this route not only ensures that your functional/logic tests never break because of look & feel changes but also it becomes clear to the entire team that this element and attribute are utilized by tests and shouldn't get removed


Otherwise: You want to test the login functionality that spans many components, logic and services, everything is set up perfectly - stubs, spies, Ajax calls are isolated. All seems perfect. Then the test fails because the designer changed the div CSS class from 'thick-border' to 'thin-border'


Code Examples

👏 Doing It Right Example: Querying an element using a dedicated attribute for testing

// the markup code (part of React component)
<h3>
  <Badge pill className="fixed_badge" variant="dark">
    <span data-test-id="errorsLabel">{value}</span>
    <!-- note the attribute data-test-id -->
  </Badge>
</h3>
// this example is using react-testing-library
test("Whenever no data is passed to metric, show 0 as default", () => {
  // Arrange
  const metricValue = undefined;

  // Act
  const { getByTestId } = render(<dashboardMetric value={undefined} />);

  expect(getByTestId("errorsLabel").text()).toBe("0");
});

👎 Anti-Pattern Example: Relying on CSS attributes

<!-- the markup code (part of React component) -->
<span id="metric" className="d-flex-column">{value}</span>
<!-- what if the designer changes the classs? -->
// this exammple is using enzyme
test("Whenever no data is passed, error metric shows zero", () => {
  // ...

  expect(wrapper.find("[className='d-flex-column']").text()).toBe("0");
});

⚪ ️ 3.3 Whenever possible, test with a realistic and fully rendered component

Do: Whenever reasonably sized, test your component from outside like your users do, fully render the UI, act on it and assert that the rendered UI behaves as expected. Avoid all sort of mocking, partial and shallow rendering - this approach might result in untrapped bugs due to lack of details and harden the maintenance as the tests mess with the internals (see bullet 'Favour blackbox testing'). If one of the child components is significantly slowing down (e.g. animation) or complicating the setup - consider explicitly replacing it with a fake

With all that said, a word of caution is in order: this technique works for small/medium components that pack a reasonable size of child components. Fully rendering a component with too many children will make it hard to reason about test failures (root cause analysis) and might get too slow. In such cases, write only a few tests against that fat parent component and more tests against its children


Otherwise: When poking into a component's internal by invoking its private methods, and checking the inner state - you would have to refactor all tests when refactoring the components implementation. Do you really have a capacity for this level of maintenance?


Code Examples

👏 Doing It Right Example: Working realistically with a fully rendered component

class Calendar extends React.Component {
  static defaultProps = { showFilters: false };

  render() {
    return (
      <div>
        A filters panel with a button to hide/show filters
        <FiltersPanel showFilter={showFilters} title="Choose Filters" />
      </div>
    );
  }
}

//Examples use React & Enzyme
test("Realistic approach: When clicked to show filters, filters are displayed", () => {
  // Arrange
  const wrapper = mount(<Calendar showFilters={false} />);

  // Act
  wrapper.find("button").simulate("click");

  // Assert
  expect(wrapper.text().includes("Choose Filter"));
  // This is how the user will approach this element: by text
});

👎 Anti-Pattern Example: Mocking the reality with shallow rendering

test("Shallow/mocked approach: When clicked to show filters, filters are displayed", () => {
  // Arrange
  const wrapper = shallow(<Calendar showFilters={false} title="Choose Filter" />);

  // Act
  wrapper
    .find("filtersPanel")
    .instance()
    .showFilters();
  // Tap into the internals, bypass the UI and invoke a method. White-box approach

  // Assert
  expect(wrapper.find("Filter").props()).toEqual({ title: "Choose Filter" });
  // what if we change the prop name or don't pass anything relevant?
});

⚪ ️ 3.4 Don't sleep, use frameworks built-in support for async events. Also try to speed things up

Do: In many cases, the unit under test completion time is just unknown (e.g. animation suspends element appearance) - in that case, avoid sleeping (e.g. setTimeOut) and prefer more deterministic methods that most platforms provide. Some libraries allows awaiting on operations (e.g. Cypress cy.request('url')), other provide API for waiting like @testing-library/dom method wait(expect(element)). Sometimes a more elegant way is to stub the slow resource, like API for example, and then once the response moment becomes deterministic the component can be explicitly re-rendered. When depending upon some external component that sleeps, it might turn useful to hurry-up the clock. Sleeping is a pattern to avoid because it forces your test to be slow or risky (when waiting for a too short period). Whenever sleeping and polling is inevitable and there's no support from the testing framework, some npm libraries like wait-for-expect can help with a semi-deterministic solution

Otherwise: When sleeping for a long time, tests will be an order of magnitude slower. When trying to sleep for small numbers, test will fail when the unit under test didn't respond in a timely fashion. So it boils down to a trade-off between flakiness and bad performance


Code Examples

👏 Doing It Right Example: E2E API that resolves only when the async operations is done (Cypress)

// using Cypress
cy.get("#show-products").click(); // navigate
cy.wait("@products"); // wait for route to appear
// this line will get executed only when the route is ready

👏 Doing It Right Example: Testing library that waits for DOM elements

// @testing-library/dom
test("movie title appears", async () => {
  // element is initially not present...

  // wait for appearance
  await wait(() => {
    expect(getByText("the lion king")).toBeInTheDocument();
  });

  // wait for appearance and return the element
  const movie = await waitForElement(() => getByText("the lion king"));
});

👎 Anti-Pattern Example: custom sleep code

test("movie title appears", async () => {
  // element is initially not present...

  // custom wait logic (caution: simplistic, no timeout)
  const interval = setInterval(() => {
    const found = getByText("the lion king");
    if (found) {
      clearInterval(interval);
      expect(getByText("the lion king")).toBeInTheDocument();
    }
  }, 100);

  // wait for appearance and return the element
  const movie = await waitForElement(() => getByText("the lion king"));
});

⚪ ️ 3.5 Watch how the content is served over the network

Do: Apply some active monitor that ensures the page load under real network is optimized - this includes any UX concern like slow page load or un-minified bundle. The inspection tools market is no short: basic tools like pingdom, AWS CloudWatch, gcp StackDriver can be easily configured to watch whether the server is alive and response under a reasonable SLA. This only scratches the surface of what might get wrong, hence it's preferable to opt for tools that specialize in frontend (e.g. lighthouse, pagespeed) and perform richer analysis. The focus should be on symptoms, metrics that directly affect the UX, like page load time, meaningful paint, time until the page gets interactive (TTI). On top of that, one may also watch for technical causes like ensuring the content is compressed, time to the first byte, optimize images, ensuring reasonable DOM size, SSL and many others. It's advisable to have these rich monitors both during development, as part of the CI and most important - 24x7 over the production's servers/CDN


Otherwise: It must be disappointing to realize that after such great care for crafting a UI, 100% functional tests passing and sophisticated bundling - the UX is horrible and slow due to CDN misconfiguration


Code Examples

👏 Doing It Right Example: Lighthouse page load inspection report


⚪ ️ 3.6 Stub flaky and slow resources like backend APIs

Do: When coding your mainstream tests (not E2E tests), avoid involving any resource that is beyond your responsibility and control like backend API and use stubs instead (i.e. test double). Practically, instead of real network calls to APIs, use some test double library (like Sinon, Test doubles, etc) for stubbing the API response. The main benefit is preventing flakiness - testing or staging APIs by definition are not highly stable and from time to time will fail your tests although YOUR component behaves just fine (production env was not meant for testing and it usually throttles requests). Doing this will allow simulating various API behavior that should drive your component behavior as when no data was found or the case when API throws an error. Last but not least, network calls will greatly slow down the tests


Otherwise: The average test runs no longer than few ms, a typical API call last 100ms>, this makes each test ~20x slower


Code Examples

👏 Doing It Right Example: Stubbing or intercepting API calls

// unit under test
export default function ProductsList() {
  const [products, setProducts] = useState(false);

  const fetchProducts = async () => {
    const products = await axios.get("api/products");
    setProducts(products);
  };

  useEffect(() => {
    fetchProducts();
  }, []);

  return products ? <div>{products}</div> : <div data-test-id="no-products-message">No products</div>;
}

// test
test("When no products exist, show the appropriate message", () => {
  // Arrange
  nock("api")
    .get(`/products`)
    .reply(404);

  // Act
  const { getByTestId } = render(<ProductsList />);

  // Assert
  expect(getByTestId("no-products-message")).toBeTruthy();
});

⚪ ️ 3.7 Have very few end-to-end tests that spans the whole system

Do: Although E2E (end-to-end) usually means UI-only testing with a real browser (See bullet 3.6), for other they mean tests that stretch the entire system including the real backend. The latter type of tests is highly valuable as they cover integration bugs between frontend and backend that might happen due to a wrong understanding of the exchange schema. They are also an efficient method to discover backend-to-backend integration issues (e.g. Microservice A sends the wrong message to Microservice B) and even to detect deployment failures - there are no backend frameworks for E2E testing that are as friendly and mature as UI frameworks like Cypress and Puppeteer. The downside of such tests is the high cost of configuring an environment with so many components, and mostly their brittleness - given 50 microservices, even if one fails then the entire E2E just failed. For that reason, we should use this technique sparingly and probably have 1-10 of those and no more. That said, even a small number of E2E tests are likely to catch the type of issues they are targeted for - deployment & integration faults. It's advisable to run those over a production-like staging environment


Otherwise: UI might invest much in testing its functionality only to realizes very late that the backend returned payload (the data schema the UI has to work with) is very different than expected


⚪ ️ 3.8 Speed-up E2E tests by reusing login credentials

Do: In E2E tests that involve a real backend and rely on a valid user token for API calls, it doesn't payoff to isolate the test to a level where a user is created and logged-in in every request. Instead, login only once before the tests execution start (i.e. before-all hook), save the token in some local storage and reuse it across requests. This seem to violate one of the core testing principle - keep the test autonomous without resources coupling. While this is a valid worry, in E2E tests performance is a key concern and creating 1-3 API requests before starting each individual tests might lead to horrible execution time. Reusing credentials doesn't mean the tests have to act on the same user records - if relying on user records (e.g. test user payments history) than make sure to generate those records as part of the test and avoid sharing their existence with other tests. Also remember that the backend can be faked - if your tests are focused on the frontend it might be better to isolate it and stub the backend API (see bullet 3.6).


Otherwise: Given 200 test cases and assuming login=100ms = 20 seconds only for logging-in again and again


Code Examples

👏 Doing It Right Example: Logging-in before-all and not before-each

let authenticationToken;

// happens before ALL tests run
before(() => {
  cy.request('POST', 'http://localhost:3000/login', {
    username: Cypress.env('username'),
    password: Cypress.env('password'),
  })
  .its('body')
  .then((responseFromLogin) => {
    authenticationToken = responseFromLogin.token;
  })
})

// happens before EACH test
beforeEach(setUser => () {
  cy.visit('/home', {
    onBeforeLoad (win) {
      win.localStorage.setItem('token', JSON.stringify(authenticationToken))
    },
  })
})

⚪ ️ 3.9 Have one E2E smoke test that just travels across the site map

Do: For production monitoring and development-time sanity check, run a single E2E test that visits all/most of the site pages and ensures no one breaks. This type of test brings a great return on investment as it's very easy to write and maintain, but it can detect any kind of failure including functional, network and deployment issues. Other styles of smoke and sanity checking are not as reliable and exhaustive - some ops teams just ping the home page (production) or developers who run many integration tests which don't discover packaging and browser issues. Goes without saying that the smoke test doesn't replace functional tests rather just aim to serve as a quick smoke detector


Otherwise: Everything might seem perfect, all tests pass, production health-check is also positive but the Payment component had some packaging issue and only the /Payment route is not rendering


Code Examples

👏 Doing It Right Example: Smoke travelling across all pages

it("When doing smoke testing over all page, should load them all successfully", () => {
  // exemplified using Cypress but can be implemented easily
  // using any E2E suite
  cy.visit("https://mysite.com/home");
  cy.contains("Home");
  cy.contains("https://mysite.com/Login");
  cy.contains("Login");
  cy.contains("https://mysite.com/About");
  cy.contains("About");
});

⚪ ️ 3.10 Expose the tests as a live collaborative document

Do: Besides increasing app reliability, tests bring another attractive opportunity to the table - serve as live app documentation. Since tests inherently speak at a less-technical and product/UX language, using the right tools they can serve as a communication artifact that greatly aligns all the peers - developers and their customers. For example, some frameworks allow expressing the flow and expectations (i.e. tests plan) using a human-readable language so any stakeholder, including product managers, can read, approve and collaborate on the tests which just became the live requirements document. This technique is also being referred to as 'acceptance test' as it allows the customer to define his acceptance criteria in plain language. This is BDD (behavior-driven testing) at its purest form. One of the popular frameworks that enable this is Cucumber which has a JavaScript flavor, see example below. Another similar yet different opportunity, StoryBook, allows exposing UI components as a graphic catalog where one can walk through the various states of each component (e.g. render a grid w/o filters, render that grid with multiple rows or with none, etc), see how it looks like, and how to trigger that state - this can appeal also to product folks but mostly serves as live doc for developers who consume those components.

Otherwise: After investing top resources on testing, it's just a pity not to leverage this investment and win great value


Code Examples

👏 Doing It Right Example: Describing tests in human-language using cucumber-js

// this is how one can describe tests using cucumber: plain language that allows anyone to understand and collaborate

Feature: Twitter new tweet

  I want to tweet something in Twitter

  @focus
  Scenario: Tweeting from the home page
    Given I open Twitter home
    Given I click on "New tweet" button
    Given I type "Hello followers!" in the textbox
    Given I click on "Submit" button
    Then I see message "Tweet saved"

👏 Doing It Right Example: Visualizing our components, their various states and inputs using Storybook

alt text



⚪ ️ 3.11 Detect visual issues with automated tools

Do: Setup automated tools to capture UI screenshots when changes are presented and detect visual issues like content overlapping or breaking. This ensures that not only the right data is prepared but also the user can conveniently see it. This technique is not widely adopted, our testing mindset leans toward functional tests but it's the visuals what the user experience and with so many device types it's very easy to overlook some nasty UI bug. Some free tools can provide the basics - generate and save screenshots for the inspection of human eyes. While this approach might be sufficient for small apps, it's flawed as any other manual testing that demands human labor anytime something changes. On the other hand, it's quite challenging to detect UI issues automatically due to the lack of clear definition - this is where the field of 'Visual Regression' chime in and solve this puzzle by comparing old UI with the latest changes and detect differences. Some OSS/free tools can provide some of this functionality (e.g. wraith, PhantomCSS but might charge significant setup time. The commercial line of tools (e.g. Applitools, Percy.io) takes is a step further by smoothing the installation and packing advanced features like management UI, alerting, smart capturing by eliminating 'visual noise' (e.g. ads, animations) and even root cause analysis of the DOM/CSS changes that led to the issue


Otherwise: How good is a content page that display great content (100% tests passed), loads instantly but half of the content area is hidden?


Code Examples

👎 Anti-Pattern Example: A typical visual regression - right content that is served badly

alt text


👏 Doing It Right Example: Configuring wraith to capture and compare UI snapshots

​# Add as many domains as necessary. Key will act as a label​

domains:
  english: "http://www.mysite.com"​

​# Type screen widths below, here are a couple of examples​

screen_widths:

  - 600​
  - 768​
  - 1024​
  - 1280​

​# Type page URL paths below, here are a couple of examples​
paths:
  about:
    path: /about
    selector: '.about'​
  subscribe:
      selector: '.subscribe'​
    path: /subscribe

👏 Doing It Right Example: Using Applitools to get snapshot comparison and other advanced features

import * as todoPage from "../page-objects/todo-page";

describe("visual validation", () => {
  before(() => todoPage.navigate());
  beforeEach(() => cy.eyesOpen({ appName: "TAU TodoMVC" }));
  afterEach(() => cy.eyesClose());

  it("should look good", () => {
    cy.eyesCheckWindow("empty todo list");
    todoPage.addTodo("Clean room");
    todoPage.addTodo("Learn javascript");
    cy.eyesCheckWindow("two todos");
    todoPage.toggleTodo(0);
    cy.eyesCheckWindow("mark as completed");
  });
});



Section 4️⃣: Measuring Test Effectiveness



⚪ ️ 4.1 Get enough coverage for being confident, ~80% seems to be the lucky number

Do: The purpose of testing is to get enough confidence for moving fast, obviously the more code is tested the more confident the team can be. Coverage is a measure of how many code lines (and branches, statements, etc) are being reached by the tests. So how much is enough? 10–30% is obviously too low to get any sense about the build correctness, on the other side 100% is very expensive and might shift your focus from the critical paths to the exotic corners of the code. The long answer is that it depends on many factors like the type of application — if you’re building the next generation of Airbus A380 than 100% is a must, for a cartoon pictures website 50% might be too much. Although most of the testing enthusiasts claim that the right coverage threshold is contextual, most of them also mention the number 80% as a thumb of a rule (Fowler: “in the upper 80s or 90s”) that presumably should satisfy most of the applications.

Implementation tips: You may want to configure your continuous integration (CI) to have a coverage threshold (Jest link) and stop a build that doesn’t stand to this standard (it’s also possible to configure threshold per component, see code example below). On top of this, consider detecting build coverage decrease (when a newly committed code has less coverage) — this will push developers raising or at least preserving the amount of tested code. All that said, coverage is only one measure, a quantitative based one, that is not enough to tell the robustness of your testing. And it can also be fooled as illustrated in the next bullets


Otherwise: Confidence and numbers go hand in hand, without really knowing that you tested most of the system — there will also be some fear and fear will slow you down


Code Examples

👏 Example: A typical coverage report

alt text


👏 Doing It Right Example: Setting up coverage per component (using Jest)

alt text



⚪ ️ 4.2 Inspect coverage reports to detect untested areas and other oddities

Do: Some issues sneak just under the radar and are really hard to find using traditional tools. These are not really bugs but more of surprising application behavior that might have a severe impact. For example, often some code areas are never or rarely being invoked — you thought that the ‘PricingCalculator’ class is always setting the product price but it turns out it is actually never invoked although we have 10000 products in DB and many sales… Code coverage reports help you realize whether the application behaves the way you believe it does. Other than that, it can also highlight which types of code is not tested — being informed that 80% of the code is tested doesn’t tell whether the critical parts are covered. Generating reports is easy — just run your app in production or during testing with coverage tracking and then see colorful reports that highlight how frequent each code area is invoked. If you take your time to glimpse into this data — you might find some gotchas

Otherwise: If you don’t know which parts of your code are left un-tested, you don’t know where the issues might come from


Code Examples

👎 Anti-Pattern Example: What’s wrong with this coverage report?

Based on a real-world scenario where we tracked our application usage in QA and find out interesting login patterns (Hint: the amount of login failures is non-proportional, something is clearly wrong. Finally it turned out that some frontend bug keeps hitting the backend login API)

alt text



⚪ ️ 4.3 Measure logical coverage using mutation testing

Do: The Traditional Coverage metric often lies: It may show you 100% code coverage, but none of your functions, even not one, return the right response. How come? it simply measures over which lines of code the test visited, but it doesn’t check if the tests actually tested anything — asserted for the right response. Like someone who’s traveling for business and showing his passport stamps — this doesn’t prove any work done, only that he visited few airports and hotels.

Mutation-based testing is here to help by measuring the amount of code that was actually TESTED not just VISITED. Stryker is a JavaScript library for mutation testing and the implementation is really neat:

(1) it intentionally changes the code and “plants bugs”. For example the code newOrder.price===0 becomes newOrder.price!=0. This “bugs” are called mutations

(2) it runs the tests, if all succeed then we have a problem — the tests didn’t serve their purpose of discovering bugs, the mutations are so-called survived. If the tests failed, then great, the mutations were killed.

Knowing that all or most of the mutations were killed gives much higher confidence than traditional coverage and the setup time is similar

Otherwise: You’ll be fooled to believe that 85% coverage means your test will detect bugs in 85% of your code


Code Examples

👎 Anti-Pattern Example: 100% coverage, 0% testing

function addNewOrder(newOrder) {
  logger.log(`Adding new order ${newOrder}`);
  DB.save(newOrder);
  Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`);

  return { approved: true };
}

it("Test addNewOrder, don't use such test names", () => {
  addNewOrder({ assignee: "[email protected]", price: 120 });
}); //Triggers 100% code coverage, but it doesn't check anything

👏 Doing It Right Example: Stryker reports, a tool for mutation testing, detects and counts the amount of code that is not tested (Mutations)

alt text



⚪ ️4.4 Preventing test code issues with Test linters

Do: A set of ESLint plugins were built specifically for inspecting the tests code patterns and discover issues. For example, eslint-plugin-mocha will warn when a test is written at the global level (not a son of a describe() statement) or when tests are skipped which might lead to a false belief that all tests are passing. Similarly, eslint-plugin-jest can, for example, warn when a test has no assertions at all (not checking anything)


Otherwise: Seeing 90% code coverage and 100% green tests will make your face wear a big smile only until you realize that many tests aren’t asserting for anything and many test suites were just skipped. Hopefully, you didn’t deploy anything based on this false observation


Code Examples

👎 Anti-Pattern Example: A test case full of errors, luckily all are caught by Linters

describe("Too short description", () => {
  const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead
  it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words
});

it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite
  expect("somevalue"); // error:no-assert
});

it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests
});



Section 5️⃣: CI 及びその他の品質基準



⚪ ️ 5.1 リンターを充実させ、リントに問題がある時はビルドを止める

こうしましょう: リンターはフリーランチです。5分のセットアップで、コードを守る自動操縦装置を無料で手に入れることができ、重要な問題をキャッチすることができます。 リンティングが装飾のためのものだった時代はもう終わりました。現在ではリンターは、正しくスローされないことによって情報が失われてしまうエラーのような深刻な問題を検知することができます。 ESLint standardAirbnb style のような基本的なルールセットに加え、eslint-plugin-chai-expect はアサーションのないテストを、eslint-plugin-promise は resolve しない promise を、eslint-plugin-security は DOS 攻撃に使われる可能性のある正規表現を、eslint-plugin-you-dont-need-lodash-underscore は Lodash の_.map(…)ような V8 コアメソッドの一部であるユーティリティーライブラリメソッドをコードが使用している場合に警告することができます。

さもなくば: 雨の日に、本番環境がクラッシュし続けているのに、ログにはエラーのスタックトレースが表示されていない場合を考えてみましょう。何が起こったのでしょうか?あなたのコードが誤ってエラーではないオブジェクトを投げてしまい、スタックトレースが失われたのです。そんなことが起こった日には、壁に頭を打ち付けたくなりますよね。5分間のリンターのセットアップでこのタイポを検出し、あなたの一日を救うことができます。


コード例

👎 アンチパターン例: 間違ったエラーオブジェクトが誤ってスローされ、このエラーのスタックトレースは表示されません。幸運なことに、ESLint は次の本番環境でのバグをキャッチします。

alt text



⚪ ️ 5.2 ローカルでの開発者とCIのフィードバックループを短くする

こうしましょう: テスト、リンティング、脆弱性チェックなどの品質検査がピカイチのCIを使っていますか? 開発者がパイプラインをローカルで実行し即座にフィードバックを得られるようにして、フィードバックループ を短くしましょう。なぜか? 効率的なテストプロセスは、多くの反復的なループを構成しているからです。(1)トライアウト -> (2)フィードバック -> (3)リファクタリング。フィードバックが早ければ早いほど、開発者はモジュールごとに改善の反復回数が増え、結果を完璧にすることができます。逆に、フィードバックが遅くなると、1日にできる改善の反復回数が少なくなり、チームはすでに別のトピック/タスク/モジュールに進んでしまい、そのモジュールを改善する気にならないかもしれません。

実際に、いくつかのCIベンダー(例:CircleCI local CLI) は、パイプラインをローカルで実行することができます。wallaby のようないくつかの商用ツールは、開発者のプロトタイプとして非常に価値のあるテスト用のインサイトを提供しています。または、すべての品質チェックのコマンド(例:テスト、リント、脆弱性チェック)を実行するnpmスクリプトをpackage.jsonに追加するだけでも構いません。並列化のために concurrently のようなツールを使用し、ツールの1つが失敗した場合でも0以外の終了コードを返すようにしましょう。開発者は「npm run quality」などのコマンドを実行するだけで、即座にフィードバックを得ることができます。githookを使って品質チェックに失敗したときにコミットを中止することも検討してみましょう(husky が使えます)。

さもなくば: 品質チェックの結果がコードの翌日に出るようでは、テストは開発の一部ではなく、後付の形式的な成果物になってしまいます。


コード例

👏 正しい例: コード品質のチェックを行うnpmスクリプトは、手動または開発者が新しいコードをプッシュしようとしているときに自動ですべて並行して実行されます。

"scripts": {
    "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"",
    "inspect:lint": "eslint .",
    "inspect:vulnerabilities": "npm audit",
    "inspect:license": "license-checker --failOn GPLv2",
    "inspect:complexity": "plato .",

    "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\""
  },
  "husky": {
    "hooks": {
      "precommit": "npm run inspect:all",
      "prepush": "npm run inspect:all"
    }
}



⚪ ️5.3 本番環境のミラーでのe2eテストの実施

こうしましょう: エンドツーエンド (e2e) テスティングは、すべてのCIパイプラインの主な課題です - 本番環境と同一の一時的な環境を、関連するすべてのクラウド・サービスと一緒にその場で作成するのは面倒でコストがかかります。最適な妥協案を見つけるのがあなたの仕事です: Docker-compose は、1つのプレーンなテキストファイルを使用して、同一のコンテナで隔離されたdocker環境を作ることができますが、裏側の技術(例: ネットワークやデプロイメントモデル)は、実際の本番環境とは異なります。AWS Local と組み合わせることで、実際のAWSサービスのスタブを利用することができます。サーバーレスにした場合は、serverlessAWS SAM などの複数のフレームワークにより、FaaSコードのローカル起動が可能になります。

巨大なKubernetesのエコシステムでは、多くの新しいツールが頻繁に発表されていますが、ローカルおよびCI-ミラーリングのための標準的で便利なツールはまだ公式化されていません。1つのアプローチとして MinikubeMicroK8s などのツールを使って最小化されたKubernetesを実行する方法があります。これらのツールは本物に似ていますが、オーバーヘッドが少ないのが特徴です。 他のアプローチとしては、リモートの実際のKubernetes上でテストする方法があります。いくつかのCIプロバイダー(例:Codefresh) はKubernetes環境とネイティブに統合されており、実際のKubernetes上でCIパイプラインを簡単に実行できます。他のプロバイダーはリモートのKubernetesに対してカスタムスクリプトを実行できます。

さもなくば: 本番環境とテスト環境で異なるテクノロジーを使用すると、2つのデプロイメントモデルを維持する必要があり、開発者と運用チームが分離されてしまいます。


コード例

👏 例: CIパイプライン上でその場でKubernetesクラスタを生成する (出典: Dynamic-environments Kubernetes)

deploy:
stage: deploy
image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
script:
- ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN
- kubectl create ns $NAMESPACE
- kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL"
- mkdir .generated
- echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF"
- sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml"
- kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml
- kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml
environment:
name: test-for-ci



⚪ ️5.4 テスト実行を並列化する

こうしましょう: 正しい方法で行えば、テストは24時間365日ほぼ即座にフィードバックを提供してくれる友人です。 しかし、実践的には、1つのスレッドで500のCPUバウンドのユニットテストを実行するには時間がかかりすぎます。 幸いなことに、最新のテストランナーやCIプラットフォーム(JestAVAMocha extensions のような)では、テストを複数のプロセスに並列化し、フィードバックまでの時間を大幅に改善することができます。CIベンダーの中には、テストをコンテナ間(!)で並列化するものもあり、これによりフィードバックループがさらに短縮されます。ローカルで複数のプロセスを使用しても、クラウドのCLIで複数のマシンを使用しても、それぞれが異なるプロセスで実行される可能性があるため、並列化によってテストを自律的に維持する必要があります。

さもなくば: 新しいコードをプッシュしてから1時間後にテストの結果が出るのでは、その頃には既に次の機能のコーディングをしているでしょうから、テストの効果を半減させてしまいます。


コード例

👏 正しい例: テストの並列化により、Mocha parallelとJestは従来のMochaを簡単に凌駕しました (出典: JavaScript Test-Runners Benchmark)

alt text



⚪ ️5.5 ライセンスチェックと盗用チェックで法的問題を回避しよう

こうしましょう: ライセンスや盗用の問題は、おそらく今は主な関心事ではないでしょうが、10分でこの項目を満たせるとしたらどうでしょう? license checkplagiarism check (商用利用可能な無料プラン)などのnpmパッケージは、CIパイプラインに簡単に組み込むことができ、制限付きライセンスの依存関係や、Stack Overflowからコピーペーストされたコードなど、明らかに著作権に違反しているコードを検査することができます。

さもなくば: 意図せずに不適切なライセンスのパッケージを使用したり、商用コードをコピーペーストしたりして、法的な問題が発生する可能性があります。


コード例

👏 正しい例:

// license-checker をローカル又はCI環境にインストールしてください
npm install -g license-checker

// すべてのライセンスをスキャンし、未承認のライセンスを見つけた場合は0以外の終了コードで失敗するようにします。CI環境では、この失敗をキャッチして、ビルドを停止する必要があります。
license-checker --summary --failOn BSD

alt text



⚪ ️5.6 脆弱性のある依存関係を常に検査する

こうしましょう: Expressなどの最も信頼できる依存関係であっても、既知の脆弱性があります。これは、npm audit のようなコミュニティツールや、snyk (無料のコミュニティバージョンもあります)のような商用ツールを使えば、簡単に解決できます。これらのツールは、ビルドのたびにCIから起動することができます。

さもなくば: 専用のツールを使わずにコードを脆弱性から守るには、新たな脅威に関するオンラインの情報を常にチェックする必要があります。非常に面倒です。


コード例

👏 例: NPM Audit の結果

alt text



⚪ ️5.7 依存関係のアップデートを自動化する

こうしましょう: Yarnとnpmのpackage-lock.jsonの導入は、深刻な課題をもたらしました(地獄への道は善意で敷かれています) - 標準では、パッケージはもはや更新されません。‘npm install’ と ‘npm update’ で何度もデプロイを繰り返すチームでも、新しいアップデートは得られません。その結果、依存パッケージのバージョンは良くても標準以下になり、最悪の場合は脆弱なコードになります。現在、チームは手動でpackage.jsonを更新するために開発者の善意と記憶力に頼っていたり、ncu のようなツールを手動で使用しています。 より確実な方法は、最も信頼性の高い依存関係のバージョンを取得するプロセスを自動化することですが、まだ銀の弾丸のような解決策はありません。ただ、可能性のある自動化の道は2つあります:

(1) CIで、‘npm outdated’ や‘npm-check-updates (ncu)’などのツールを使って、古い依存関係を持つビルドを失敗させます。これにより、開発者に依存関係の更新を強制することができます。

(2) コードをスキャンして、依存関係を更新したプルリクエストを自動的に作成する商用ツールを使用します。残る一つの興味深い問題は、依存関係の更新ポリシーをどうするかということです。- パッチごとに更新するとオーバーヘッドが大きくなりすぎますし、メジャーリリース直後に更新すると不安定なバージョンになってしまう可能性もあるでしょう(多くのパッケージがリリース後数日で脆弱性が発見されています。eslint-scopeのインシデント をみてください)。

効率的なアップデートポリシーでは、いくつかの「権利確定期間」を設けることができます - ローカルが古くなったと判断する前に、コードを@latestよりもしばらく遅れたバージョンになるようにします(例:ローカルバージョンは1.3.1、リポジトリバージョンは1.3.8)。

さもなくば: 作成者によってリスクがあると明示的にタグ付けされたパッケージがプロダクションで実行されます。


コード例

👏 例: ncu は手動またはCIパイプライン上で、コードがどの程度最新バージョンから遅れているかを検出するために使用できます。

alt text



⚪ ️ 5.8 その他、Nodeに関連のないCIのTips

こうしましょう: この記事は、Node JSに関連するか、少なくともNode JSで例示できるテストのアドバイスに焦点を当てています。ですがこの項目では、Nodeに関連しないけれどよく知られているCIのTipsをいくつかまとめて紹介します。

  1. 宣言型の構文を使用する。ほとんどのベンダーではこれが唯一の選択肢ですが、Jenkinsの古いバージョンでは、コードやUIを使用することができます。
  2. Dockerにネイティブで対応しているベンダーを選ぶ。
  3. 早期に失敗し、最速のテストを最初に実行する。複数の高速な検査(例:リンティング、ユニットテスト)をまとめた「スモークテスト」のステップ/マイルストーンを作成し、コードコミッターに迅速なフィードバックを提供しましょう。
  4. テストレポート、カバレッジレポート、ミューテーションレポート、ログなど、すべてのビルド成果物に簡単に目を通すことができる。
  5. イベントごとに複数のパイプライン/ジョブを作成し、それらの間でステップを再利用する。例えば、フィーチャーブランチのコミット用のジョブと、マスターブランチのPR用のジョブには別のジョブを設定します。それぞれが共有ステップを使ってロジックを再利用できるようにします(ほとんどのベンダーがコード再利用のための何らかのメカニズムを提供しています)。
  6. ジョブ宣言に機密情報を埋め込まない。特定の保存場所やジョブの設定から機密情報を取得するようにしてください。
  7. リリースビルドで明示的にバージョンを上げるか、少なくとも開発者が行ったことを保証する。
  8. 一度だけビルドし、単一のビルド成果物(例:Docker Image)に対してすべての検査を実行する
  9. ビルド間で状態が移動しない短命な環境でテストを行う。node_modulesのキャッシュは唯一の例外かもしれません。

さもなくば: 長年の知恵を失うことになるでしょう



⚪ ️ 5.9 ビルドマトリックス: 複数のNodeバージョンで同じCIステップを実行する

こうしましょう: 品質チェックは偶然の発見であり、カバーする範囲が広ければ広いほど、問題を早期に発見することができます。再利用可能なパッケージを開発したり、様々な構成やNodeのバージョンを持つ複数の顧客の製品を開発する場合、CIはすべての構成の組み合わせに対してテストのパイプラインを実行する必要があります。 例えば、ある顧客にはMySQLを使用し、他の顧客にはPostgresを使用する場合、いくつかのCIベンダーは「マトリックス」と呼ばれる機能をサポートしており、MySQL、Postgres、そしてNodeバージョン8、9、10のような複数のすべての組み合わせに対してテストスイートを実行することができます。これは設定のみで行われ、追加の手間はかかりません(テストまたはその他の品質チェックが既にあることを前提としています)。マトリックスをサポートしていない他のCIでは、拡張機能や調整機能で対応しているかもしれません。

さもなくば: テストを書くという大変な作業をすべて終えた後に、設定の問題だけでバグが紛れ込むのを許すのでしょうか?


コード例

👏 例: Travis(CIベンダー)のビルド定義を使って、同じテストを複数のNodeバージョンで実行する

language: node_js
node_js:
- "7"
- "6"
- "5"
- "4"
install:
- npm install
script:
- npm run test



Team

Yoni Goldberg



Role: Writer

About: I'm an independent consultant who works with Fortune 500 companies and garage startups on polishing their JS & Node.js applications. More than any other topic I'm fascinated by and aims to master the art of testing. I'm also the author of Node.js Best Practices

📗 Online Course: Liked this guide and wish to take your testing skills to the extreme? Consider visiting my comprehensive course Testing Node.js & JavaScript From A To Z


Follow:




Role: Tech reviewer and advisor

Took care to revise, improve, lint and polish all the texts

About: full-stack web engineer, Node.js & GraphQL enthusiast



Role: Concept, design and great advice

About: A savvy frontend developer, CSS expert and emojis freak

Role: Helps keep this project running, and reviews security related practices

About: Loves working on Node.js projects and web application security.

Contributors ✨

Thanks goes to these wonderful people who have contributed to this repository!


Scott Davis

🖋

Adrien REDON

🖋

Stefano Magni

🖋

Yeoh Joer

🖋

Jhonny Moreira

🖋

Ian Germann

🖋

Hafez

🖋

Ruxandra Fediuc

🖋

Jack

🖋

Peter Carrero

🖋

Huhgawz

🖋

Haakon Borch

🖋

Jaime Mendoza

🖋

Cameron Dunford

🖋

John Gee

🖋

Aurelijus Rožėnas

🖋

Aaron

🖋

Tom Nagle

🖋

Yves yao

🖋

Userbit

🖋

Glaucia Lemos

🚧

koooge

🖋

Michal

🖋

roywalker

🖋

dangen

🖋

biesiadamich

🖋

Yanlin Jiang

🖋

sanguino

🖋

Morgan

🖋

Lukas Bischof

⚠️ 🖋

JuanMa Ruiz

🖋

Luís Ângelo Rodrigues Jr.

🖋

José Fernández

🖋

Alejandro Gutierrez Barcenilla

🖋

Jason

🖋

Otavio Araujo

⚠️ 🖋

Alex Ivanov

🖋

Yiqiao Xu

🖋