TDDは間違っていますか?

免責事項クリックベイトで捕まった。 明らかに、TDDはエラーと呼ばれることはありませんが、...


内容





エントリー



私のキャリアの最初の6年間、私はフリーランスで、小さなスタートアップの人生の初期段階に参加しました。 これらのプロジェクトにはテストがありませんでした...本当に、単一のテストではありません。







これらの条件では、 昨日の機能を実装する必要があります。 市場の需要は常に変化しているため、テストは終了する前に廃止されます。 そして、これらのテストでさえも、作成したいものが正確にわかっている場合にのみ作成できますが、これは必ずしもそうではありません。 R&Dを行うとき、最終結果がどうあるべきか分からない場合があります。 また、特定の成功を達成したとしても、明日市場(およびそれによって要件)が変わらないことを確信することはできません。 一般に、テスト時間を節約するビジネス上の理由があります。







私たちの業界は単なるスタートアップではないことに同意します。

約2年前、私はあらゆる規模のクライアントにサービスを提供するかなり大規模なアウトソーシング会社に就職しました。

キッチン/喫煙室での会話の中で、ユニットテストとTDDが一種のベストプラクティスであることにほぼ全員が同意することがわかりました。 しかし、私が参加したこの会社のすべてのプロジェクトでは、テストはありませんでした。 いいえ、私はその決定をしませんでした。 もちろん、優れたテストカバレッジを持つプロジェクトもありますが、それらは非常に官僚化されています。







それで問題は何ですか?

なぜTDDが優れていると誰もが同意するのに、だれもそれを使用したくないのですか

たぶんTDDは間違っていますか? -いや!

おそらくビジネス上の利点はありませんか? -そして再び、いいえ!

たぶん開発者は怠け者ですか? -はい! しかし、それが理由ではありません。

問題はテスト自体にあります!

これは奇妙に聞こえるかもしれませんが、それを証明しようとします。







テストが問題です!



この調査に基づくと、エコシステム全体で最小の全体的な満足度は



属し



。 2016年と2017年でした。 以前の研究は見つかりませんでしたが、これはあまり重要ではありません。







ちょっとした歴史



2008 に、最初のJSテストフレームワーク( QUnit )の1つがリリースされました。

ジャスミン2010年に登場しました。

2011 - モカ

私が見つけた最初のJestリリースは2014年でした。







テストツールのタイムライン







比較のため。

Angular.jsは 2010年に リリースされました。

エンバー2011年に登場しました。

React - 2013

など...







この記事の執筆時点では、JSフレームワークは作成されていません...

とにかく、私によって。

フレームワークとライブラリのタイムライン







同じ期間に、 うなり声 、そしてぐるぐる音の上昇と下降を目にしました。その後、 npmスクリプトのフルパワーを実感し、安定したwebpackリリースがリリースされました







予定表







過去10年間ですべてが変化しました。 テストを除くすべて。







小テスト



あなたの知識をテストしましょう。 これらのライブラリ/フレームワークは何ですか?







1:







 var hiddenBox = $("#banner-message"); $("#button-container button").on("click", function(event) { hiddenBox.show(); });
      
      





2:







 @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent{ hero: Hero = { id: 1, name: 'Windstorm' }; constructor() { } }
      
      





3:







 function Avatar(props) { return ( <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} /> ); }
      
      





回答:







  1. JQuery
  2. Angular2 +
  3. 反応する


いいね あなたの答えはすべて正しかったと確信しています。 しかし、これらのテストフレームワークはどうでしょうか。

1:







 var assert = require('assert'); describe('Array', function() { describe('#indexOf()', function() { it('should return -1 when the value is not present', function() { assert.equal([1,2,3].indexOf(4), -1); }); }); });
      
      





2:







 const sum = require('./sum'); test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); });
      
      





3:







 test('timing test', function (t) { t.plan(2); t.equal(typeof Date.now, 'function'); var start = Date.now(); setTimeout(function () { t.equal(Date.now() - start, 100); }, 100); });
      
      





