• <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>
  • 測試驅動開發(TDD)入門

    發表于:2019-09-24來源:oschina作者:謝小呆點擊數: 標簽:tdd測試驅動開發
    測試驅動開發,英文全稱 Test-Driven Development(簡稱 TDD),是由Kent Beck 先生在極限編程(XP)中倡導的開發方法。以其倡導先寫測試程序,然后編碼實現其功能得名。本文不打算扯過多

    測試驅動開發(TDD)入門

    測試驅動開發,英文全稱 Test-Driven Development(簡稱 TDD),是由Kent Beck 先生在極限編程(XP)中倡導的開發方法。以其倡導先寫測試程序,然后編碼實現其功能得名。

    本文不打算扯過多的理論,而是通過操練的方式,帶著大家去操練一下,讓同學們切身感受一下 TDD,究竟是怎么玩的。開始之前先說一下 TDD 的基本步驟。

    TDD 的步驟

    1. 寫一個失敗的測試
    2. 寫一個剛好讓測試通過的代碼
    3. 重構上面的代碼

    簡單設計原則

    重構可以遵循簡單設計原則:

    簡單設計原則,優先級從上至下降低,也就是說 「通過測試」的優先級最高,其次是代碼能夠「揭示意圖」和「沒有重復」,「最少元素」則是讓我們使用最少的代碼完成這個功能。

    操練

    Balanced Parentheses 是我在 cyber-dojo 上最喜歡的一道練習題之一,非常適合作為 TDD 入門練習。

    先來看一下題目:

    Write a program to determine if the the parentheses (), the brackets [], and the braces {}, in a string are balanced.
    For example:
    {{)(}} is not balanced because ) comes before (
    ({)} is not balanced because ) is not balanced between {} and similarly the { is not balanced between ()
    [({})] is balanced
    {}([]) is balanced
    {()}[[{}]] is balanced
    

    我來翻譯一下:

    寫一段程序來判斷字符串中的小括號 () ,中括號 [] 和大括號 {} 是否是平衡的(正確閉合)。

    例如:

    {{)(}} 是沒有閉合的,因為 ) 在 ( 之前。

    ({)} 是沒有閉合的,因為 ) 在 {} 之間沒有正確閉合,同樣 { 在 () 中間沒有正確閉合。

    [({})] 是平衡的。

    {}([]) 是平衡的。

    {()}[[{}]] 是平衡的。

    需求清楚了,按照一個普通程序員的思維需要先思考一下,把需求理解透徹而且思路要完整,在沒思路的情況下完全不能動手。

    而使用 TDD 首先要將需求拆分成很小的任務,每個任務足夠簡單、獨立,通過完成一個個小任務,最終交付一個完整的功能。

    這個題目起碼有兩種技術方案,我們先來嘗試第一種。

    先來拆分第一步:

    輸入一個空字符串,期望是平衡的,所以返回 true 。

    我們來先寫測試:

    import assert from 'assert';
    
    describe('Parentheses', function() {
      it('如果 輸入字符串為 "" ,當調用 Parentheses.execute(),則結果返回 true', () => {
        assert.equal(Parentheses.execute(''), true);
      });
    });

    此時運行測試:

    1. Parentheses 如果 輸入字符串為 "" ,當調用 Parentheses.execute(),則結果返回 true: ReferenceError: Parentheses is not defined at Context.Parentheses (test/parentheses.spec.js:5:18)

    接下來寫這個 case 的實現:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
      }
    };

    運行:

    Parentheses ? 如果 輸入字符串為 "" ,當調用 Parentheses.execute(),則結果返回 true
    1 passing (1ms)
    

    第二步:

    輸入符串為 () ,期望的結果是 true 。

    先寫測試:

    it('如果 輸入字符串為 () ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('()'), true);
    });

    運行、失??!因為篇幅原因這里就不再貼報錯結果。

    然后繼續寫實現:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        if (str === '()') {
          return true;
        }
    
        return false;
      }
    };

    這個實現雖然有點傻,但的確是通過了測試,回顧一下 “簡單設計原則” ,以上兩步代碼都過于簡單,沒有值得重構的地方。

    第三步:

    輸入符串為 ()() ,期望的結果是 true 。

    測試:

    it('如果 輸入字符串為 ()() ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('()()'), true);
    });

    運行、失??!

    實現:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        if (str === '()') {
          return true;
        }
    
        if (str === '()()') {
          return true;
        }
    
        return false;
      }
    };

    這個實現更傻,傻到我都不好意思往上貼,回顧一下 TDD 的步驟「通過測試」,可以重構了。

    其中 if (str === '()')if (str === '()()') 看起來有些重復,來看看是否可以這樣重構一下:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const replacedResult = str.replace(/\(\)/gi, '');
        if (replacedResult === '') {
          return true;
        }
    
        return false;
      }
    };

    將字符串中的 () 全部替換掉,如果替換后的字符串結果等于 '' 則是正確閉合的。

    運行,通過!

    我們再來增加一個case :

    it('如果 輸入字符串為 ()()( ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute('()()('), false);
    });

    運行,通過!

    第四步

    輸入符串為 [] ,期望的結果是 true 。

    測試:

    it('如果 輸入字符串為 [] ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('[]'), true);
    });

    運行、失??!

    實現:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        let replacedResult = str.replace(/\(\)/gi, '');
        replacedResult = replacedResult.replace(/\[\]/gi, '');
        if (replacedResult === '') {
          return true;
        }
    
        return false;
      }
    };

    運行,通過!

    正則表達式可以將兩條語句合并成一條,但是合并成一條語句的可讀性較差,所以這里寫成了兩句。

    第五步:

    輸入符串為 {} ,期望的結果是 true 。

    測試:

    it('如果 輸入字符串為 {} ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('{}'), true);
    });

    實現:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        let replacedResult = str.replace(/\(\)/gi, '');
        replacedResult = replacedResult.replace(/\[\]/gi, '');
        replacedResult = replacedResult.replace(/\{\}/gi, '');
        if (replacedResult === '') {
          return true;
        }
    
        return false;
      }
    };

    運行、通過!

    第六步:

    輸入符串為 [({})] ,期望的結果是 true 。

    寫測試:

    it('如果 輸入字符串為 [({})] ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('[({})]'), true);
    });

    運行、失??!

    原因是我們的替換邏輯是有順序的,當替換完成的結果有值,如果等于輸入值則返回 false ,如果不等于輸入值則繼續替換, 這里用到了遞歸。

    來修改一下實現代碼:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        let replacedResult = str.replace(/\(\)/gi, '');
        replacedResult = replacedResult.replace(/\[\]/gi, '');
        replacedResult = replacedResult.replace(/\{\}/gi, '');
    
        if (replacedResult === '') {
          return true;
        }
    
        if (replacedResult === str) {
          return false;
        }
    
        return this.execute(replacedResult);
      }
    };

    運行、通過!

    再添加一些測試用例

    it('如果 輸入字符串為 {}([]) ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('{}([])'), true);
    });
    
    it('如果 輸入字符串為 {()}[[{}]] ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('{()}[[{}]]'), true);
    });
    
    it('如果 輸入字符串為 {{)(}} ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute('{{)(}}'), false);
    });
    
    it('如果 輸入字符串為 ({)} ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute('({)}'), false);
    });

    運行、通過!

    這個功能我們就這樣簡單的實現了,需求如此,所以這個方案有些簡陋,甚至我們都沒有做錯誤處理。在這里我們不花太多時間進行重構,直接進入方案二。

    方案二

    我們將需求擴展一下:

    輸入字符串為:

    const fn = () => {
        const arr = [1, 2, 3];
        if (arr.length) {
          alert('success!');
        }
    };

    判斷這個字符串的括號是否正確閉合。

    通過剛剛 git 提交的記錄找到第二步重新拉出一個分支:

    git log
    git checkout <第二步的版本號> -b plan-b

    運行、通過!

    測試已經有了,我們直接修改實現:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (char === '(') {
            pipe.push(chart);
          }
    
          if (char === ')') {
            pipe.pop();
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    這個括號的閉合規則是先進后出的,使用數組就 ok。

    運行、通過!

    第三步:

    上面的實現滿足這個任務,但是有一個明顯的漏洞,當輸入只有一個 ) 時,期望得到返回 false ,我們增加一個 case:

    it('如果 輸入字符串為 ) ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute(')'), false);
    });

    運行、失??!

    再修改實現:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (char === '(') {
            pipe.push(char);
          }
    
          if (char === ')') {
            if (pipe.pop() !== '(')
              return false;
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    運行、通過!如果 pop() 的結果不是我們放進去管道里的值,則認為沒有正確閉合。

    重構一下,if 語句嵌套的沒有意義:

    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (char === '(') {
            pipe.push(char);
          }
    
          if (char === ')' && pipe.pop() !== '(') {
            return false;
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    ( ) 在程序中應該是一組常量,不應當寫成字符串,所以繼續重構:

    const PARENTHESES = {
      OPEN: '(',
      CLOSE: ')'
    };
    
    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (char === PARENTHESES.OPEN) {
            pipe.push(char);
          }
    
          if (char === PARENTHESES.CLOSE
              && pipe.pop() !== PARENTHESES.OPEN) {
            return false;
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    運行、通過!

    再增加幾個case:

    it('如果 輸入字符串為 ()() ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('()()'), true);
    });
    
    it('如果 輸入字符串為 ()()( ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute('()()('), false);
    });

    第四步:

    如果輸入字符串為 ] ,這結果返回 false

    測試:

    it('如果 輸入字符串為 ] ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute(']'), false);
    });

    運行、失??!

    這個邏輯很簡單,只要復制上面的邏輯就ok。

    實現:

    const PARENTHESES = {
      OPEN: '(',
      CLOSE: ')'
    };
    
    const BRACKETS = {
      OPEN: '[',
      CLOSE: ']'
    };
    
    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (char === PARENTHESES.OPEN) {
            pipe.push(char);
          }
    
          if (char === PARENTHESES.CLOSE
              && pipe.pop() !== PARENTHESES.OPEN) {
            return false;
          }
    
          if (char === BRACKETS.OPEN) {
            pipe.push(char);
          }
    
          if (char === BRACKETS.CLOSE
              && pipe.pop() !== BRACKETS.OPEN) {
            return false;
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    運行、通過!

    接下來我們開始重構,這兩段代碼完全重復,只是判斷條件不同,如果后面增加 } 邏輯也是相同,所以這里我們將重復的代碼抽成函數。

    const PARENTHESES = {
      OPEN: '(',
      CLOSE: ')'
    };
    
    const BRACKETS = {
      OPEN: '[',
      CLOSE: ']'
    };
    
    const holderMap = {
      '(': PARENTHESES,
      ')': PARENTHESES,
      '[': BRACKETS,
      ']': BRACKETS,
    };
    
    const compare = (char, pipe) => {
      const holder = holderMap[char];
      if (char === holder.OPEN) {
        pipe.push(char);
      }
    
      if (char === holder.CLOSE
          && pipe.pop() !== holder.OPEN) {
        return false;
      }
    
      return true;
    };
    
    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (!compare(char, pipe)) {
            return false;
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    運行、通過!

    第五步

    輸入符串為 } ,期望的結果是 false 。

    測試:

    it('如果 輸入字符串為 } ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute('}'), false);
    });

    運行、失??!

    1. Parentheses 如果 輸入字符串為 } ,當調用 Parentheses.execute(),則結果返回 false: TypeError: Cannot read property 'OPEN' of undefined at compare (src/parentheses.js:22:4) at Object.execute (src/parentheses.js:45:12) at Context.it (test/parentheses.spec.js:29:48)

    報錯信息和我們期望的不符,原來是 } 字符串沒有找到對應的 holder 會報錯,來修復一下:

    const PARENTHESES = {
      OPEN: '(',
      CLOSE: ')'
    };
    
    const BRACKETS = {
      OPEN: '[',
      CLOSE: ']'
    };
    
    const holderMap = {
      '(': PARENTHESES,
      ')': PARENTHESES,
      '[': BRACKETS,
      ']': BRACKETS,
    };
    
    const compare = (char, pipe) => {
      const holder = holderMap[char];
      if (!holder) return true;
      if (char === holder.OPEN) {
        pipe.push(char);
      }
    
      if (char === holder.CLOSE
          && pipe.pop() !== holder.OPEN) {
        return false;
      }
    
      return true;
    };
    
    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (!compare(char, pipe)) {
            return false;
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    運行、失??!這次失敗的結果與我們期望是相同的,然后再修改邏輯。

    const PARENTHESES = {
      OPEN: '(',
      CLOSE: ')'
    };
    
    const BRACKETS = {
      OPEN: '[',
      CLOSE: ']'
    };
    
    const BRACES = {
      OPEN: '{',
      CLOSE: '}'
    };
    
    const holderMap = {
      '(': PARENTHESES,
      ')': PARENTHESES,
      '[': BRACKETS,
      ']': BRACKETS,
      '{': BRACES,
      '}': BRACES
    };
    
    const compare = (char, pipe) => {
      const holder = holderMap[char];
      if (!holder) return true;
      if (char === holder.OPEN) {
        pipe.push(char);
      }
    
      if (char === holder.CLOSE
          && pipe.pop() !== holder.OPEN) {
        return false;
      }
    
      return true;
    };
    
    export default {
      execute(str) {
        if (str === '') {
          return true;
        }
    
        const pipe = [];
        for (let char of str) {
          if (!compare(char, pipe)) {
            return false;
          }
        }
    
        if (!pipe.length) return true;
        return false;
      }
    };

    因為前面的重構,增加 {} 的支持只是增加一些常量的配置。

    運行、通過!

    再增加些 case:

    it('如果 輸入字符串為 [({})] ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('[({})]'), true);
    });
    
    it('如果 輸入字符串為 {}([]) ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('{}([])'), true);
    });
    
    it('如果 輸入字符串為 {()}[[{}]] ,當調用 Parentheses.execute(),則結果返回 true', () => {
      assert.equal(Parentheses.execute('{()}[[{}]]'), true);
    });
    
    it('如果 輸入字符串為 {{)(}} ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute('{{)(}}'), false);
    });
    
    it('如果 輸入字符串為 ({)} ,當調用 Parentheses.execute(),則結果返回 false', () => {
      assert.equal(Parentheses.execute('({)}'), false);
    });

    運行、通過!

    再加最后一個 case:

    const inputStr = `
        const fn = () => {
          const arr = [1, 2, 3];
          if (arr.length) {
            alert('success!');
          }
        };
      `;
    
    it(`如果 輸入字符串為 ${inputStr} ,當調用 Parentheses.execute(),則結果返回 false`, () => {
      assert.equal(Parentheses.execute(inputStr), true);
    });

    完成!

    總結

    通過上面的練習,相信大家應該能夠感受到 TDD 的威力,有興趣的同學可以不使用 TDD 將上面的功能重新實現一遍,對比一下兩次實現的時間和質量就知道要不要學習 TDD 這項技能。

    原文轉自:https://my.oschina.net/xbl/blog/3107866

    老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月

  • <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>