TDD

測試導向開發 (Test Driven Development, TDD)

2022/03/24 (新增連結)

概念

測試導向開發不只是重視測試,而且是在開發前先準備測試案例,而且以測試來主導開發。根據Version One(2018)的調查,就敏捷的測試相關實務而言,有75%採用Unit Test,35%採用TDD,32%採用Automated Acceptance Testing,17%採用BDD。

很多人會認為,開發的時間都來不及了,怎麼可能先準備好測試案例? 以TDD的概念,就是在寫需求的時候,就開始準備測試案例,也利用撰寫測試案例的過程來確認需求,這樣並不會花費更多時間,因為可以節省誤會需求的浪費,另外,也會因為測試自動化,可以減少在測試上花的時間,也一併提昇開發的品質。

TDD.pptx

2021/12/16 (新增投影片)

Katalon

API Testing

測試api要比測試web UI稍微單純一點,所以,我們來介紹一下如何利用Katalon進行API測試。

利用Katalon提供的API來測試。

先設定web request,命名為UserGet,URL:

https://sample-web-service-aut.herokuapp.com/api/users/1

應該收到的內容:

<User>

<id>1</id>

<username>John Smith</username>

<password>123</password>

<gender>MALE</gender>

<age>25</age>

<avatar/>

</User>

Test case的腳本:

import static com.kms.katalon.core.checkpoint.CheckpointFactory.findCheckpoint

import static com.kms.katalon.core.testcase.TestCaseFactory.findTestCase

import static com.kms.katalon.core.testdata.TestDataFactory.findTestData

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import static com.kms.katalon.core.testobject.ObjectRepository.findWindowsObject

import com.kms.katalon.core.checkpoint.Checkpoint as Checkpoint

import com.kms.katalon.core.cucumber.keyword.CucumberBuiltinKeywords as CucumberKW

import com.kms.katalon.core.mobile.keyword.MobileBuiltInKeywords as Mobile

import com.kms.katalon.core.model.FailureHandling as FailureHandling

import com.kms.katalon.core.testcase.TestCase as TestCase

import com.kms.katalon.core.testdata.TestData as TestData

import com.kms.katalon.core.testobject.TestObject as TestObject

import com.kms.katalon.core.webservice.keyword.WSBuiltInKeywords as WS

import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI

import com.kms.katalon.core.windows.keyword.WindowsBuiltinKeywords as Windows

import internal.GlobalVariable as GlobalVariable


response = WS.sendRequest(findTestObject('UserGet'))


WS.verifyResponseStatusCode(response, 200)


WS.verifyElementPropertyValue(response, 'id', 1)

WS.verifyElementPropertyValue(response, 'username', 'John Smith')

WS.verifyElementPropertyValue(response, 'password', '123')

如果想要有自己的API,可以使用Airtable。Airtable是個雲端資料庫,跟一般的關聯式資料庫不同的是,airtable支援資料表間的多對多關係,使用Airtable還有一個好處是,Airtable提供RESTful web service的API。

新增web request,命名為: Airtable Table1,並設定web request的URL:

https://api.airtable.com/v0/appLNbQOB0WL1ZeDD/Table%201?maxRecords=30&view=Grid%20view

HTTP header:

Authorization Bearer KeyXXXXXXXXXX

老師寫的script

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import com.kms.katalon.core.webservice.keyword.WSBuiltInKeywords as WS


response = WS.sendRequest(findTestObject('Airtable Table1'))


WS.verifyResponseStatusCode(response, 200)


WS.verifyElementPropertyValue(response, 'records[0].fields.Name', 'Ben Wu')

WS.verifyElementPropertyValue(response, 'records[1].fields.Name', 'Ana')


WS.verifyElementPropertyValue(response, 'records[0].fields.City', '台北市')

WS.verifyElementPropertyValue(response, 'records[1].fields.City', '新北市')

API Testing.pptx

Web UI Testing

可參考老師錄好的script 

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import com.kms.katalon.core.testobject.TestObject as TestObject

import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI


WebUI.openBrowser('https://katalon-demo-cura.herokuapp.com/')


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/a_Make Appointment'))


WebUI.setText(findTestObject('Object Repository/Page_CURA Healthcare Service/input_Username_username'), 'John Doe')


WebUI.setEncryptedText(findTestObject('Object Repository/Page_CURA Healthcare Service/input_Password_password'), 'g3/DOGG74jC3Flrr3yH+3D/yKbOqqUNM')


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/button_Login'))


WebUI.selectOptionByValue(findTestObject('Object Repository/Page_CURA Healthcare Service/select_Tokyo CURA Healthcare Center        _5b4107'), 

    'Hongkong CURA Healthcare Center', true)


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/input_Apply for hospital readmission_hospit_63901f'))


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/input_Medicaid_programs'))


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/span_Visit Date (Required)_glyphicon glyphi_cada34'))


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/td_20'))


