Testing Out TDD (Test-Driven Development)

devfish·2023년 3월 29일
0

Javascript

목록 보기
29/30

Why TDD?

According to Wikipedia, TDD is a SW development process relying on SW requirements being converted to test cases before a SW is fully developed, and tracking all SW dev by repeatedly testing the SW against all test cases. The opposite: SW being developed first, then test cases created later. It's an iterative process: you write test cases piecemeal, then write code that would pass the test cases, and repeat.

Reasons to use TDD

The idea is that thinking about testing prior to implementation would prime you to write more structured, less bug-prone code. You'd be more focused on meeting the SW requirements, and maybe you'd avoid unnnecessary planning. In short, it'll motivate you to write less buggy, more effective code.

This might look more time-consuming than jumping right into implementing features, but according to the proponents of TDD, this process might ultimately help you save time later for the reasons above. (Constant testing means you're constantly refining and removing unnecessary code.)

Not many developers strictly follow this approach, but might adopt some of its principles when and where they think it's needed. Whether TDD is still necessary and relevant today is a hotly debated topic (google "Is TDD Dead?" and see.)

TDD vs. BDD

https://cucumber.io/blog/bdd/bdd-vs-tdd/

Is TDD (or BDD) Relevant for Front-End?

It is definitely not irrelevant, but most definitely not standard practice. It is a non-trivial problem: TDD for front-end is not a well-defined problem with agreed-upon guidelines for developers to follow. Not to say there haven't been attempts to do this that offered valuable insights, just that the information available is scattered and hasn't been consolidated into something beyond individual reflections.

This link explores various facets and considerations for writing testable front-end code in React. This article asks can we? how? should we? guide for getting started with TDD in front-end (including a full demonstration as an example.) This article tries to outline a starting point for a structured, balanced apprach in testing UI components (again, using React as its example.) From what I've gathered, rigorous testing may not be the norm for present-day front-end development, but it's still a very relevant, important concern worth seriously pondering and chewing over.

https://www.smashingmagazine.com/2022/07/testable-frontend-architecture/

Test Frameworks

Commonly used testing frameworks/libraries such as mocha, chai, and Jest are open-source (not built-in to JS), and use keywords such as describe, it, assert, expect, etc. Mocha and chai run on Node.js and the browser, and can also be used in React. Jest works both in Node and React as well.

Using console.log is also a type of testing that doesn't use a framework.

Why Mocha?

Mocha's describe allows you to structure your tests by grouping test cases into a test suite (the describe function block is a suite, and the individual test/it functions are test cases.) describe can be nested.

Basic syntax

describes("test description", function(){
 
  it("A non-test that cannot fail", function() {
    // This does not test anything, so it's not even read 
    let even = function(num) {
      return num % 2 === 0;
    };
    return even(10) === true;
  });

  
  
  it("An error occurs when the expected behavior does not match the actual behavior", function() {
    let even = function(num) {
      return num / 2 === 0; //should be changed to num % 2 === 0  
    };

    if (even(10) !== true) {
      throw new Error("10 is not an even number?");
    }
  });
}

Installation & Set-Up

npm install mocha --save-dev
npm install mocha-multi-reporters --save-dev
npm i -D @mochajs/json-file-reporter --save-dev

-> save-dev : package will appear in your devDependencies. Plugins listed here will not be included in the production build.

Inside package.json, you can find all three above in the devDependencies.

  • json-file-reporter outputs the latest test results (incl. # of test suites, tests, start time, end time) in a report.json file.
  • mocha-multi-reporters enables you to "generate multiple mocha reports in a single mocha execution."

Inside the multi-reporters.json config, you can include the reporters in reporterEnabled (need to look more into this for how it works..)

{
    "reporterEnabled": "spec, my-org/custom" //custom folder inside the my-org node module
}

You can configure npm test to run a test file. You can set up basic configurations in an index test file and also load a different file that only includes the tests themselves like below.

global.chai = require("chai");
global.detectNetwork = require("../detectNetwork.js");

describe("Bare Minimum Requirements", function() {
  require("../detectNetwork.test.js");
});

Why Chai?

npm install chai --save-dev
config file: global.chai = require("chai");

Chai has convenient functions for testing. Without chai functions, you'd have to structure your tests in the if/throw format like below, which can be a drag. Chai allows assert, expect, should functions that are much more readable, and throws an error when the input is false. It also shows you exactly which part of the code produced the error.

// if/throw pattern 
describe("Diner's Club", function() {
  it("has a prefix of 38 and a length of 14", function() {
    if (detectNetwork("38345678901234") !== "Diner's Club") {
      throw new Error("Test failed");
    }
  });

  it("has a prefix of 39 and a length of 14", function() {
    if (detectNetwork("39345678901234") !== "Diner's Club") {
      throw new Error("Test failed");
    }
  });
});

//chai's assert function
describe("Visa", function() {
  let assert = chai.assert;
  //this is basically the same as.. 
  // let assert = function(isTrue) {
  //   if (!isTrue) {
  //     throw new Error("Test failed");
  //   }
  // };

  it("has a prefix of 4 and a length of 13", function() {
    assert(detectNetwork("4123456789012") === "Visa");
  });

  it("has a prefix of 4 and a length of 16", function() {
    assert(detectNetwork("4123456789012345") === "Visa");
  });

  it("has a prefix of 4 and a length of 19", function() {
    assert(detectNetwork("4123456789012345678") === "Visa");
  });
});

expect vs. should

Choose one style over another for consistency!

The BDD style comes in two flavors: expect and should. Both use the same chainable language to construct assertions, but they differ in the way an assertion is initially constructed. In the case of should, there are also some caveats and additional tools to overcome the caveats. ~Chai style guide

https://www.chaijs.com/guide/styles/

describe("MasterCard", function() {
  let expect = chai.expect;

  it("has a prefix of 51 and a length of 16", function() {
    expect(detectNetwork("5112345678901234")).to.equal("MasterCard");
  });
  
  //both were used for demonstration purposes - just stick to one in actual use
  let should = chai.should();

  it("has a prefix of 54 and a length of 16", function() {
    should.equal(detectNetwork("5412345678901234"), "MasterCard");
  });

  it("has a prefix of 55 and a length of 16", function() {
    should.equal(detectNetwork("5512345678901234"), "MasterCard");
  });
});

Sinon.js - Why Use Spies?

npm install sinon

Sinon.js tests spies, stubs and mocks for JS.

A spy is an object in testing that tracks calls made to a method. By tracking its calls, we can verify that it is being used in the way our function is expected to use it.

const spyDetectNetwork = sinon.spy(window, "detectNetwork");
const chaiSpyShould = sinon.spy(window.chai, "should");
const chaiSpyExpect = sinon.spy(window.chai, "expect");

//...

function checkDiscoverTest() {
  return checkCombination(
    [6011, 644, 645, 646, 647, 648, 649, 65],
    [16, 19],
    spyDetectNetwork
  );
}


function checkCombination(prefixs, lengths, spyDetectNetwork) {
  // 모든 카드 조합이 담겨인는 테이블을 만듭니다.
  const combinationTable = creatCombinationTable(prefixs, lengths);

  // 이때까지 detectNetwork함수에 실행된 카드번호와 프리픽스를 대조해보면서
  // 모든 테스트가 수행됬는지 검사합니다.
  spyDetectNetwork.args.forEach(function(cardNumArr) {
    // 카드 넘버가 배열에 담겨있어 배열을 카드 번호로 바꾼다.
    let cardNum = cardNumArr[0];

    // 조합 테이블에서 실행된 카드 번호를 찾아 해당 조합을 true로 변경합니다.
    for (let prefix in combinationTable) {
      // 테스트한 카드번호에 prefix가 있는지 확인합니다.
      if (checkPrefixCardNumber(prefix, cardNum)) {
        // 있다면 길이가 있는지 확인합니다.
        if (combinationTable[prefix].hasOwnProperty(cardNum.length)) {
          // 모두 있다면 조합 테이블에서 해당 조합을 true로 변경합니다.
          combinationTable[prefix][cardNum.length] = true;
        }
      }
    }
  });

  // 조합 테이블에서 false가 하나라도 있으면 false를 리턴합니다.
  for (let prefix in combinationTable) {
    for (let len in combinationTable[prefix]) {
      if (!combinationTable[prefix][len]) {
        return false;
      }
    }
  }

  // 조합 테이블 검사에서 모두 통과했다면 true를 리턴합니다.
  return true;
}

function creatCombinationTable(prefixs, lenghts) {
  const table = {};

  prefixs.forEach(function(prefix) {
    // 가능 한 모든 조합을 만들고 조합의 값을 false로
    lenghts.forEach(function(len) {
      if (!(prefix in table)) {
        table[prefix] = {};
      }
      table[prefix][len] = false;
    });
  });

  return table;
}

function checkPrefixCardNumber(prefix, cardNum) {
  // 카드 넘버와 프리픽스가 일치하는지 확인합니다.
  return prefix === cardNum.slice(0, prefix.length);
}

Practice: Credit Card Network Detector

Some hiccups..

  • How I structured my code- the detectNetwork function
function someInArray(arr, values){
  return values.some(value => {
    return arr.includes(value);
  })
}

function detectNetwork(cardNumber) {

  const DinersClub = {
    prefix : [38, 39],
    length : [14]
  };
  const AmExpress = {
    prefix : [34, 37],
    length : [15]
  };
  const MasterCard = {
    prefix : [51, 52, 53, 54, 55],
    length : [16]
  };
  const Visa = {
    prefix : [4],
    length : [13, 16, 19]
  };
  const Discover = {
    prefix : [6011, 65, ...Array.from({length: 6}, (_, i) => i + 644)],
    length : [16, 19]
  }


  const firstTwoNums = Number(cardNumber.slice(0,2));
  const len = cardNumber.length;
  //Diner's club
  if (DinersClub.prefix.includes(firstTwoNums) && DinersClub.length.includes(len)) return "Diner's Club";
  if (AmExpress.prefix.includes(firstTwoNums) && AmExpress.length.includes(len)) return "American Express";
  if (MasterCard.prefix.includes(firstTwoNums) && MasterCard.length.includes(len)) return "MasterCard";

  const firstNum = Number(cardNumber[0]);
  if (Visa.prefix.includes(firstNum) && Visa.length.includes(len)) return "Visa";

  // const firstFourNums = Number(cardNumber.slice(0,4));
  const prefixes = [firstTwoNums, Number(cardNumber.slice(0,3)), Number(cardNumber.slice(0,4))];
  if (someInArray(Discover.prefix, prefixes) && Discover.length.includes(len)) return "Discover";

  //throw error-?
  //this is just a function to see which card company it belongs to, so most likely it'll be
  //used by another function that validates if the number is correct
  //input the right credit card number
  return "None";
}

Points to Remember

  • Use for loops to test all scenarios
  • indexOf can be useful for quickly checking if an element is included in an array
    (returns an index number (>-1) when it's there, and -1 when it's not)
  else if (cardNumber[0] === '4' && ([13,16,19].indexOf(cardLength) > -1) && ['4903','4905','4911','4936'].indexOf(cardNumberFour) === -1) {
    return 'Visa';
  }
  • RegEx is super powerful for testing strings!

    let dinnerRegex = /^3[89]\d{12}$/;
    let americanRegex = /^3[47]\d{13}$/;
    let visaRegex = /^4(?:\d{12}|\d{15}|\d{18})$/;
    let masterRegex = /^5[1-5]\d{14}$/;
    let discoverRegex = /^65|^6011|^64[4-9]/;
    if(dinnerRegex.test(cardNumber)) {
      
      return "Diner's Club";
    }
    else if(americanRegex.test(cardNumber)) {
      return "American Express";
    }
    //Visa 카드번호는 항상 4로 시작하고 13, 16, 혹은 19자리의 숫자입니다.
    //indexOf()
    else if(visaRegex.test(cardNumber)){
      return "Visa";
    }
    //MasterCard 카드번호는 항상 51, 52, 53, 54, 혹은 55로 시작하고 16자리의 숫자입니다.
    // 51 <= Number(cardNumberTwo) <= 55
    else if(masterRegex.test(cardNumber)){
      return "MasterCard";
    }
    //Discover 카드번호는 항상 6011, 65, 644에서 649까지의 숫자로 시작하고 // 16 또는 19자리의 숫자입니다.
    else if(discoverRegex.test(cardNumber)){
      if([16, 19].indexOf(cardLength) > -1) return "Discover";
    }

References

Is TDD Dead?
BDD vs. TDD

TDD in a React frontend
Testable Frontend: The Good, The Bad And The Flaky
프론트엔드에서 TDD가 가능하다는 것을 보여드립니다. (FEConf Korea)
[2019] 실용적인 프런트엔드 테스트 전략 (NHN FE개발랩)
Google's Retrofitted Testing Culture
Do teams from Google and Facebook use TDD to write software?

Best Practices for Spies, Stubs and Mocks in Sinon.js
Using Spies for Testing in JavaScript with Sinon.js

profile
la, di, lah

0개의 댓글