4:







 let When2IsAddedTo2Expect4 = Assert.AreEqual(4, 2+2)
      
      





回答:







  1. モカ
  2. ジェスト
  3. テープ
  4. F#のテスト


それらのいくつかを推測したかもしれませんが、一般に、それらはすべて非常によく似ています。 言語を変更しても、ほとんど変更されないことに注意してください。







JavaScriptの世界で少なくとも8年の単体テストの経験があります。

しかし、その時点で既存のものを単純に適合させました。 私たちが知っているように、単体テストははるかに早く現れました。 Test Anything Protocol (1987)のリリースを基準点とすると、現在のアプローチを現在よりも長く使用します。







TDDは古いもので ないにしても、それほど若いものではありません。 これらすべてが、すべての長所と短所を客観的に評価できるという事実につながります。







TDDレビュー



TDDとは何かを思い出しましょう。







テストによる開発(英語のテスト駆動開発、 TDD )は、非常に短い開発サイクルの繰り返しに基づくソフトウェア開発手法です。まず、目的の変更をカバーするテストを作成し、次にテストをパスするコードを作成し、最終的にリファクタリングを実行します。関連する標準に対する新しいコード。 (c) ウィキペディア

tddサイクル







しかし、これは私たちに何を与えますか?







テストは正式な要件です。



これは部分的にしか当てはまりません。







TDDの実践は1999年にケントベックによって「再発明」されましたが、 アジャイルマニフェストはわずか2年後(2001年)に採用されました。 TDDはカスケードモデルの黄金時代に生まれ、この事実がTDDが設計された最も有利な条件とプロセスを決定することを理解できるように、これを強調する必要があります。 明らかに、TDDはこれらの条件下で最適に機能します。







したがって、次のようなプロジェクトで作業している場合:







  1. 要件は明確です。
  2. あなたはそれらを完全に理解しています。
  3. それらは安定しており、頻繁に変更されることはありません。


要件の形式化としてテストを作成できます。

ただし、 既存のテストを同じ方法で使用するには、次の点も満たす必要があります。







  1. テストにエラーはありません。
  2. それらは関連しています。
  3. また、ほとんどすべてのユースケースをカバーしています(コードカバレッジと混同しないでください)。


したがって、 「テストは形式化された要件」は、「ウォーターフォールモデル」または「顧客」が科学者およびエンジニアであるNASAプロジェクトのように、開発自体のにこれらの要件が存在する場合にのみ当てはまります。







特定の条件下では、これはアジャイルプロセスで機能します。 特にBDDのようなものが使用される場合、それはまったく別の話です。


TDDは優れたアーキテクチャを奨励します



また、これは部分的にしか当てはまりません。

TDDはモジュール性を推奨します。これは必要ですが、優れたアーキテクチャには十分ではありません。







アーキテクチャの品質は開発者に依存します。 経験豊富な開発者は、単体テストを使用しても使用しなくても、優れたコードを作成できます。

一方、貧弱な開発者は、低品質のテストで覆われた低品質のコードを作成します。優れたテストを作成することは、プログラミングそのものであると同時に一種の芸術であるためです。







もちろん、テストはセックスのようなものです。「最高は悪いことより悪いことです。」 しかし...