WebUI.setText(findTestObject('Object Repository/Page_CURA Healthcare Service/textarea_Comment_comment'), 'A regular health exam')


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/button_Book Appointment'))


WebUI.verifyElementText(findTestObject('Object Repository/Page_CURA Healthcare Service/p_Hongkong CURA Healthcare Center'), 

    'Hongkong CURA Healthcare Center')


WebUI.verifyElementText(findTestObject('Object Repository/Page_CURA Healthcare Service/p_Yes'),'Yes')


WebUI.verifyElementText(findTestObject('Object Repository/Page_CURA Healthcare Service/p_Medicaid'),'Medicaid')


WebUI.verifyElementText(findTestObject('Object Repository/Page_CURA Healthcare Service/p_20052020'),'20/05/2020')


WebUI.verifyElementText(findTestObject('Object Repository/Page_CURA Healthcare Service/p_A regular health exam'), 'A regular health exam')


WebUI.click(findTestObject('Object Repository/Page_CURA Healthcare Service/a_Go to Homepage'))


WebUI.closeBrowser()


Web UI Testing.pptx

Mobile Testing

參考資料

Javascript

寫jest的測試,如:src/utils/friday.test.js

import isItFriday from './friday';


describe('Today is or is not Friday',() => {

  test('Sunday is "Nope"', () => {

    expect(isItFriday('Sunday')==='Nope');

  });

  test('Friday is "TGIF"', () => {

    expect(isItFriday('Friday')==='TGIF');

  });

});

也可以這樣寫:

import isItFriday from './friday';

describe.each`

  day    | answer

  ${'Friday'} | ${'TGIF'}

  ${'Sunday'} | ${'Nope'}

  ${'anything else'} | ${'Nope'}

`('$day + $answer', ({day,answer}) => {

  test(`Sunday is ${answer}`, () => {

    expect(isItFriday(day)===answer);

  });

});

src/utils/friday.js

export default (today) => {

    if (today === "Friday") {

        return "TGIF";

      } else {

        return "Nope";

      }};

看起來,測試要比被測試的程式碼還要長。當然,我們不會測試每一個簡單的功能,而是針對一些比較複雜的功能來寫測試,確保程式碼的正確性。而且,當新功能完成時,可以執行所有的測試,確保新功能沒有影響既有的所有功能。所以,當系統越長越大的時候,就可以感受到測試自動化的好處了。

//Define the Calc constructor method 

function Calc(firstNumber, secondNumber) {

    //Define the two properties 

    this.firstNumber = firstNumber;

    this.secondNumber = secondNumber;

    //Define your add method that will return the result of adding two properties.

    this.add = function() {

        return this.firstNumber + this.secondNumber;

    }

}


//Assign variables to an instance of Calc with 5 and 10, and also numbers switched around. 

const numberToAdd = new Calc(5, 10);

const numberToAddReversed = new Calc(10, 5);


//Group your add test

describe('Check if the add method works as inteded', () => {

    test('Check is 5 + 10 is falsey', () => {

        expect(numberToAdd.add() === 50).toBeFalsy();

    });

    test('Check is 5 + 10 is truthy', () => {

        expect(numberToAdd.add() === 15).toBeTruthy();

    });

    test('Check if numbers switched around is truthy', () => {

        expect(numberToAddReversed.add()).toEqual(15);

    });

})

如果要測試react,enzyme提供了一些方法來測試

先安裝相關的套件 (假設已經安裝jest)

npm install enzyme enzyme-adapter-react-16

src/setupTests.js

import { configure } from 'enzyme';

import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

src/App.js

import React, { Component } from 'react';

//import logo from './logo.svg';

import './App.css';

import Calculator from './Calculator/Calculator';


class App extends Component {

  render() {

    return (

      <div>

      <h2>React Calculator</h2>

      <Calculator />

    </div>

    );

  }

}


export default App;

src/App.css

.App {

  text-align: center;

}


.App-logo {

  animation: App-logo-spin infinite 20s linear;

  height: 40vmin;

  pointer-events: none;

}


.App-header {

  background-color: #282c34;

  min-height: 100vh;

  display: flex;

  flex-direction: column;

  align-items: center;

  justify-content: center;

  font-size: calc(10px + 2vmin);

  color: white;

}


.App-link {

  color: #61dafb;

}


@keyframes App-logo-spin {

  from {

    transform: rotate(0deg);

  }

  to {

    transform: rotate(360deg);

  }

}

src/App.test.js 測試元件存在及測試內容,shallow是只有產生App,而mount則是產生及App所包含的元件。

import React from 'react';

import App from './App';

import {shallow, mount} from 'enzyme' 


describe('Calculator component', () => {

  it('renders Calculator', () => {

    const component = shallow(

      <App />

    )

    expect(component.exists('Calculator')).toBe(true)

  })

})


describe('App: ', () => {  

  const wrapper = mount(<App/>);    

  it('It shows,"React Calculator"', () => {   

      expect(wrapper.text()==='React Calculator');    

  })    

});


參考資料