このテストでは、優れたシステム設計に進むことはできません。







 import { inject, TestBed } from '@angular/core/testing'; import { UploaderService } from './uploader.service'; describe('UploaderService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [UploaderService], }); }); it('should be created', inject([UploaderService], (service: UploaderService) => { expect(service).toBeTruthy(); })); });
      
      





彼は何もテストしないからです。







何もテストしないために15行のコードを使用したことに注意してください。

ただし、このテストではシステム設計が改善されることはありません。







 var IotSimulation = artifacts.require("./IotSimulation.sol"); var SmartAsset = artifacts.require("./SmartAsset.sol"); var BuySmartAsset = artifacts.require("./BuySmartAsset.sol"); var BigInt = require('big-integer'); contract('BuySmartAsset', function (accounts) { it("Should sell asset", async () => { var deliveryCity = "Lublin"; var extra = 1000; // var gasPrice = 100000000000; const smartAsset = await SmartAsset.deployed(); const iotSimulation = await IotSimulation.deployed(); const buySmartAsset = await BuySmartAsset.deployed() const result = await smartAsset.createAsset(Date.now(), 200, "docUrl", 1, "email@email1.com", "Audi A8", "VIN02", "black", "2500", "car"); const smartAssetGeneratedId = result.logs[0].args.id.c[0]; await iotSimulation.generateIotOutput(smartAssetGeneratedId, 0); await iotSimulation.generateIotAvailability(smartAssetGeneratedId, true); await smartAsset.calculateAssetPrice(smartAssetGeneratedId); const assetObjPrice = await smartAsset.getSmartAssetPrice(smartAssetGeneratedId); assert.isAbove(parseInt(assetObjPrice), 0, 'price should be bigger than 0'); await smartAsset.makeOnSale(smartAssetGeneratedId); var assetObj = await smartAsset.getAssetById.call(smartAssetGeneratedId); assert.equal(assetObj[9], 3, 'state should be OnSale = position 3 in State enum list'); await smartAsset.makeOffSale(smartAssetGeneratedId); assetObj = await smartAsset.getAssetById.call(smartAssetGeneratedId); assert.equal(assetObj[9], 2, 'state should be PriceCalculated = position 2 in State enum list'); await smartAsset.makeOnSale(smartAssetGeneratedId); const calculatedTotalPrice = await buySmartAsset.getTotalPrice.call(smartAssetGeneratedId, '112', '223'); await buySmartAsset.buyAsset(smartAssetGeneratedId, '112', '223', { from: accounts[1], value: BigInt(calculatedTotalPrice.toString()).add(BigInt(extra)) }); assetObj = await smartAsset.getAssetById.call(smartAssetGeneratedId); assert.equal(assetObj[9], 0, 'state should be ManualDataAreEntered = position 0 in State enum list'); assert.equal(assetObj[10], accounts[1]); const balanceBeforeWithdrawal = await web3.eth.getBalance(accounts[1]); const gas = await buySmartAsset.withdrawPayments.estimateGas({ from: accounts[1] }); await buySmartAsset.withdrawPayments({ from: accounts[1], gasPrice: gasPrice }); const balanceAfterWithdrawal = await web3.eth.getBalance(accounts[1]); var totalGas = gas * gasPrice; assert.isOk((BigInt(balanceAfterWithdrawal.toString()).add(BigInt(totalGas))).eq(BigInt(balanceBeforeWithdrawal.toString()).add(BigInt(extra)))); }) })
      
      





このテストの最大の問題は元のコードベースですが、それでも、既に動作しているプロジェクトをリファクタリングしなくても、大幅に改善される可能性があります。







一般に、最終的なアーキテクチャに対するTDDの影響は、選択したライブラリ/フレームワークの影響とほぼ同じレベルになります( 少なくともNestRxJs 、およびMobXは、私の個人的な意見では、はるかに強い効果があります)。







しかし、TDDもフレームワークも、悪いコードや失敗したアーキテクチャソリューションからあなたを救うことはありません。







特効薬はありません。


TDDは時間を節約します



そして、それはすでに多くの要因に依存しています...

それを仮定しましょう:







  1. プロジェクトの全員が、選択したテストツール、TDD方法論、および単体テストのベストプラクティスに精通しています。
  2. そして、誰もが上記のすべてを等しく理解しています。
  3. また、要件は透明で安定しています。
  4. さらに、開発チームは「製品所有者」と同じ方法でそれらを理解します。
  5. また、管理者はTDDによって引き起こされるすべての組織の問題(たとえば、新しい開発者をチームに導入するためのより長いプロセス)を解決する準備ができています。


この場合でも、最初に時間と労力を費やす必要があります。これにより、初期開発フェーズが長くなり、しばらくしてから、バグの修正と製品サポートに必要な時間を短縮できます。

もちろん、2番目はスタートアップ投資以上のものかもしれません。その場合、TDDの利点は明らかです。

また、テストでは意図しない変更がすぐに検出されるため、場合によっては、新しい機能の導入にかかる時間を節約できます。

しかし、非常に動的な現実の世界では、要件は変化する可能性があり、以前は正しい動作だったものが正しくなくなります。 この場合、新しい現実に関連してテストを書き直す必要があります。 そして、明らかに、すぐに報われない新しい努力をしてください。







このタイプのサイクルに入ることさえできます:







tdd-cycle-for-changes-wrong







さて、このサイクルはTDDの原則に反しています。 しかし、以下はもはやありません:







tdd-cycle-for-changes-right







それらの重要な違いを見つけてみてください。







テストは最高のドキュメントです。



いや 彼らはこれが得意ですが、間違いなく最高ではありません。







角度のドキュメントを見てみましょう:







アングルドキュメントスクリーンショット







または反応する







react-docs-screenshot







彼らには共通点があると思いますか? -どちらもコード例に基づいています 。 そしてさらにそれ以上。 これらの例はすべて簡単に実行できます(angularはStackBlitzを使用し、reactはCodePenを使用します )。そのため、出力で何が得られ、何かを変更した場合に何が起こるかを確認できます。

もちろん、プレーンテキストもありますが、コード内のコメントのようなものです。コード自体から何かを理解していない場合にのみコメントが必要です。







実行可能なコード例 -ここに最高のドキュメントがあります!







テストはこれに近いですが、十分ではありません。







 describe('ReactTypeScriptClass', function() { beforeEach(function() { container = document.createElement('div'); attachedListener = null; renderedName = null; }); it('preserves the name of the class for use in error messages', function() { expect(Empty.name).toBe('Empty'); }); it('throws if no render function is defined', function() { expect(() => expect(() => ReactDOM.render(React.createElement(Empty), container) ).toThrow() ).toWarnDev([ // A failed component renders twice in DEV 'Warning: Empty(...): No `render` method found on the returned ' + 'component instance: you may have forgotten to define `render`.', 'Warning: Empty(...): No `render` method found on the returned ' + 'component instance: you may have forgotten to define `render`.', ]); });
      
      





これは、 reactの実際のテストからの小さな断片です。 コードの例を強調表示できます。







 container = document.createElement('div'); Empty.name;
      
      





 container = document.createElement('div'); ReactDOM.render(React.createElement(Empty), container);
      
      





それ以外はすべて、手書きのインフラストラクチャコードです。







正直に言って、上記のテスト例は実際のドキュメントよりもはるかに読みにくいです。 そして、問題はこの特定のテストではありません-私はfacebookの人が良いコードと良いテストを書く方法を知っていると確信しています:)テストツールとそのようなアサーションライブラリからのこのすべてのゴミ、 describe



test



to.be.true



あなたのテスト。







ちなみに、 equal



/ deepEqual



のみを使用してテストを書き換えることができるため、最小限のAPIを備えたテープと呼ばれるライブラリがあり、これらの用語で考えることは一般に単体テストの良い習慣です。 しかし、 tape



のテストでさえ、単に実行可能なコードサンプルとは非常にかけ離れています

しかし、テストがドキュメントとしての使用にまだ非常に適していることは注目に値します。 それらは時代遅れになる可能性は本当に低く、私たちの意識はそれらを読むときに単に過剰を捨てます。 テストがどのように変化するかを頭の中で視覚化しようとすると、次のようになります。







test-as-doc-example







ご覧のとおり、これはすでに元のテストよりも実際のドックにはるかに近いものです。







いくつかの結論



  1. テストは 、安定している場合、 正式な要件です。
  2. 開発者のスキルが十分であれば、 TDDは優れたアーキテクチャを推奨します。
  3. TDDを最初に投資すれば時間を節約できます。
  4. 他の実行可能なコード例がない場合、テストは最高のドキュメントです。


TDDはまだ間違っていますか? -いいえ、TDDは間違っていません。

それは正しい方向を指し示し、重要な質問を提起します。 適用方法を考え直して変更するだけです。







解決策は何ですか?



TDDを特効薬として受け取らないでください。

たとえば、アジャイルプロセスと見なさないでください。

代わりに、その本当の強みに焦点を当てます:







  1. 意図しない変更の防止、言い換えると、既存の動作を一種の「ベースライン」として修正します(英語の用語「ベースライン」は依然として有用です)。
  2. ドキュメントのサンプルをテストとして使用します。


単体テストを開発者ツールと考えてください。 たとえば、 リンターコンパイラのように。







リンターを使用する許可をプロダクトオーナーに求めることはありません。使用するだけです。

いつかこれはユニットテストの現実になるでしょう。 TDDに必要な努力がタイマーまたはバンドラーを使用するレベルにあるとき。 ただし、これまでは、 実行可能なサンプルにできるだけ近いテストを作成し、それらをプロジェクトの現在のベースライン状態として使用することで、コストを最小限に抑えます。







人気のあるツールのほとんどが他の目的のために設計されているという事実を考えると、これは難しいことだと理解しています。







確かに、上記のすべての問題を考慮して、そのようなものを作成しました。 彼は呼ばれます







バセット







基本的な概念は非常に単純です。 コードを書く:







 export function sampleFn(a: any, b: any) { return a + b + b + a; }
      
      





そして、あなたのテストでそれを使用してください:







 import { sampleFn } from './index'; export = { values: [ sampleFn(1, 1), sampleFn(1000000, 1000000), sampleFn('abc', 'cba'), sampleFn(1, 'abc'), sampleFn('abc', 1), new Promise(resolve => resolve(sampleFn('async value', 1))), ], };
      
      





注:もちろん、テストは非常に模擬的なものです-デモ用です。

次に、 baset test



コマンドを実行して、一時的なベースラインを取得します。







 { "values": [ 4, 4000000, "abccbacbaabc", "1abcabc1", "abc11abc", "async value11async value" ] }
      
      





値が正しい場合、 baset accept



を実行し、作成したベースラインをリポジトリにコミットします。







テストの以降のすべての実行では、既存のベースラインをテストからエクスポートされた値と比較します。 それらが異なる場合、テストは失敗し 、そうでない場合、合格ます。

要件が変更された場合は、コードを変更し、テストを実行して新しいベースラインを受け入れます







このツールは、最小限の労力で、不注意による変更から保護します。 実行する必要があるのは、 実行可能なコードサンプルを作成することだけです 。これは、優れたドキュメントの基盤です。







いくつかの例



反応で使用します。 テストは次のとおりです。







 import * as React from 'react'; import { jsxFn } from './index'; export const value = ( <div> {jsxFn('s', 's')} {jsxFn('abc', 'cba')} {jsxFn('s', 'abc')} {jsxFn('abc', 's')} </div> );
      
      





ベースラインのような.md



ファイルを作成します:










exports.value:









 <div data-reactroot=""> <div class="cssCalss"> ss </div> <div class="cssCalss"> abccba </div> <div class="cssCalss"> sabc </div> <div class="cssCalss"> abcs </div> </div>
      
      








またはpixi.jsで







 import 'pixi.js'; interface IResourceDictionary { [index: string]: PIXI.loaders.Resource; } const ASSETS = './assets/assets.json'; const RADAR_GREEN = 'Light_green'; const getSprite = async () => { await new Promise(resolve => PIXI.loader .add(ASSETS) .load(resolve)); return new PIXI.Sprite(PIXI.utils.TextureCache[RADAR_GREEN]); }; export const sprite = getSprite();
      
      





このテストは、次のようなベースラインを作成します。










exports.sprite:









exports.sprite










計画について少し



このツールはまだ開発の非常に初期の段階にあり、たとえば、次のよう多くの革新がまだあると言わなければなりません。







  1. 監視/ワークフローモード
  2. TAPの互換性
  3. Git受け入れ戦略
  4. VSコード拡張
  5. ...および少なくとも24人。


計画の約40%のみが実装されました。 しかし、すべての基本機能はすでに機能しているので、試してみてください。 たぶん、あなたはそれが好きです、誰が知っていますか?








All Articles