pool13433

Cypress Route คืออะไร ใช้งานอย่างไร EP6

การทดสอบ application ในบางครั้งก็จะมี dependencies อื่น ๆ ที่เกี่ยวข้องมากมาย โดยหนึ่งในนี้คือ การเรียก API data ที่ไม่สามารถทราบได้เลยว่าการที่ API ตอบกลับมานั้นจะช้าหรือเร็วแค่ใหน การเทสบางครั้งอาจราบลื่นเป็นปกติ เพราะไม่มีการถูกขัดจังหวะด้วย response time ของ API data ที่เร็ว แต่เมื่อ run test อีกครั้งกับได้ผลรับไม่เหมือนเดิม เกิด failures เพราะมี error message แจ้งกลับมาว่า timeout waiting 5000ms บ่งบอกได้ถึงการรอ network request API ที่นานจนโปรแกรมรอไม่ไหว แบบนี้จะแก้ได้อย่างไร

การ Mock API แก้ pain point นี้ได้

การ Mock API ช่วยแก้ pain point นี้ได้ โดยมีหลักการคือการ จำลอง response data ที่สร้างขึ้นมาเพื่อที่จะไม่ต้องเรียก API request จริง วิธีการนี้จะช่วย ควบคุม response time ได้ทำให้ test ของเราทำงานได้ราบลื่นไม่ติดขัด

Network Requests Route คืออะไร

Cypress ได้สร้าง API Command ที่ชื่อว่า route เพื่อใช้บริหารจัดการ XHR Object Request ของระดับ HTTP โดยทำการสร้างสภาพแวดล้อมเสมือนคอยตรวจสอบดักจับการเรียก Network URL ที่สร้างขึ้นโดยที่เราสามารถปรับปรุงเปลี่ยนแปลง Request ,Response ของ HTTP ในระดับ network layer ได้

ตัวอย่างและวิธีการเรียกใช้งาน

cy.server()

cy.route(url)
cy.route(url, response)
cy.route(method, url)
cy.route(method, url, response)
cy.route(callbackFn)
cy.route(options)

usercase ที่จะนำมายกตัวอย่างคือการ call API ของ coronavirus-19-api data ของประเทศทั้งหมดในโลกที่มีการติดเชื้อ Covid ณ ปัจจุบัน จะยกตัวอย่างการเปลี่ยนแปลงข้อมูล เมื่อเรียก api นี้ route จะทำหน้าที่ mock response ของ API เดิมที่จะ return Response Data ของทุกประเทศ แต่จะ return Response Data ของ Thailand เท่านั้น

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Window Methods</title>
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css"
    />
  </head>
  <body>
    <div class="ui container">
      <h2>Course Cypress Automated Testing</h2>
      <div class="ui top attached tabular menu">
        <a class="item" href="index.html" data-tab="zero">Index</a>
        <a class="item" href="register.html" data-tab="first">Register</a>
        <a class="item" href="register-list.html" data-tab="first-list"
          >Register List</a
        >
        <a class="item" href="window.html" data-tab="second">Window</a>
        <a class="item" href="login.html" data-tab="third">Login</a>
        <a class="item" href="change-password.html" data-tab="four"
          >Change Password</a
        >
        <a class="item active" href="table.html" data-tab="five"
          >Table Coronavirus-19</a
        >
        <a class="item" href="elements.html" data-tab="six">Chai-jQuery</a>
      </div>
      <div class="ui bottom attached tab segment active" data-tab="first">
        <table class="ui tablet stackable celled striped table" id="covidState">
          <thead>
            <tr>
              <th>No.</th>
              <th>Country</th>
              <th>Cases</th>
              <th>Today Cases</th>
              <th>Deaths</th>
              <th>Today Deaths</th>
              <th>Recovered</th>
              <th>Active</th>
              <th>Critical</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>
    <script>
      Number.prototype.toCurrency = function (n = 2, x = 3) {
        var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\.' : '$') + ')';
        return this.toFixed(Math.max(0, ~~n)).replace(
          new RegExp(re, 'g'),
          '$&,'
        );
      };

      window.onload = () => {
        const data = fetch('https://coronavirus-19-api.herokuapp.com/countries')
          .then((http) => http.json())
          .then((data) => {
            //console.log('data ::==', data);
            const $covidState = document.getElementById('covidState');
            let $tbody = covidState.children[1]; // tbody
            //console.log('$tbody ::==',$tbody)
            data
              .map((item, index) => {
                return { ...item, ...{ no: index + 1 } };
              })
              .forEach((item) => {
                let $tr = document.createElement('TR');
                $tr.innerHTML = `
              <td>${item.no}</td>
              <td>${item.country}</td>
              <td>${(item.cases || 0).toCurrency(0)}</td>
              <td>${(item.todayCases || 0).toCurrency(0)}</td>
              <td>${(item.deaths || 0).toCurrency(0)}</td>
              <td>${(item.todayDeaths || 0).toCurrency(0)}</td>
              <td>${(item.recovered || 0).toCurrency(0)}</td>
              <td>${(item.active || 0).toCurrency(0)}</td>
              <td>${(item.critical || 0).toCurrency(0)}</td>
          `;
                $tbody.appendChild($tr);
              });
          });
      };
    </script>
  </body>
</html>

การทำงานของโปรแกรมมหน้านี้ เมื่อโหลดหน้าขึ้นมาจะไปเรียก API Covid นี้ทันทีด้วย window.fetch

ทดสอบสร้าง testscript ของการใช้คำสั่ง route

describe('ทดสอบการทำงานและเรียกใช้งาน Route', () => {

  let polyfill

  before(() => {
    const polyfillUrl = 'https://unpkg.com/[email protected]/dist/fetch.umd.js'
    cy.request(polyfillUrl).then(response => {
      polyfill = response.body
    })
  })

  Cypress.on('window:before:load', win => {
    delete win.fetch
    win.eval(polyfill)
  })

  it('เรียก API Covid', () => {
    cy.server()
    cy.route({
      url: '**/countries',
      method: 'GET',      // Route all GET requests
      response: [{
        "country": "Thailand",
        "cases": 3202,
        "todayCases": 5,
        "deaths": 58,
        "todayDeaths": 0,
        "recovered": 3085,
        "active": 59,
        "critical": 1,
        "casesPerOneMillion": 46,
        "deathsPerOneMillion": 0,
        "totalTests": 603657,
        "testsPerOneMillion": 8648
      }]
    }).as('routeCovid')
    cy.visit('https://cypress-testing-143fd.web.app/table.html')
      .get('#covidState').contains('Country')
      .wait('@routeCovid', { timeout: 10000 }).its('status').should('have.eq', 200)
  })
})

อธิบายการทำงานในส่วนโค๊ดนี้

บรรทัดที่ 18 เริ่มสร้าง network server (cy.server())
บรรทัดที่ 19 สร้าง route เพื่อ mock api ที่ติดต่อภายนอกโดยกำหนด response คือ array object ของ ประเทศไทย เท่านั้น
บรรทัดที่ 37 visit เข้าไปที่หน้าเว็บตัวอย่าง
บรรทัดที่ 39 ตรวจสอบ status code equal 200 หรือไม่

เมื่อลองทดสอบ run test การทำงานของโปรแกรมจะเปลี่ยนไป ระบบจะแสดงแค่ข้อมูลของประเทศไทย โดยที่ไม่ได้มีการแก้ไขโปรแกรมที่ทดสอบแต่อย่างไร เพียงแต่ทำการ mockup API ที่แสดงข้อมูลด้วย Cypress Route เท่านั้น

เพียงเท่านี้เราก็สามารถควบคุม data ที่มาจากการ call API จากที่ต่าง ๆ ได้ แล้วโดยไม่ต้องรอการ response กลับจาก API นั้น ๆ

หากเพื่อนคนใดอยากที่จะทำความรู้จัก Route Command นี้เพิ่มเติมให้เข้าไปที่ลิ้งนี้ได้เลย Cypress Route

Cypress ทำความรู้จัก และตัวอย่างการใช้งาน Spy,Stub EP5

Spy & Stub เป็น API Command ที่มีความพิเศษกว่า API Command อื่น ๆ เราจะไม่ค่อยได้ใช้งาน command นี้มากนัก แต่ถ้าถึงสถานะการนั้นแล้ว spy ,stub นี้แหละที่จะมาเป็นพระเอกช่วยเราได้ในแต่ละสถานะการนั้น ๆ เลย

Spy ,Stub คืออะไร

ก่อนที่จะไปรู้ถึงว่าจะนำ Spy ,Stub ไปใช้กับลักษณะงานแบบไหน ควรต้องเข้าใจก่อนว่า 2 commands นี้ คืออะไร มีความสามารถอย่างไรบ้าง

Stub ทำหน้าที่จำลอง Behavior ของโปรแกรมที่เรากำลังจะทดสอบ โดยหลักการทำงานของ Stub ก็ดัก function ของ Object ของโปรแกรมในที่นี้คือ Object ที่กำลังสนใจมีส่วนการทำงานเกี่ยวข้องกับระบบ หรือ feature ที่กำลังทำงานในขณะนั้น โดยจะคอยดักจับ และยังสามารถเปลี่ยนแปลงการทำงานของ Behavior function นั้น ไปให้เป็นไปตามผลลัพธ์ที่เราต้องการจะให้เป็น โดยข้อดีของการใช้ Stub เพื่อแก้ไข function (Behavior) นี้เพื่อลด dependencies ต่าง ๆ ของโปรแกรมทำไม่ให้เกิด Flaky Test เช่น window.confirm ของ window object ที่ต้องการ action ของปุ่ม Ok (ตกลง) หรือ Cancel (ยกเลิก) เพื่อทำให้โปรแกรมทำงานใน Step ถัด ๆ ไป โดยปกติ API Command ที่มีให้นั้นไม่สามารถจะแก้ไขหรือเลือก choice นี้ได้ จำเป็นต้องใช้ Stub มาเปลี่ยนแปลง Behavior นี้

Spy เกือบที่จะเหมือนกับ Stub ถ้าในทางการเรียกใช้งานแทบจะแยกไม่ออกเลยว่าแตกต่างกันอย่างไร แต่มีสิ่งที่ Spy เองไม่สามารถทำได้เหมือน Stub คือการเปลี่ยนแปลง Behavior ของการทำงานได้เหมือน Stub แต่ทำหน้าที่ได้เพียงแฝงตัว สอดแนม อาจจะเพิ่มการ capture ,record หรือ track log ของการทำงานได้เท่านั้น

Spy /Stub การทำงานเหล่านี้ได้นำเข้ามาจาก Sinon.js จึงสามารถเปิด document ได้เลย สามารถอ่านเนื้อหาการใช้งานได้จากที่นี่
Stub https://sinonjs.org/releases/latest/stubs/
Spy https://sinonjs.org/releases/latest/spies/

เมื่อไหร่ และสถานการณ์แบบไหนถึงจะนำมาใช้ละ

มอง scenarios case จะเกี่ยวกับ behavior ของ javascript function ต่าง ๆ ที่ทีในโปรแกรมที่กำลังทดสอบ เช่น window.alert ,window.confirm ,window.promt หรือแม้แต่ function ต่าง ๆ ที่ custom define ที่ window object ก็สามารถดัก track ติดตามได้หมด

ตัวอย่างโปรแกรม

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button type="button" onclick="clickPromt()">Click Promt</button>
    <script>
      function clickPromt(){
        const data = window.prompt('enter your name.')
        if(data){
          window.alert(data)
        }
      }
    </script>
  </body>
</html>
describe('ทดสอบการทำงานของ Stub', () => {
  it('ทดสอบดักจับ behavior ของ window.promt', () => {
    cy.visit('http://127.0.0.1:5500/cypress/integration/spy-stub/index.html')
    .window().then(win =>{
      cy.stub(win,'prompt',()=>{
        return 'Hardcode "Hello World"'
      }).as('stubPromt')
    })
    .get('button').click()
    .get('@stubPromt').should('be.calledOnce')
  });
});

ตัวอย่างการทำงานของ Stub

เมื่อมีการเขียนคำสั่ง stub เพื่อเริ่มใช้งาน cypress จะสร้างแถบ SPIES/STUBS มาพร้อมแสดงชื่อการทำงานของ behavior นั้น เมื่อโปรแกรมมีการเรียกใช้งาน behavior (function) stub จะแสดงเลข 1 หมายถึงการถูกเรียกทำงาน 1 ครั้ง และโปรแกรมจะ alert ค่าที่รับจาก promt ว่า “Hardcode “Hello World”” ที่ถูก command stub return ค่ามาให้ จึงทำให้การ call function ภายในโปรแกรมทุกครั้งจะได้ promt value ว่า “Hardcode Hello World” เสมอ เพราะถูก stub ดักเปลี่ยนค่าในระหว่างการทำงาน
เพิ่มการ assert เพื่อตรวจสอบการ call function นั้นด้วย “be.calledOnce” อีกครั้งเพื่อให้แน่ใจว่าโปรแกรมทำงาน function อย่างน้อย 1 ครั้ง

นี่คือตัวอย่างการใช้งาน stub กับ function promt ที่ทาง window object ของ browser มีให้ใช้งานเป็นปกติอยู่แล้ว เพื่อน ๆ ที่ติดตามบทความนี้มีปัญหา หรืออยากปรึกษา ขอความช่วยเหลือ สามารถฝากคำถามไว้ใต้โพสนี้ได้เลย

Cypress Assertions ต่าง ๆ ที่ควรต้องรู้ EP4

บทความก่อนหน้านี้ ทำให้เราได้พอที่จะเข้าใจ concept ของการใช้วาน Cypress กันมาบ้างแล้ว บทความนี้จะขอกล่าวถึงเรื่องการ Assertion Expect Case ต่าง ๆ ว่าเขียนอย่างไร และสามารถทำอยากที่เราต้องการได้หรือไม่

อธิบายการ Assertions ของ Automated Testing

assertion (การยืนยัน) ในทางงาน test คือการยืนยันการตรวจสอบของสิ่งที่กำลังสนใจ กับผลลัพธ์ที่คาดหวัง

describe('My First Test', () => {
  it('Does not do much!', () => {
    expect(true).to.equal(true)
  })
})

สร้างไฟล์ my-first-test.spec.js เขียนโค๊ดการเทสง่าย ๆ จากนั้นใส่ assertion expect(true).to.equal(true) โดย true แรกคือการที่เราจะแทนค่าที่เรากำลังสนใจที่จะตรวจสอบ true ค่าที่ 2 คือผลลัพธ์ที่ต้องการให้เป็นผลลัพธ์สำหรับการเทสในเงื่อนไขนี้

BDD Assertions

การทดสอบ case scenario ก่อนการเริ่มเขียนโค๊ดจริง ที่เพิ่มการคาดหวังทดสอบ ในเรื่องพฤษติกรรมของทำงานของ website cypress ใช้ library ของ Chaijs Assertion Library (Expect/Should) ที่มีพร้อมครบในเรื่องการทำ assertion case ต่าง ๆ

const person = {
  name: 'Poolsawat',
  friends: [
    { name: 'Jane' },
    { name: 'Poom', age: 30 },
  ],
  cars: [
    'Nissan',
    'Toyota'
  ],
  getName: () => {
    return "Poolsawat"
  },
  "aa bb" : "Test"
}

// https://github.com/chaijs/chai
describe('BDD Assertions Chaijs', () => {

  it('use expect expected json data', () => {
    expect(person.name).to.not.equal('Jane')
    expect(person.friends[0]).to.deep.equal({ name: 'Jane' })
    expect({ a: { b: ['x', 'y'] } }).to.have.nested.property('a.b[1]')
    expect({ a: { b: ['x', 'y'] } }).to.nested.include({ 'a.b[1]': 'y' })
    expect([1, 2]).to.have.ordered.members([1, 2]).but.not.have.ordered.members([2, 1])
    expect(person.friends[1]).to.have.any.keys('age')
    expect(person.friends[1]).to.have.all.keys('name', 'age')
    expect(person.name).to.be.a('string')

    expect([1, 2, 3]).to.include(2)

    expect(undefined).to.not.be.ok
    expect(true).to.be.true
    expect(false).to.be.false
    expect(null).to.be.null
    expect(undefined).to.be.undefined
    expect(person['aa bb']).to.exist
    expect([]).to.be.empty
    expect(arguments).to.be.arguments

    expect(42).to.equal(42)

    expect({ name: 'Jane' }).to.deep.equal({ name: 'Jane' })
    expect({ name: 'Jane' }).to.eql({ name: 'Jane' })

    expect(10).to.be.greaterThan(5)

    expect(10).to.be.at.least(10)

    expect(5).to.be.lessThan(10)

    expect('test').to.have.length.of.at.most(4)

    expect(7).to.be.within(5, 10)
    expect([1, 2, 3]).to.be.instanceOf(Array)

    expect(person).to.have.property('name')
    expect(person).to.have.deep.property('friends')
    expect('test').to.have.ownProperty('length')

    expect({ a: 1 }).to.have.ownPropertyDescriptor('a')

    expect('test').to.have.lengthOf(4)
    expect('testing').to.match(/^test/)

    expect('testing').to.have.string('test')
    expect({ pass: 1, fail: 2 }).to.have.keys('pass', 'fail')
    const fn = () => {
      throw Error
    }
    //expect(fn).to.throw(Error)

    expect(person).to.respondTo('getName')

    //expect(person).itself.to.respondTo('name')
    expect(1).to.satisfy((num) => { return num > 0 })

    expect(1.5).to.be.closeTo(1, 0.5)

    expect([1, 2, 3]).to.include.members([3, 2])
    expect(2).to.be.oneOf([1, 2, 3])
    //expect(fn).to.change(obj, 'val')
    //expect(fn).to.increase(obj, 'val')
    //expect(fn).to.decrease(obj, 'val')

  })
})
Chainer Example
not expect(name).to.not.equal('Jane')
deep expect(obj).to.deep.equal({ name: 'Jane' })
nested expect({a: {b: ['x', 'y']}}).to.have.nested.property('a.b[1]')
expect({a: {b: ['x', 'y']}}).to.nested.include({'a.b[1]': 'y'})
ordered expect([1, 2]).to.have.ordered.members([1, 2]).but.not.have.ordered.members([2, 1])
any expect(arr).to.have.any.keys('age')
all expect(arr).to.have.all.keys('name', 'age')
a(type)
Aliases: an
expect('test').to.be.a('string')
include(value)
Aliases: contain, includes, contains
expect([1,2,3]).to.include(2)
ok expect(undefined).to.not.be.ok
true expect(true).to.be.true
false expect(false).to.be.false
null expect(null).to.be.null
undefined expect(undefined).to.be.undefined
exist expect(myVar).to.exist
empty expect([]).to.be.empty
arguments
Aliases: Arguments
expect(arguments).to.be.arguments
equal(value)
Aliases: equals, eq
expect(42).to.equal(42)
deep.equal(value) expect({ name: 'Jane' }).to.deep.equal({ name: 'Jane' })
eql(value)
Aliases: eqls
expect({ name: 'Jane' }).to.eql({ name: 'Jane' })
greaterThan(value)
Aliases: gt, above
expect(10).to.be.greaterThan(5)
least(value)
Aliases: gte
expect(10).to.be.at.least(10)
lessThan(value)
Aliases: lt, below
expect(5).to.be.lessThan(10)
most(value)
Aliases: lte
expect('test').to.have.length.of.at.most(4)
within(start, finish) expect(7).to.be.within(5,10)
instanceOf(constructor)
Aliases: instanceof
expect([1, 2, 3]).to.be.instanceOf(Array)
property(name, [value]) expect(obj).to.have.property('name')
deep.property(name, [value]) expect(deepObj).to.have.deep.property('tests[1]', 'e2e')
ownProperty(name)
Aliases: haveOwnProperty, own.property
expect('test').to.have.ownProperty('length')
ownPropertyDescriptor(name)
Aliases: haveOwnPropertyDescriptor
expect({a: 1}).to.have.ownPropertyDescriptor('a')
lengthOf(value) expect('test').to.have.lengthOf(3)
match(RegExp)
Aliases: matches
expect('testing').to.match(/^test/)
string(string) expect('testing').to.have.string('test')
keys(key1, [key2], […])
Aliases: key
expect({ pass: 1, fail: 2 }).to.have.keys('pass', 'fail')
throw(constructor)
Aliases: throws, Throw
expect(fn).to.throw(Error)
respondTo(method)
Aliases: respondsTo
expect(obj).to.respondTo('getName')
itself expect(Foo).itself.to.respondTo('bar')
satisfy(method)
Aliases: satisfies
expect(1).to.satisfy((num) => { return num > 0 })
closeTo(expected, delta)
Aliases: approximately
expect(1.5).to.be.closeTo(1, 0.5)
members(set) expect([1, 2, 3]).to.include.members([3, 2])
oneOf(values) expect(2).to.be.oneOf([1,2,3])
change(function)
Aliases: changes
expect(fn).to.change(obj, 'val')
increase(function)
Aliases: increases
expect(fn).to.increase(obj, 'val')
decrease(function)
Aliases: decreases
expect(fn).to.decrease(obj, 'val')

TDD Assertions

คล้ายกับ BDD คือเป็นการเขียน test ต่าง ก่อนการเริ่มเขียน code แต่จะไม่ละเอียดเท่า BDD เพราะไม่รู้ Behavior(พฤติกรรม) ของผู้ใช้งานสามารถใช้ ChaiJs Assert Feature

describe('TDD Assertions', () => {
  it('use assert.* ', () => {
    assert.isOk('everything', 'everything is ok')
    assert.isNotOk(false, 'this will pass')
    assert.equal(3, 3, 'vals equal')
    assert.notEqual(3, 4, 'vals not equal')
    assert.strictEqual(true, true, 'bools strict eq')
    assert.notStrictEqual(5, '5', 'not strict eq')
    assert.deepEqual({ id: '1' }, { id: '1' })
    assert.notDeepEqual({ id: '1' }, { id: '2' })
    assert.isAbove(6, 1, '6 greater than 1')
    assert.isAtLeast(5, 2, '5 gt or eq to 2')
    assert.isBelow(3, 6, '3 strict lt 6')
    assert.isAtMost(4, 4, '4 lt or eq to 4')
    assert.isTrue(true, 'this val is true')
    assert.isNotTrue('tests are no fun', 'val not true')
    assert.isFalse(false, 'val is false')
    assert.isNotFalse('tests are fun', 'val not false')
    //assert.isNull(err, 'there was no error')
    assert.isNotNull('hello', 'is not null')
    assert.isNaN(NaN, 'NaN is NaN')
    assert.isNotNaN(5, '5 is not NaN')
    assert.exists(5, '5 is not null or undefined')
    assert.notExists(null, 'val is null or undefined')
    assert.isUndefined(undefined, 'val is undefined')
    assert.isDefined('hello', 'val has been defined')
    assert.isFunction(x => x * x, 'val is func')
    assert.isNotFunction(5, 'val not funct')
    assert.isObject({ num: 5 }, 'val is object')
    assert.isNotObject(3, 'val not object')
    assert.isArray(['unit', 'e2e'], 'val is array')
    assert.isNotArray('e2e', 'val not array')
    assert.isString('e2e', 'val is string')
    assert.isNotString(2, 'val not string')
    assert.isNumber(2, 'val is number')
    assert.isNotNumber('e2e', 'val not number')
    //assert.isFinite('e2e', 'val is finite')
    assert.isBoolean(true, 'val is bool')
    assert.isNotBoolean('true', 'val not bool')
    assert.typeOf('e2e', 'string', 'val is string')
    assert.notTypeOf('e2e', 'number', 'val not number')

  })
})
Assertion Example
.isOk(object, [message]) assert.isOk('everything', 'everything is ok')
.isNotOk(object, [message]) assert.isNotOk(false, 'this will pass')
.equal(actual, expected, [message]) assert.equal(3, 3, 'vals equal')
.notEqual(actual, expected, [message]) assert.notEqual(3, 4, 'vals not equal')
.strictEqual(actual, expected, [message]) assert.strictEqual(true, true, 'bools strict eq')
.notStrictEqual(actual, expected, [message]) assert.notStrictEqual(5, '5', 'not strict eq')
.deepEqual(actual, expected, [message]) assert.deepEqual({ id: '1' }, { id: '1' })
.notDeepEqual(actual, expected, [message]) assert.notDeepEqual({ id: '1' }, { id: '2' })
.isAbove(valueToCheck, valueToBeAbove, [message]) assert.isAbove(6, 1, '6 greater than 1')
.isAtLeast(valueToCheck, valueToBeAtLeast, [message]) assert.isAtLeast(5, 2, '5 gt or eq to 2')
.isBelow(valueToCheck, valueToBeBelow, [message]) assert.isBelow(3, 6, '3 strict lt 6')
.isAtMost(valueToCheck, valueToBeAtMost, [message]) assert.isAtMost(4, 4, '4 lt or eq to 4')
.isTrue(value, [message]) assert.isTrue(true, 'this val is true')
.isNotTrue(value, [message]) assert.isNotTrue('tests are no fun', 'val not true')
.isFalse(value, [message]) assert.isFalse(false, 'val is false')
.isNotFalse(value, [message]) assert.isNotFalse('tests are fun', 'val not false')
.isNull(value, [message]) assert.isNull(err, 'there was no error')
.isNotNull(value, [message]) assert.isNotNull('hello', 'is not null')
.isNaN(value, [message]) assert.isNaN(NaN, 'NaN is NaN')
.isNotNaN(value, [message]) assert.isNotNaN(5, '5 is not NaN')
.exists(value, [message]) assert.exists(5, '5 is not null or undefined')
.notExists(value, [message]) assert.notExists(null, 'val is null or undefined')
.isUndefined(value, [message]) assert.isUndefined(undefined, 'val is undefined')
.isDefined(value, [message]) assert.isDefined('hello', 'val has been defined')
.isFunction(value, [message]) assert.isFunction(x => x * x, 'val is func')
.isNotFunction(value, [message]) assert.isNotFunction(5, 'val not funct')
.isObject(value, [message]) assert.isObject({num: 5}, 'val is object')
.isNotObject(value, [message]) assert.isNotObject(3, 'val not object')
.isArray(value, [message]) assert.isArray(['unit', 'e2e'], 'val is array')
.isNotArray(value, [message]) assert.isNotArray('e2e', 'val not array')
.isString(value, [message]) assert.isString('e2e', 'val is string')
.isNotString(value, [message]) assert.isNotString(2, 'val not string')
.isNumber(value, [message]) assert.isNumber(2, 'val is number')
.isNotNumber(value, [message]) assert.isNotNumber('e2e', 'val not number')
.isFinite(value, [message]) assert.isFinite('e2e', 'val is finite')
.isBoolean(value, [message]) assert.isBoolean(true, 'val is bool')
.isNotBoolean(value, [message]) assert.isNotBoolean('true', 'val not bool')
.typeOf(value, name, [message]) assert.typeOf('e2e', 'string', 'val is string')
.notTypeOf(value, name, [message]) assert.notTypeOf('e2e', 'number', 'val not number')

Chai-jQuery

การ assert ส่วนหน้า UI (DOM object) โดยจะใช้คำสั่งการ assert เหล่านี้ หลังจากเรียก command cy.get() หรือ cy.contains()

Cypress.config('baseUrl','https://cypress-testing-143fd.web.app/')

describe('Chai-jQuery', () => {
  it('expect DOM Element ', () => {
    cy.visit('/elements.html')
      .get('form.ui.form').should($el => {
        expect($el).to.have.attr('class', 'ui form')

        const $notDisabled = $el.find('#text')
        expect($notDisabled).to.have.prop('disabled', false)

        expect($el).to.have.css('background-color', 'rgb(255, 255, 200)')
        expect($el).to.have.data('random', '77547562649602259494')
        expect($el).to.have.class('ui')

        const $email = $el.find('[name="email"]')
        expect($email).to.have.id('email')

        const $html = $el.find('#i-love-test-html')
        expect($html).to.have.html('\n              <h1>I love testing</h1>\n            ')

        const $text = $el.find('#i-love-test-text')
        expect($text).to.have.text('\n              I love testing\n            ')
        expect($text).to.contain('I love testing')

        expect($email).to.have.value('[email protected]')

        expect($email).to.be.visible

        const $hidden = $el.find('[name="hidden"]')
        expect($hidden).to.be.hidden

        const $option0 = $el.find('#select option:eq(1)')
        expect($option0).not.to.be.selected

        const $options = $el.find('#select option')
        expect($options).to.have.lengthOf(5)
        
        cy.log('lengthOf------------')
        const $mazda = $el.find('[name="mazda-checked"]')
        expect($mazda).not.to.be.checked

        const $focus = $el.find('#text')
        //expect($focus).not.to.be.focused
        //expect($focus).to.have.focus

        expect($focus).to.be.enabled

        const $disabled = $el.find('[name="text-disabled"]')
        expect($disabled).to.be.disabled

        //expect($focus).not.to.be.empty
        //expect($nonexistent).not.to.exist
        //expect($emptyEl).to.match(':empty')
        //expect($el).to.contain('text')
        //expect($el).to.have.descendants('div')
      })


  })
})
https://cypress-testing-143fd.web.app/elements.html
Chainers Assertion
attr(name, [value]) expect($el).to.have.attr('foo', 'bar')
prop(name, [value]) expect($el).to.have.prop('disabled', false)
css(name, [value]) expect($el).to.have.css('background-color', 'rgb(0, 0, 0)')
data(name, [value]) expect($el).to.have.data('foo', 'bar')
class(className) expect($el).to.have.class('foo')
id(id) expect($el).to.have.id('foo')
html(html) expect($el).to.have.html('I love testing')
text(text) expect($el).to.have.text('I love testing')
value(value) expect($el).to.have.value('[email protected]')
visible expect($el).to.be.visible
hidden expect($el).to.be.hidden
selected expect($option).not.to.be.selected
checked expect($input).not.to.be.checked
focus[ed] expect($input).not.to.be.focused
expect($input).to.have.focus
enabled expect($input).to.be.enabled
disabled expect($input).to.be.disabled
empty expect($el).not.to.be.empty
exist expect($nonexistent).not.to.exist
match(selector) expect($emptyEl).to.match(':empty')
contain(text) expect($el).to.contain('text')
descendants(selector) expect($el).to.have.descendants('div')

Sinon-Chai

การ assert behavior ของ website event ภายใน ต่าง ๆ ทั้ง window object function (alert ,confirm ,prompt) หรือ custom js function ต่าง ๆ จะใช้ assert spy , stub ตัวอย่างที่สามารถ assert ได้ เช่น มีการ call spy/stub หรือไม่ call กี่ครั้ง จะสามารถเช็คได้ด้วย assert ชุดนี้

Cypress.config('baseUrl','https://cypress-testing-143fd.web.app/')
describe('Sinon-Chai', () => {
  it('use sinon expect spy&tub', () => {
    let stubFirst ,stubSecound ,stubThird,stubFour,stubFive;
    let countStubFiveClick = 0;
    cy.visit('/window.html',{
      onLoad(win){
        stubFirst = cy.stub(win,'mySpyCalledFirst',()=>{})
        stubSecound = cy.stub(win,'mySpyCalledSecound',()=>{})
        stubThird = cy.stub(win,'mySpyCalledThird',()=>{})
        stubFour = cy.stub(win,'mySpyCalledFour',()=>{
          return 'mySpyCalledFour click 999'
        })
        stubFive = cy.stub(win,'mySpyCalledFive',()=> countStubFiveClick+= 2)
      }
    })
    .get('#btnFirst').click()
    .get('#btnSecound').click()
    .get('#btnSecound').click()
    .get('#btnThird').click()
    .get('#btnThird').click()
    .get('#btnThird').click()
    .get('#btnFour').click()
    .get('#btnFive').click().get('#btnFive').click()
    //.wait(2000)
    .then(()=>{
      expect(stubFirst).to.be.called
      expect(stubFirst).to.have.callCount(1)
      expect(stubFirst).to.be.calledOnce
      expect(stubSecound).to.be.calledTwice
      expect(stubThird).to.be.calledThrice
      expect(stubFirst).to.be.calledBefore(stubSecound)
      expect(stubThird).to.be.calledAfter(stubSecound)
      expect(stubFour).to.have.returned('mySpyCalledFour click 999')
      expect(stubFive).to.have.returned(countStubFiveClick)
      expect(stubFirst).to.have.always.returned(undefined)
      /*expect(stubFirst).to.be.calledWithNew
      expect(stubFirst).to.always.be.calledWithNew
      expect(spy).to.be.calledOn(context)
      expect(spy).to.always.be.calledOn(context)
      expect(spy).to.be.calledWith(...args)
      expect(spy).to.always.be.calledWith(...args)
      expect(spy).to.be.calledWithExactly(...args)
      expect(spy).to.always.be.calledWithExactly(...args)
      expect(spy).to.be.calledWithMatch(...args)
      expect(spy).to.always.be.calledWithMatch(...args)
      
      expect(spy).to.have.thrown(errorObjOrErrorTypeStringOrNothing)
      expect(spy).to.have.always.thrown(errorObjOrErrorTypeStringOrNothing)*/
    })
    

  })
})
Sinon.JS property/method Assertion
called expect(spy).to.be.called
callCount expect(spy).to.have.callCount(n)
calledOnce expect(spy).to.be.calledOnce
calledTwice expect(spy).to.be.calledTwice
calledThrice expect(spy).to.be.calledThrice
calledBefore expect(spy1).to.be.calledBefore(spy2)
calledAfter expect(spy1).to.be.calledAfter(spy2)
calledWithNew expect(spy).to.be.calledWithNew
alwaysCalledWithNew expect(spy).to.always.be.calledWithNew
calledOn expect(spy).to.be.calledOn(context)
alwaysCalledOn expect(spy).to.always.be.calledOn(context)
calledWith expect(spy).to.be.calledWith(...args)
alwaysCalledWith expect(spy).to.always.be.calledWith(...args)
calledWithExactly expect(spy).to.be.calledWithExactly(...args)
alwaysCalledWithExactly expect(spy).to.always.be.calledWithExactly(...args)
calledWithMatch expect(spy).to.be.calledWithMatch(...args)
alwaysCalledWithMatch expect(spy).to.always.be.calledWithMatch(...args)
returned expect(spy).to.have.returned(returnVal)
alwaysReturned expect(spy).to.have.always.returned(returnVal)
threw expect(spy).to.have.thrown(errorObjOrErrorTypeStringOrNothing)
alwaysThrew expect(spy).to.have.always.thrown(errorObjOrErrorTypeStringOrNothing)

คำสั่ง asserts เหล่านี้ต้องใช้การใช้งาน บ่อย ๆ ถึงจะจำการเรียกใช้งานได้ บทความ ถัดไปจะมาเล่าถึงการใช้งาน spy / stub คอยติดตามด้วยนะครับ

Cypress แนะนำโครงสร้างของไฟล์ testscript (*.spec.js) EP3

ก่อนที่จะเริ่มเขียน testscript (ของเรียกไฟล์โค๊ดของการ test ว่า “testscript” ) โดยที่ชื่อไฟล์โดยปกติจะสร้างกำหนดเป็นชื่อ {name}.spec.js ที่จะถูกเก็บไว้ที่ folder cypress/integration/* โดยเมื่อกำหนดชื่อ ตาม pattern นี้ cypress จะรู้จัก testscript นี้โดยอัตโนมัติ

satang pro referral

โครงสร้างไฟล์ testscript ใช้ Mocha.js library มาช่วยจัดการ

describe ,it ,before ,after ,beforeEach ,afterEach ,context ต่าง ๆ เหล่านี้เป็นแท็กคำสั่ง ของ MochaJS Testing Library ที่ได้รับความนิยมเป็นอย่างมากกับ NodeJs Project เพราะด้วยถูกพัฒนาด้วย Javascript จึงเขียน test ได้ทั้ง frontend (cypress framework) หรือแม้แต่ backend (nodejs framework project ต่างๆ) ขออธิบายหน้าที่การทำงานของแต่ละแท็กสคริปคำสั่งแต่ละ คำสั่งทำงานอย่างไรบ้าง

before (ก่อนหน้า) เป็น lifecycle ของ testscript จะทำเมื่่อเริ่มรัน describe เพียงครั้งแรกเท่านั้น
beforeEach (ก่อนหน้าแต่ละ) คล้ายกับ before แต่จะถูกทำทุกการรันแท็ก it จะทำเมื่อเริ่มแรก ของ it
describe (อธิบาย) เมื่อเริ่มเขียน testscript ทำหน้าที่อธิบายการทำงานของกลุ่ม testcase นี้ มักจะใช้งานร่วมกับแท็ก it ซึ่งจะเป็นซับ testcase ของกลุ่ม test นี้
context (บริบท) หน้าที่คล้ายกับ describe เลือกใช้งานตามความถนัด
it (มัน ,เคส) แท็กเขียนโค๊ด test ที่นี่จะเริ่มเขียน API commands ต่าง ๆ ที่นี่
afterEach (หลังจากนั้นแต่ละ) ทำหลังจากเรียก it ทุกครั้ง จะทำ test บางอย่างหลังจาก it (เคส) ทำงานครบแล้ว
after (หลังจาก) ทำท้ายสุดหลังจาก test มาถึงท้าย กลุ่มเทส (describe)

describe('กลุ่มเทสเคส', () => {
  before(() => {
    cy.log('mocha [before]')
  });
  beforeEach(() => {
    cy.log('mocha [beforeEach]')
  });
  context('หมวดย่อยเทสเคส', () => {
    it('เทสย่อย', () => {
      cy.log('mocha [context:it]')
    });
  });
  it('เทสเคส', () => {
    cy.log('mocha [it]')
  });
  afterEach(() => {
    cy.log('mocha [afterEach]')
  });
  after(() => {
    cy.log('mocha [after]')
  });

});

*.skip (* context ,describe ,it)
การสั่งให้ cypress ข้ามการทำงาน block testcase นี้ไปไม่ต้องรัน ตัวอย่างเช่น

describe('กลุ่มเทสเคส', () => {
  before(() => {
    cy.log('mocha [before]')
  });
  beforeEach(() => {
    cy.log('mocha [beforeEach]')
  });
  context.skip('หมวดย่อยเทสเคส 1', () => {
    it('เทสย่อย', () => {
      cy.log('mocha [context:it]')
    });
  });
  context('หมวดย่อยเทสเคส 2', () => {
    it('เทสย่อย', () => {
      cy.log('mocha [context:it]')
    });
  });
  it('เทสเคส', () => {
    cy.log('mocha [it]')
  });
  afterEach(() => {
    cy.log('mocha [afterEach]')
  });
  after(() => {
    cy.log('mocha [after]')
  });

});

*.only (* context ,describe ,it)
การที่กำหนดให้ cypress สนใจเพียง testscript ที่กำลังทำงานอยู่เพียงเท่านั้น ถึงจะมีโค๊ด testscript อื่นอยู่ในไฟล์ก็จะไม่แสดงในหน้าการแสดงผล (cypress ui)

describe('กลุ่มเทสเคส', () => {
  before(() => {
    cy.log('mocha [before]')
  });
  beforeEach(() => {
    cy.log('mocha [beforeEach]')
  });
  context.skip('หมวดย่อยเทสเคส 1', () => {
    it('เทสย่อย', () => {
      cy.log('mocha [context:it]')
    });
  });
  context.only('หมวดย่อยเทสเคส 2', () => {
    it('เทสย่อย', () => {
      cy.log('mocha [context:it]')
    });
  });
  it('เทสเคส', () => {
    cy.log('mocha [it]')
  });
  afterEach(() => {
    cy.log('mocha [afterEach]')
  });
  after(() => {
    cy.log('mocha [after]')
  });

});

การทำงานแบบวนลูป

การใส่ loop เพื่อรันค่าจากตัวแปรค่าข้อมูลของเราทำได้ตามปริมาณข้อมูล input ถือว่าเป็นส่วนช่วยให้ reuse การใช้งานโค๊ดได้มีประสิทธิภาพยิ่งขึ้น เหมือนการเขียนโปรแกรมทั่วไปโดยใส่ loop ครอบ describe ,context ,it นั้น ๆ ตามตัวอย่างนี้

const mock = ['A', 'B', 'C']
describe('กลุ่มเทสเคส', () => {
  before(() => {
    cy.log('mocha [before]')
  })
  beforeEach(() => {
    cy.log('mocha [beforeEach]')
  })
  mock.forEach(levelFirst => {
    context.skip('หมวดย่อยเทสเคส mock level:: ' + levelFirst, () => {
      mock.forEach(levelSecond => {
        it('เทสย่อย level:: '+levelSecond, () => {
          cy.log('mocha [context:it]')
        })
      })
    })
  })
  context('หมวดย่อยเทสเคส 2', () => {
    it('เทสย่อย', () => {
      cy.log('mocha [context:it]')
    })
  })
  it('เทสเคส', () => {
    cy.log('mocha [it]')
  })
  afterEach(() => {
    cy.log('mocha [afterEach]')
  })
  after(() => {
    cy.log('mocha [after]')
  })

})

จบแล้วสำหรับเนื้อหาบทความนี้ บทความหน้าจะมาต่อกันกับเรื่องการ should/expect case ต่าง ๆ จะสามารถทำได้อย่างไรบ้าง และตัวอย่างโค๊ดจะเขียนอย่างไร คอยติดตาม เป็นกำลังใจด้วยนะครับ

Cypress แนะนำอธิบาย 20 API Commands ที่ใช้งานบ่อย EP2

visit
ใช้สำหรับเข้า website test ถือว่าเป็น command ที่ต้องรู้ลำดับแรก ๆ ตัวอย่างการใช้งาน cy.visit(‘https://google.com‘) มี function callback ที่น่าสนใจ คือ onBeforeLoad , onLoad สามารถใส่ behavior function การทำงานในส่วนนี้ได้

get
ระบุ selector element ของหน้าจอ เพื่อจะเป็น target ในการ action command ถัดไปกรณี ต้องการให้ระบุ element เพื่อทำงานอะไรบ้างอย่าง เช่น set value ในช่อง text ,set text บน label เป็นต้น ตัวอย่างการเรียกใช้งาน cy.get(‘{selector}’) โดย selector คือ input ,.class ,#id ,[name=””] เป็นต้น

type
ใช้สำหรับ set value ให้กับ input tag ต่าง ๆ เช่น input [text ,date ,phone ,password ,number ,…] รวมถึง textarea ได้อีกด้วย ตัวอย่างการใช้งาน cy.get(‘{selector}’).type(‘commade type’)

satang pro referral

clear
ใช้ clear value ในช่อง input ตัวอย่างการใช้งาน cy.clear()

as
ทำหน้าที่เป็น command กำหนด alias เป็นการกำหนด reference ให้กับ command ที่กำลังสนใจ หรือ ใช้งานซ้ำ ๆ สามารถ reuse เรียกใช้งานซ้ำได้ ตัวอย่างการใช้งาน cy.(‘{selector}’).as(‘mySelector’) อยากต้องการระบุอ้างอิงถึงก็เพียงแค่ cy.get(‘@mySelector’) ถึงชื่อ alias ก็สามารถใช้ทำงานได้เหมือนการเรียก cy.get(‘{selector}’) นั้นอีกครั้ง

check
ใช้กับ input [radio ,checkbox] กำหนด state ให้กับ element เหล่านี้เป็นการ checked ตัวอย่างการใช้งาน cy.get(‘{selector}’).check() โดยที่ {selector} นั้นต้องเป็น radio ,checkbox command ถึงจะทำงานได้ถูกต้อง

click
ชื่อก็บอกอยู่แล้ว กำหนด event click ให้กับ element ที่กำลังสนใจ เกือบแทบทุก element สามารถเรียก command นี้ได้เกือบหมด (ถ้าไม่ถูก Disabled) ตัวอย่างการใช้งานจะใช้งานร่วมกับ command get คือ cy.get(‘{selector}’).click()

contains
สำหรับเทียบค่าข้อความที่คาดหวังว่าจะมีใน element ที่เรากำลังสนใจ (คล้ายกับการ LIKE ‘%%’) ตัวอย่างการใช้งาน cy.get(‘{selector}’).contains(‘ข้อความ’) ถ้าหา element ที่มีข้อความไม่พบจะตก fail case เลย

debug
กำหนดการ pause ให้การรัน testcase หยุดช่ำขณะเพื่อดูการทำงานของโปรแกรม สามารถกด continues ต่อไปได้ เมื่อต้องการให้โปรแกรมทำงานต่อไป

each
การ LOOP elements ที่ได้จากการ get(‘{selector}’) แล้วต้องการที่จะทำงานอะไรบ้างอย่างกับ element ที่ selector ได้ ก็จะเรียก command each นี้ โดย command จะมี callback function ให้เขียนคำสั่งอื่นได้ ๆ ตัวอย่างการใช้งาน cy.get(‘{selector}’).each(()=> {/* todo something */ })

eq
ตัวย่อของ equal ที่แปลว่า เท่ากัน case ที่ใช้งานบ่อย คือการเทียบหาลำดับของ elements ที่พบได้มากกว่า 1 element ตัวอย่างการใช้งาน cy.get(‘{selector}’).eq(0) โดย 0 คือ index ของ elements ที่เจอ

find
ต้องการค้นหา element เป้าหมายเพียง element เดียว หลังจากที่ selector ได้ elements มากกว่า 1 หรือจะระบุ selector ตั้งแต่แรกเริ่ม get เลยก็ได้ ตัวอย่างการใช้งาน cy.get(‘{selector}’).find(‘.class-unique’) command จะ return element มาเพียง 1 element เท่านั้น

fixture
command เรียก static resource date ต่าง ๆ ที่ถูกจัดเก็บไว้ที่ path cypress/fixture/** โดยมีประเภทไฟล์ที่สามารถเรียกไฟล์ได้หลายนามสกุลด้วยกัน เช่น .json ,.csv ,.txt ,.png ,.pjg เป็นต้น ตัวอย่างการเรียกใช้งาน cy.fixture(‘{file-name}’).then(response => {/* doto something */ })

log
แสดง variable หรือ text ออกทางหน้าจอ run ui คล้ายกับ console.log(”) ของ javascript เพื่อแต่พื้นที่การแสดงอยู่คนละที่กัน

next
ถัดไป จะ selector element ตำแหน่งถัดไปของ element ก่อนเรียก command นี้ โดยสังเกตุจะเป็น element level เดียวกับ element ที่เรียก command นี้ ตัวอย่างการใช้งาน cy.get(‘{selector}’).next() จะได้ element ถัดไปทันที

prev
ก่อนหน้า เมื่อมี element ถัดไป (next command) ก็ต้องมีการหา element ก่อนหน้า ดังนั้น command นี้จะทำหน้าที่หา element ก่อนหน้าที่จะเรียก command นี้

not
เป็นนิเสธน์ ใช้กรอง element ไม่สนใจ elements ที่ถูกเรียกภายใน command นี้ ตัวอย่างการเรียกใช้งาน cy.get(‘{selectors}’).not(‘{.not-use-element}’)

request
ใช้เรียก api หรือ url ที่ต้องการจะเข้าถึงแบบ background run (คล้าย ๆ การเรียก AJAX ของ jQuery เหมือนกัน ตัวอย่างการเรียกใช้งาน cy.request(‘{url-api}’) หรือจะเพิ่มการเรียก method อื่น ๆ ด้วย cy.request({method : ‘POST’ , url : ‘{url-api}’})

should
ถือว่าเป็นพระเอกของเรื่องการทำ automated testing เลยก็ว่าได้เพราะถ้าการเขียน testscript ไม่มีการเขียนการคาดหวังผลลัพธ์ เพื่อที่จะใช้ตรวจสอบข้อมูลต่าง ๆ ละก็ไม่รู้ว่าการเขียน automated test จะมีประโยชน์อะไรเลย ตัวอย่างการใช้งานก็เรียกว่าทำได้หลากหลายมาก เช่น cy.get(‘{selector}’).should(‘have.text’,'{ข้อความ}’) หรือแม้แต่จะเรียกแบบ callback function ก็ทำได้โดย cy.get(‘{selector}’).should($selector => { /* todo something */ }) โดยการตรวจสอบในรุปแบบอื่น ๆ สามารถเข้าไปดูได้จากลิ้งนี้ command should

submit
ใส่ action Submit ให้กับ element FORM ตัวอย่างการเรียกใช้งาน cy.get(‘selector*’).submit() *คือ selector ของ FORM

wait
จะเรียกว่าเป็น command ที่เป็นตัวช่วยชีวิตกรณีการไม่แน่ใจว่าการทำงานจะเสร็จเมื่อไหร่ มักจะใส่ wait นี้เพื่อรอการทำงานก่อนหน้านี้ให้แน่ใจว่าทำงานเสร็จเรียบร้อย ตัวอย่างการใช้งาน cy.wait(4000) คือ ให้รอ 4 วินาที และจึงทำงานต่อไปได้

และนี้คือ API commands ของ Cypress ที่น่าจะเป็น commands ที่เรียกใช้งานได้บ่อย ที่สุด บทความต่อไปจะไปล้วงลึกการทำงานของ Spy/Stub ที่เป็นเรื่องที่ไม่รู้ไม่ได้ เลยจริง ๆ รอติดตามนะครับ

Cypress แนะนำโครงสร้างของ Project EP1

โครงสร้างไฟล์ Project

หลังจากที่ได้ อ่านบทความเรื่อง Cypress Automate Testing (E2E) 101 แล้ว มาต่อถึงบทความนี้ จะมาอธิบายโครงสร้างไฟล์ โฟรเดอร์ ต่าง ๆ ของ Project Cypress กัน

package.json
ไฟล์แรกที่ต้องมีการถูกแก้ไขเมื่อครั้งเริ่ม Setup Project จะมีการเพิ่มโค๊ดเป็นที่แรก ด้วย “cypress”: “^4.9.0” เพื่อติดตั้ง dependencies ของ Project ยังมี “scripts” : {} ที่ยังสามารถ กำหนดค่า script command ลัดของเราเองเพื่อที่จะทำให้เรียกใช้งานได้ง่ายขึ้น npm run {script}

cypress.json
ไฟล์สำคัญของ Project Cypress สำหรับจัดการค่า Config ของ Project สำหรับ config ค่าต่าง ๆ เช่น
– baseUrl กำหนด url ของ website ที่ใช้ทดสอบ
– screenshotsFolder ระบุตำแหน่งที่เก็บไฟล์ภาพหลังจากใช้ command cy.screenshot() ค่า default “cypress/screenshots”
– browsers กำหนด browser ต่าง ๆ สำหรับใช้ทดสอบ Chrome , Firefox ,Edge ,Electron
– อื่น ๆ

satang pro referral

cypress/fixtures
เก็บไฟล์ static เช่น json ,csv ,txt ,image ฯลฯ โดยต้องการเรียกใช้งานเพียงเรียก command cy.fixture(“{filename}”).then(()=>{ /* todo */ }) ถือว่าสะดวกมาก ๆ

cypress/integration
เก็บ testscript ไฟล์ สามารถสร้างโฟรเดอร์ แบ่งย่อย testscript ได้ จะเก็บไฟล์ {filename}.spec.js

cypress/plugins
ไฟล์เพิ่ม plugins รวมถึง cy.task({taskname},()=>{}) ที่อยากจะจัดการ จะมาเขียนเก็บไว้ที่ไฟล์นี้

cypress/supports
โฟรเดอร์ที่มีไฟล์ตั้งต้น 2 ไฟล์ คือ index.js ,commands.js
– commands.js เก็บ Custom Commands ของเราที่อยากจะแชร์แบ่งให้ testscript อื่น ๆ เข้าถึงและเรียกใช้งานได้
– index.js เป็นไฟล์ที่สำหรับ import ไฟล์ที่อยากต้องการเรียกใช้งาน จะมาเพิ่มในไฟล์นี้

บทความหน้าจะมาพูดถึงการเรียก API commands ของ Cypress ซึ่งมีให้ใช้งานเยอะมาก ถ้าเราเรียกใช้งานให้เหมาะสมกับสถานะการ

Cypress Automate Testing (E2E) 101

ความเดิมครั้งแต่เก่าก่อน

การทำ automate testing เป็นเรื่องที่ดูเหมือนจะไม่ค่อยได้รับความสนใจสักเท่าไหร่ในช่วงแรก ๆ ของการ build project เพราะด้วยเวลาที่มีอย่างจำกัดและประกอบกับ requirement ที่ยังไม่แน่นอนทำให้ feature ของ application ปรับเปลี่ยนอยู่ตลอดเวลา ทำให้การเขียนเทสเพิ่มความยุ่งยากในการเขียน test ยุ่งยากเข้าเพิ่มไปอีก

เป็นงานที่น่าเบื่อ ควรต้องมองหาตัวช่วยได้แล้ว

เมื่อ application ของเรา launch ออกไปได้สักระยะนึง เมื่อจำเป็นจะต้องเพิ่มเติม feature ของ application ขึ้นมาใหม่ในส่วนใดส่วนหนึ่งของระบบ ทำให้ต้องมีการ regression test ระบบ ( การ test เพื่อตรวจสอบ side effect ที่เกิดจากการ Change ) ถือเป็นงานที่ตจำเป็นต้องทำ automate test แล้วแหละ เพราะคุณจะรู้สึกเบื่อกับการต้องมานั่ง manual test แบบเดิมซ้ำ ๆ โดยมีความคิดอยู่ในหัว “งานแบบนี้ มันเพิ่มทักษะอะไรให้กับเรา มันคืองาน routine ที่น่าเบื่อ ดี ๆ นี่เอง”

https://medium.com/@jatinvsharma/how-can-manual-testers-survive-automation-ceaa9642fdf0

เมื่อจะเริ่มทำ automate test ต้องหาเครื่องมือที่ตอบโจทย์ของเราได้ใน ทุก ๆ สถานะการ

แต่ก่อนที่จะมีการกำเนิด cypress ที่กำลังจะกล่าวถึงในบทความนี้ ก็มี automate test tools ที่เกิดขึ้นมาแล้วอย่างมากมาย โดยมี robot framework (selenium framework) ซึ่งผม ก็เคยใช้มาก่อน และก็พบกับปัญหา ต่าง ๆ ขอยกตัวอย่างมาสักหน่อยก็แล้วกัน
1. Test Library dependencies management ทำได้ไม่ค่อยดี ไม่มี dependency management tool ช่วย ทำได้เต็มที่คือสร้าง requirements.txt เก็บ list ของ library เอาไว้ทำ pip install เอา (แต่ก็ไม่ 100% อยู่ดี)
2. Test Library บางตัวเขียนด้วยภาษา C (C Python) ทำให้การเอาไปรันบน ​OS ที่แตกต่างกันมักจะมีปัญหา หรือ install ได้ยากมากๆ เช่น cx_Oracle, SSHLibrary
3. การ install ค่อนข้างยาก เพราะมี dependency สูง เช่น python version มี support ทั้ง 2.x และ 3.x แต่ test library หลายตัวยังเขียน support บน 2.x อยู่เลย
เหล่านี้เป็นส่วนหนึ่งในปัญหาใหญ่ ๆ ที่คนเคยเขียน robot framework ต้องเคยเจอ ทำให้ความตั้งใจในการเขียน automate test ล้มเลิกไปเพราะ ความยุ่งยากและข้อจำกัดต่าง ๆ เหล่านี้

การมาของ Cypress Automate Testing (E2E Testing) ถือกำเนิดเกิดขึ้นมาเพื่อช่วย solve ปัญหาการประบวนการของ automate testing tools ที่ถูกสร้างขึ้นมาก่อนหน้าที่ cypress จะถือดำเนิดเกิดขึ้น cypress ถือเป็นการนำข้อเสีย (ข้อจำกัด) ของเหล่า automate tools ทั้งหลายมาถูกแก้ไข และก็มีข้อดีที่ถูกพัฒนาด้วย javascript จึงถือเป็น tools ที่น่าจับตามองในปี 2020 นี้

Cypress คือ

Cypress เป็น Test Framework ที่ถูก Design ขึ้นมาในปี 2014 (ถูกสร้างขึ้นหลัง Selenium 10 ปีเอง) โดยตัว Cypress จะถูกรันขึ้นมาอยู่ใน Run Loop Process เดียวกับ Web Application ของเรา ซึ่งเบื้องหลังการทำงานของมันคือ NodeJS Server

ติดตั้ง Cypress และ Hello world

Cypress ถูกพัฒนาบน NodeJS ฉะนั้นย่อมต้องมีที่ NPM (Node package manager) ไปติดตั้งกันเลย

mkdir cypress101
cd cypress101
npm init -y
npm install [email protected] --save--dev

จบการติดตั้ง ต่อไปมาถึงขั้นตอน สั่ง cypress ให้เริ่มทำงาน

./node_modules/cypress/bin/cypress open

แก้ไข เพิ่ม npm run script เพิ่ม “cypress:open” : “cypress open”

{
  "name": "cypress101",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "cypress:open" : "cypress open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "cypress": "^4.2.0"
  }
}

รันคำสั่งถัดไป เพื่อ start cypress

npm run cypress:open

ทำการสร้างไฟล์ test spec ของเราที่ cypress\integration\hello\hello.spec.js

describe('first testcase', () => {
  it('hello world on google searching', () => {
      cy.visit('https://www.google.co.th/')
      .get('[aria-label="ค้นหา"]').type('hello world')      
      .get('[action="/search"]').submit()
  });
});

จากนั้นกลับมาที่หน้าจอ

click ที่ไฟล์ hello.spec.js

บทความนี้ก็ขอจบไว้เท่านี้ก่อน บทความหน้าจะเอาเรื่องการเขียน cypress step ถัดไป ขอบคุณที่ติดตามครับ

เขียน Smart Contract บน Blockchain ของ Ethereum ด้วยภาษา Solidity พร้อมตัวอย่างระบบ Vote

Blog นี้เงียบเหงามานานมากเพราะที่หายไปเน้นไปเขียน Blog ที่ medium.com เป็นหลักแต่เลือกที่จะเขียน content นี้ที่นี่เพราะอยากให้มีอะไรอัพเดทที่ Blog นี้บ้าง

satang pro referral

ที่มาสำหรับบทความนี้

codemania 1001 ที่เพิ่งผ่านพ้นไป ได้เลือกที่จะเข้าร่วม session Blockchain Dev เป็น workshop ในช่วงเวลา 2 ชั่วโมงที่คุ้มค่ามาก ถึงแม้จะไม่ได้นำ Laptop ไปแต่ก็ไม่พลาดที่จะจดเนื้อหาแล้วเอามาเขียนในบทความนี้

อธิบายหลักการที่ความเข้าใจส่วนตัวหลังจากได้เข้าร่วม workshop Blockchain Dev ในเวลา 2 ชม.

  1. Ehtereum จะมีเครือข่ายโดยเฉพาะที่กระจายอยู่แต่ละที่โดยจะใช้คำเรียกว่า Node ซึ่งมีอยู่ทั่วโลก
  2. การทำ Transaction บน Ethereum จำเป็นต้องมีการจ่ายค่าธรรมเนียมที่เรียกว่าค่า “Gas”
  3. การที่จะทำ Transaction ได้นั้นจำเป็นต้องมีกระเป๋า “Wallet” เพื่อใช้เอาไว้เก็บ Coin โดยจะมีสิ่งที่สำคัญอยู่ 2 อย่างคือ public key ,private key
    1. private key เปรียบได้กับรหัสบัตรประชาชนของเราซึ่งจะไม่มีทางซ้ำกับคนอื่นได้แน่นอน
    2. public key (address) เปรียบได้กับบัญชีธนาคารซึ่งการที่จะสร้างบัญชีใหม่ได้จำเป็นต้องใช้ private key เป็นส่วนประกอบในการสร้าง เปิดเผยได้และใช้รหัสสำหรับอ้างถึงบัญชีรับ

ทำความรู้จักเครื่องมือต่าง ๆ ในสำหรับการเริ่มพัฒน

  • Remix Ethereum IDE Online เป็นเครื่องมือใช้เขียน Code ,Compile ,Deploy
  • MetaMask (Browser Extension) ใช้เป็น Program แทนกระเป๋าเงิน (Chrome ,Firefox ,Opera)
  • Web Server for Chrome เอาไว้ใช้เป็น web server run static web (เป็นตัวเลือก ถ้ามี tool อื่น run static web ได้ก็แทนได้เหมือนกัน)

เริ่ม workshop กัน

    1. ติดตั้ง MetaMask Chrome Extension และ Register ให้เรียบร้อย ขั้นตอนตรงนี้ขอข้ามไปนะ 
    2. เมื่อได้ Create account ที่ Metamask เรียบร้อยแล้ว จะเริ่มขั้นตอนการเติม Coin เข้า Wallet ช่องทางการเติมเงิน (ETH coin) จะร้องขอได้ที่ Ethereum Node Test  เช่น Kovan ,Rinkeby ,Kulap (Ethereum สัญาชาติไทย) เป็นต้น
      1. ต้องเปลี่ยน Network ของ Ehtereum Node เป็นของ Rinkeby Test Network
      2. เข้าลิ้ง https://www.rinkeby.io/#faucet โดยทาง Node จะให้เราแชร์ public key (address) บน Social Network (Facebook,Twitter,…) แบบเปิด public post จากนั้น copy link ของ post นั้นมาใส่
        1. 3 Ethers / 8 hours
        2. 7.5 Ethers / 1 day
        3. 18.75 Ethers / 3 days
      3. รอ.. สังเกตุ wallet เราก็จะมี token เพิ่มเข้ามา เราก็พร้อมจะไป step ถัดไป
    3. เริ่มเขียน code เข้าไปที่ลิ้ง https://remix.ethereum.org เลือก code ด้วย solidity
    4. เลือกเมนู File Explorers กดบวก สร้างไฟล์ ตั้งชื่อไฟล์ว่า “SimpleVoting.sol” และใส่ code ข้างล่างนี้
    5. กด Crtl + S เพื่อ save file
      1. Deploy ใส่ input ที่ใน code จะให้ใส่ค่าเป็น Array ให้ใส่ [“Google”,”Line”,”Facebook”] เพื่อใช้เป็น Choice Vote จากนั้นกด Deploy
        1. VoteForCan… สำหรับ Test Vote
        2. candidateC…
        3. candidateList get Choice Vote โดยส่ง index จะได้ item ใน List
        4. totalVotesFor ดูสถานะ Total Vote รวม
      2. ทดสอบ Vote
        1. ใส่ “Google” กด Vote ไป 2 ครั้ง
        2. ตรวจสอบสถานะของ “Google” จาก totalVotesFor จะได้ 2

ทั้งหมดนี้ของบทความต้องขอขอบคุณตัวอย่าง code workshop จาก Kulapio Githup

SpringBoot2 สร้าง CRUD RESTful API พร้อม UnitTest แบบรวดเร็ว

Web Service แบบ RESTful protocol ได้รับความนิยมเป็นอย่างมากในปัจจุบัน หลายภาษามี web framework ของตัวอย่าง Java Spring Framework ก็เช่นกัน

SpringBoot เป็น Framework ที่ได้รับความนิยมมากกับ Java เพราะด้วยความง่ายที่เป็นสิ่งที่ถูกพัฒนาเพื่อแก้ปัญหาในการ Setup Project ที่ค่อนข้างยุ่งยากและซับซ้อนในการ Build RESTful API ขึ้นมาใช้งาน Springboot จึงได้รับความนิยมและได้เปรียบเรื่องความรวดเร็วในการ Setup Project

สิ่งที่จะได้รับเมื่ออ่านบทความบทนี้จบ

  • Create Springboot Project
  • CRUD Data with H2 Database
  • Use GET,POST,PUT,DELETE Methods
  • Content-Type : “application/json”
  • MockMvc
  • CrudRepository Interface crud



เริ่มกันเลย

  1. init project https://start.spring.io/
    1. 1
  2. เพิ่ม h2database ในไฟล์ pom.xml
    1. <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
      
        <groupId>com.poolsawat</groupId>
        <artifactId>MediumTestRestful</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
      
        <name>MediumTestRestful</name>
        <url>http://maven.apache.org</url>
      
        <properties>
          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          <java.version>1.8</java.version>
        </properties>
      
        <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.0.5.RELEASE</version>
        </parent>
      
      
        <dependencies>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
          </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
          </dependency>
          <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
          </dependency>
        </dependencies>
      
      
        <profiles>
          <profile>
            <id>DEV</id>
            <build>
              <plugins>
                <plugin>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
              </plugins>
            </build>
          </profile>
          <profile>
            <id>TEST</id>
            <build>
              <plugins>
                <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-surefire-plugin</artifactId>
                  <version>2.15</version><!--$NO-MVN-MAN-VER$ -->
                </plugin>
              </plugins>
            </build>
          </profile>
      
        </profiles>
      
      </project>
      
  3. สร้าง Project Structure ตามนี้
    1. ไฟล์ CrudController.java
      1. package com.poolsawat.medium.testrestful.controller;
        
        import java.util.Optional;
        
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Controller;
        import org.springframework.web.bind.annotation.DeleteMapping;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.PathVariable;
        import org.springframework.web.bind.annotation.PostMapping;
        import org.springframework.web.bind.annotation.PutMapping;
        import org.springframework.web.bind.annotation.RequestBody;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.ResponseBody;
        
        import com.poolsawat.medium.testrestful.entity.Blog;
        import com.poolsawat.medium.testrestful.repository.BlogRepository;
        
        @Controller
        public class CrudController {
          
          @Autowired
          private BlogRepository blogRepository;
          
          
          @RequestMapping("/")
            public @ResponseBody String greeting() {
                return "Hello World";
            }
          
          @GetMapping("/get")
          public @ResponseBody Iterable<Blog> getBlogs(){
            return this.blogRepository.findAll();
          }
          
          @GetMapping("/get/id/{id}")
          public @ResponseBody Optional<Blog> getBlog(@PathVariable(name="id") Long id){
            return this.blogRepository.findById(id);
          }
          
          @PostMapping("/save")
          public @ResponseBody Blog saveBlog(@RequestBody Blog blog){
            return this.blogRepository.save(blog);
          }
          
          @PutMapping("/update")
          public @ResponseBody Blog updateBlog(@RequestBody Blog blog){		
            return this.blogRepository.save(blog);
          }
          
          @DeleteMapping("/id/{id}")
          public @ResponseBody Long deleteBlog(@PathVariable(name="id") Long id) {
            this.blogRepository.deleteById(id);
            return id;
          }
          
        }
        



    2. ไฟล์ Blog.java
      1. package com.poolsawat.medium.testrestful.entity;
        
        import java.io.Serializable;
        
        import javax.persistence.Entity;
        import javax.persistence.GeneratedValue;
        import javax.persistence.GenerationType;
        import javax.persistence.Id;
        
        @Entity
        public class Blog implements Serializable{
          /**
           * 
           */
          private static final long serialVersionUID = 6833355522200232153L;
        
        
          @Id
          @GeneratedValue(strategy = GenerationType.AUTO)
          private Long id;
            
          private String title;
          
          private String content;
          
          private String author;
          
        
          public Blog() {
            super();
            // TODO Auto-generated constructor stub
          }
        
          public Blog(Long id, String title, String content, String author) {
            super();
            this.id = id;
            this.title = title;
            this.content = content;
            this.author = author;
          }
        
          public Long getId() {
            return id;
          }
        
          public void setId(Long id) {
            this.id = id;
          }
        
          public String getTitle() {
            return title;
          }
        
          public void setTitle(String title) {
            this.title = title;
          }
        
          public String getContent() {
            return content;
          }
        
          public void setContent(String content) {
            this.content = content;
          }
        
          public String getAuthor() {
            return author;
          }
        
          public void setAuthor(String author) {
            this.author = author;
          }
        
          @Override
          public String toString() {
            return "Blog [id=" + id + ", title=" + title + ", content=" + content + ", author=" + author + "]";
          }
          
          
        }
        
    3. ไฟล์ BlogRepository.java
      1. package com.poolsawat.medium.testrestful.repository;
        
        import java.util.List;
        
        import org.springframework.data.repository.CrudRepository;
        import org.springframework.stereotype.Repository;
        
        import com.poolsawat.medium.testrestful.entity.Blog;
        
        @Repository
        public interface BlogRepository extends CrudRepository<Blog, Long> {	
          List<Blog> findByTitle(String title) throws Exception;
          List<Blog> findByAuthor(String author) throws Exception;
        }
        
    4. ไฟล์ Application.java
      1. package com.poolsawat.medium.testrestful;
        
        import org.springframework.boot.CommandLineRunner;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.context.annotation.Bean;
        
        import com.poolsawat.medium.testrestful.entity.Blog;
        import com.poolsawat.medium.testrestful.repository.BlogRepository;
        
        @SpringBootApplication
        public class Application {
        
          public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
          }
          
          @Bean
          public CommandLineRunner demo(BlogRepository blogRepository) {
            return (args) -> {				
              blogRepository.save(new Blog(Long.valueOf("1"), "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.2) “Nuxt Directory Structure”", 
                  "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.2) “Nuxt Directory Structure”","poolsawat"));
              blogRepository.save(new Blog(Long.valueOf("2"), "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.1) “Setup Nuxt Project”", 
                  "SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.1) “Setup Nuxt Project”","poolsawat"));
              blogRepository.save(new Blog(Long.valueOf("3"), "สร้าง Project JSF Primeface ด้วย Maven พร้อมกับสอนทำระบบ Template Layout", 
                  "สร้าง Project JSF Primeface ด้วย Maven พร้อมกับสอนทำระบบ Template Layout","poolsawat"));
            };
          }
        }
    5. ไฟล์ CrudControllerTest.java
      1. package com.poolsawat.medium.testrestful;
        
        
        import static org.hamcrest.CoreMatchers.equalTo;
        import static org.hamcrest.Matchers.hasSize;
        import static org.hamcrest.Matchers.nullValue;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
        import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
        import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
        import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
        import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
        
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.http.MediaType;
        import org.springframework.test.context.junit4.SpringRunner;
        import org.springframework.test.web.servlet.MockMvc;
        
        @RunWith(SpringRunner.class)
        @SpringBootTest
        @AutoConfigureMockMvc
        public class CrudControllerTest {
          
          @Autowired
            private MockMvc mockMvc;	
          
          @Test
          public void testShouldSaveBlog() throws Exception {
            String content = "{\"id\" : 4,\"title\" : \"TestRestful\"}";
            this.mockMvc.perform(
                post("/save")
                .content(content)
                .contentType(MediaType.APPLICATION_JSON)
                )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.title", equalTo("TestRestful")))
                .andExpect(jsonPath("$.content", nullValue()))
                .andExpect(jsonPath("$.author", nullValue()));
          }
          
          @Test
          public void testShouldGetReturnBlogs() throws Exception {
            this.mockMvc.perform(get("/get"))
            .andDo(print())
            .andExpect(status().isOk())        
                .andExpect(jsonPath("$", hasSize(2)));
          }
          
          @Test
          public void testShouldGetReturnBlogById() throws Exception {
            Integer id = 3;
            this.mockMvc.perform(get("/get/id/"+id))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id", equalTo(id)))
                .andExpect(jsonPath("$.title", equalTo("สร้าง Project JSF Primeface ด้วย Maven พร้อมกับสอนทำระบบ Template Layout")));
          }
          
          @Test
          public void testShouldDeleteBlogById() throws Exception {
            Integer id = 1;
            this.mockMvc.perform(delete("/id/"+id))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", equalTo(id)));
          }
          
          @Test
          public void testShouldUpdateBlog() throws Exception {		
            String content = "{\"id\" : 2,\"title\" : \"ShouldUpdateBlog\",\"author\" : \"poolsawat\"}";
            this.mockMvc.perform(
                put("/update")
                .content(content)
                .contentType(MediaType.APPLICATION_JSON)
                )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id", equalTo(2)))
            .andExpect(jsonPath("$.title", equalTo("ShouldUpdateBlog")))
            .andExpect(jsonPath("$.content", nullValue()))
            .andExpect(jsonPath("$.author", equalTo("poolsawat")));
          }
        }
        
  4. สั่ง mvn spring-boot:run
    1. [INFO] --- maven-compiler-plugin:3.7.0:testCompile (default-testCompile) @ MediumTestRestful ---
      [INFO] Nothing to compile - all classes are up to date
      [INFO]
      [INFO] <<< spring-boot-maven-plugin:2.0.5.RELEASE:run (default-cli) < test-compile @ MediumTestRestful <<<
      [INFO]
      [INFO]
      [INFO] --- spring-boot-maven-plugin:2.0.5.RELEASE:run (default-cli) @ MediumTestRestful ---
      
        .   ____          _            __ _ _
       /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
       \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
        '  |____| .__|_| |_|_| |_\__, | / / / /
       =========|_|==============|___/=/_/_/_/
       :: Spring Boot ::        (v2.0.5.RELEASE)
      ...
      ...
      2018-12-05 18:51:43.509  INFO 3096 --- [           main] c.p.medium.testrestful.Application       : Started Application in 10.0
      65 seconds (JVM running for 16.349)
  5. สั่ง mvn test (ไม่ต้อง spring-boot:run แล้ว) เพื่อ run unittest ทดสอบ api CRUD
    1. ...
      [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.749 s - in com.poolsawat.medium.testrestful.CrudContr
      ollerTest
      2018-12-05 19:02:23.789  INFO 5576 --- [       Thread-3] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework
      [email protected]: startup date [Wed Dec 05 19:02:12 ICT 2018]; root of context hierar
      chy
      2018-12-05 19:02:23.798  INFO 5576 --- [       Thread-3] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFa
      ctory for persistence unit 'default'
      2018-12-05 19:02:23.798  INFO 5576 --- [       Thread-3] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed
       drop of schema as part of SessionFactory shut-down'
      2018-12-05 19:02:23.805  INFO 5576 --- [       Thread-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown ini
      tiated...
      2018-12-05 19:02:23.811  INFO 5576 --- [       Thread-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown com
      pleted.
      [INFO] 
      [INFO] Results:
      [INFO] 
      [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
      [INFO] 
      [INFO] ------------------------------------------------------------------------
      [INFO] BUILD SUCCESS
      [INFO] ------------------------------------------------------------------------
      [INFO] Total time:  17.707 s
      [INFO] Finished at: 2018-12-05T19:02:24+07:00
      [INFO] ------------------------------------------------------------------------

  6. github source

SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.2) “Nuxt Directory Structure”

หลังจากที่ได้ทำการ create nuxt project เป็นที่เรียบร้อยแล้ว (ถ้ายังไม่รู้ว่า setup Nuxt อย่างไรเข้าไปติดตามได้ที่บทความนี้ ” SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.1) “Setup Nuxt Project” “ ) สังเกตุว่าภายใต้ project จะเจอ folders และ files ต่าง ๆ  แต่ละส่วนแบ่งการทำงานและมีหน้าที่ต่างกัน

  • assets
    • คือ folder เก็บไฟล์ resources อย่างเช่น image ,javascript ,stylesheet หรือเป็นไฟล์อื่น ๆ ที่ต้องการทำเป็น static resource
    • การเรียกใช้งาน <img src=“~/assets/image.png”>
    • <template>
        <img src="~/assets/image.png">
      </template>
  • components
    • คือ folder เก็บ File components (.vue)
  • layouts
    • ใช้เก็บไฟล์ layout ของ project ภายในจะมีไฟล์ default.vue เป็น template layout  สามารถกำหนด error page กรณีที่มี error status ที่ไม่ใช้ 200 และยังสามารถสร้าง custom layout ในแบบที่เราต้องการได้อีกด้วย
    • <template>
        <div>
          <nuxt/>
        </div>
      </template>
  • middleware
    • เก็บไฟล์ที่เป็น intercepter ของ project ตัวอย่างไฟล์ authentication กรณีทำระบบ login (auth.js) ก็จะสร้างเก็บไว้ที่ folder นี้
    • export default function (context) {
        context.userAgent = context.isServer ? context.req.headers['user-agent'] : navigator.userAgent
      }
  • pages
    • เก็บไฟล์ views (route mapping) ของ project
    •  
    • <template>
        <div class="container">
          <h1 v-if="error.statusCode === 404">Page not found</h1>
          <h1 v-else>An error occurred</h1>
          <nuxt-link to="/">Home page</nuxt-link>
        </div>
      </template>
      
      <script>
      export default {
        props: ['error'],
        layout: 'blog' // you can set a custom layout for the error page
      }
      </script>
  •  plugins
    • เก็บไฟล์ config plugin เพื่อเรียกใช้ plugin ร่วมกับ project เรา เช่น axios เป็นต้น
  • static
    • เก็บไฟล์ static content ที่สามารถเรียกเข้าถึงได้ผ่าน route url ของ project ได้
    • <!-- Static image from static directory -->
      <img src="/my-image.png"/>
      
      <!-- webpacked image from assets directory -->
      <img src="~/assets/my-image-2.png"/>
  • store
    • เก็บไฟล์ data store Vuex Store store เก็บ data ทำ passing data ภายใน project โดยจะมี main file index.js และเรียก import  modules ต่าง ๆ ได้
    • import Vuex from 'vuex'
      
      const createStore = () => {
        return new Vuex.Store({
          state: () => ({
            counter: 0
          }),
          mutations: {
            increment (state) {
              state.counter++
            }
          }
        })
      }
      
      export default createStore
  • nuxt.config.js
    • file config หลักของ Project ใช้ config เรื่อง build ,css ,js ,plugin ,dev ,env ,generate ,head ฯลฯ เป็นต้น
    • const pkg = require('./package')
      
      module.exports = {
        mode: 'universal',
      
        /*
        ** Headers of the page
        */
        head: {
          title: pkg.name,
          meta: [
            { charset: 'utf-8' },
            { name: 'viewport', content: 'width=device-width, initial-scale=1' },
            { hid: 'description', name: 'description', content: pkg.description }
          ],
          link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
        },
      
        /*
        ** Customize the progress-bar color
        */
        loading: { color: '#fff' },
      
        /*
        ** Global CSS
        */
        css: [],
      
        /*
        ** Plugins to load before mounting the App
        */
        plugins: [],
      
        /*
        ** Nuxt.js modules
        */
        modules: [
          // Doc: https://github.com/nuxt-community/axios-module#usage
          '@nuxtjs/axios',
          // Doc:https://github.com/nuxt-community/modules/tree/master/packages/bulma
          '@nuxtjs/bulma'
        ],
        /*
        ** Axios module configuration
        */
        axios: {
          // See https://github.com/nuxt-community/axios-module#options
        },
      
        /*
        ** Build configuration
        */
        build: {
          postcss: {
            preset: {
              features: {
                customProperties: false
              }
            }
          }
          /*
          ** You can extend webpack config here
          */
          // extend(config, ctx) {
          //   // Run ESLint on save
          //   if (ctx.isDev && ctx.isClient) {
          //     config.module.rules.push({
          //       enforce: 'pre',
          //       test: /\.(js|vue)$/,
          //       loader: 'eslint-loader',
          //       exclude: /(node_modules)/
          //     })
          //   }
          // }
        }
      }
      
  • package.json
    • lib dependencies ของ Nuxt Project
    • {
        "name": "poolsawat",
        "version": "1.0.0",
        "description": "My shining Nuxt.js project",
        "author": "pool13433",
        "private": true,
        "scripts": {
          "dev": "nuxt",
          "build": "nuxt build",
          "start": "nuxt start",
          "generate": "nuxt generate",
          "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
          "precommit": "npm run lint"
        },
        "dependencies": {
          "cross-env": "^5.2.0",
          "nuxt": "^2.0.0",
          "@nuxtjs/bulma": "^1.2.0",
          "@nuxtjs/axios": "^5.0.0"
        },
        "devDependencies": {
          "nodemon": "^1.11.0",
          "babel-eslint": "^8.2.1",
          "eslint": "^5.0.1",
          "eslint-loader": "^2.0.0",
          "eslint-plugin-vue": "^4.0.0",
          "eslint-config-prettier": "^3.1.0",
          "eslint-plugin-prettier": "2.6.2",
          "prettier": "1.14.3"
        }
      }

บทความนี้เป็นเพียงรายละเอียดคร่าว ๆ ของแต่ละส่วนของ Project Nuxt ศึกษาเพิ่มเติมได้จากลิ้งนี้ Nuxt Doc

SPA สร้าง Web Site Universal ด้วย Nuxt.js (EP.1) “Setup Nuxt Project”

พวกเรามาถึงยุคที่ต้องมีเว็บไซต์ที่เข้าใช้งานแล้วรู้สึกว่ารวดเร็วทันใจเพราะเมื่อมีอะไรมาขัดจังหวะคนใช้งานจะรู้สึกไม่อยากรอ หรือ อาจจะเลิกสนใจ website ของคุณไปเลย แล้วถ้าอยากจะสร้าง website ลักษณะที่ตอบโจทย์ที่กล่าวมาละ เครื่องมืออะไรจะตอบโจทย์นี้ได้
SPA หรือ single page application คือการที่มี application ที่มีการทำงานในหน้าเดียวกล่าวคือจะไม่มีการ refresh หน้า ถ้าในสมัยเก่าก่อนวิธีการที่นิยมที่สุดก็คงเป็นการใช้ AJAX ในการ รับ-ส่งค่าข้อมูลระหว่างกันโดยเมื่อมีข้อมูลหน้าเว็บเปลี่ยนก็สั่งให้ DOM เปลี่ยนแปลงโดยใช้งานร่วมกับ jQuery พระเอกตลอดการของเรานั้นเอง ฟังดูยากสำหรับเมื่อก่อน
SPA ก็มี framework ในการพัฒนา 3 ค่ายดัง ๆ อย่าง React(Facebook) ,Angular(Google), Vue(ทีมพัฒนาอิสระ) ซึ่ง 3 framework ก็ล้วนเป็น open source แล้วทั้งสิน ถ้าเมื่อจะเลือกเครื่องมืออะไรสักอย่างก็ต้องเข้าไปใช้งาน หรือต้องได้ลองเล่น และเมื่อเกิดปัญหา หรือรู้สึกว่าติดขัดก็จะเลิกและเปลี่ยนไปใช้งานตัวถัดไป และเมื่อถ้าได้ลองจะครบทั้ง 3 framework แล้วตัวเราเองก็จะเลือกได้เองว่าจะเลือก framework ตัวใด
Vue คือ framework ระดับแรก ๆ ที่นักพัฒนามือใหม่เลือกใช้งาน ด้วยเหตุผลเรื่องความง่ายในการติดตั้งไม่จำเป็นต้องติดตั้ง build tool ก็สามารถเริ่มต้นใช้งานได้
Nuxt.js เป็น framework ที่พัฒนาต่อยอดจาก VueJS เพื่อช่วยแก้ปัญหาสำหรับ website ที่ต้องการทำเรื่อง SEO(Search Engine Optimization) เพื่อในพบการค้นหาใน google ได้ง่ายยิ่งขึ้น
เริ่มต้นใช้งาน
เริ่มแรกติดตั้ง Nuxt.js

  1. ติดตั้ง npx (ไม่ขอกล่าวถึงสำหรับขั้นตอนการติดตั้งส่วนนี้) หรือจะใช้ yarn ก็ได้ตามความถนัด
  2. npx: create nuxt project
    npx create-nuxt-app <project-name>
  3. yarn: create nuxt project
    yarn create nuxt-app <project-name>
  4. ใส่ชื่อ project? Project name (poolsawat)
  5. ใส่คำอธิบาย? Project description (My shining Nuxt.js project)
  6. เลือก integrate ร่วมกับ framework อื่น เลือก None (Nuxt default server)
    > none
    express
    koa
    adonis
    hapi
    feathers
    micro
  7. เลือก ui framework
    > none
    bootstrap
    vuetify
    bulma
    tailwind
    element-ui
    buefy
  8. Single Page App เลือก mode website SEO เลือก Universal
    > Universal
    SPA
  9. website ที่มีการต่อเรียก api ก็ให้ติดตั้ง axios ด้วย
    no
    > yes
  10. เพิ่มการตรวจสอบ source code ด้วย ESLint
    no
    > yes
  11. เพิ่มให้มีการจัด format code หลัง save
    no
    > yes
  12. รอ…
  13. ทำตาม stepcd poolsawat
    npm run dev

ติดตั้งเรียบร้อยลอง start application ดู

d:\Blogger\nuxt\poolsawat>npm run dev

> [email protected] dev d:\Blogger\nuxt\poolsawat
> nuxt

INFO Building project

√ success Builder initialized
√ success Nuxt files generated

READY Listening on http://localhost:3000

เสร็จสิ้นเป็นที่เรียบร้อย ตอนหน้าจะพาไปแนะนำโครงสร้างภายใน Nuxt.js กันว่าแต่ละส่วนใช้ทำงานอะไรบ้าง และถ้าจะสร้าง page ใหม่จะต้องทำอย่างไร

สร้าง Project JSF Primeface ด้วย Maven พร้อมกับสอนทำระบบ Template Layout

Java Server Faces (JSF) เป็นเว็บแอ็พพลิเคชันที่ใช้ Java ซึ่งมีจุดมุ่งหมายเพื่อลดความซับซ้อนในการพัฒนาอินเทอร์เฟซสำหรับผู้ใช้บนเว็บ JavaServer Faces เป็นเทคโนโลยีการแสดงผลแบบมาตรฐานซึ่งได้รับการประกาศไว้ในข้อกำหนดผ่านกระบวนการ Java Community Process

JSF ถูกนำไปพัฒนาต่อยอดความสามารถโดยบริษัทขนาดใหญ่ หลายบริษัท

  • Apache MyFaces – The Apache Foundation JSF implementation with Ajax components
  • Backbase Enterprise Ajax – JSF Edition – Ajax framework
  • BootsFaces Open source JSF Framework based on Bootstrap
  • IBM Notes – XPages
  • ICEfaces – open-source, Java JSF extension framework and rich components, Ajax without JavaScript
  • JBoss RichFaces (derived from and replaces Ajax4jsf) – Ajax-enabled JSF components for layout, file upload,
  • forms, inputs and many other features. It reached its end-of-life in June 2016.
  • OmniFaces – open-source JSF utility library
  • Open Faces – Ajax framework with JSF components
  • Oracle ADF Faces Rich Client – Oracle Application Development Framework
  • PrimeFaces – Ajax framework with JSF components
  • Sun Java BluePrints AJAX components
  • ZK – Ajax framework with JSF components





Primeface เป็น Framework ที่ไม่หยุดพัฒนาและมี UI Component มากมายและสวยงาม

ด้วยความสม่ำเสมอในการพัฒนาและมีการปรับปรุง framework อยู่ตลอดเวลา ออกผลิตภัณฑ์ตัวใหม่ออกมา เช่น PrimeNG ,PrimeReact ,PrimeUI เป็นต้น ทำให้เลือก Primeface เป็นตัวเลือกในการพัฒนา Project Webapp นี้

มาเริ่มสร้าง Project กัน

  1. Build Maven Project (maven-archetype-webapp) ขึ้นมา
  2. เพิ่ม Dependencies
  3. สร้างไฟล์ webapp/WEB-INF/faces-config.xml
  4. เพิ่ม Configure Faces Servlet in webapp/WEB-INF/web.xml
  5. สร้างไฟล์ webapp/templates/layout.xhtml
  6. สร้างไฟล์ webapp/templates/themeMenu.xhtml เพื่อทำเป็น navigator Menu ลิ้งไปที่หน้าอื่น ๆ
  7. สร้าง page webapp/pages/page1.xhtml ,page2.xhtml ,page3.xhtml ,page4.xhtml

    1. จุดที่สำคัญคือเพิ่ม template=”../templates/layout.xhtml” เข้าไปเพิ่มที่ ui:composition ทำให้อ้างถึง template layout ได้
  8. การเปลี่ยน Primeface Theme ของ website

  9. ตัวอย่างหน้า web site

DynamicReport Engine สร้างครั้งเดียวออก Report ได้ทุกแบบ (Hybrid Report)

โปรเจคเกือบจะทุกระบบที่พัฒนาขึ้นมาจำเป็นต้องมองหาระบบออกรายงาน เพื่อดูรูปแบบมิติของข้อมูลในมุมมองต่าง ๆ และรูปแบบชนิดของไฟล์หลากหลายชนิด เช่น PDF , XLS ,HTML , DOCX ,PPTX , CSV , TEXT , XML เป็นต้น ถ้าความต้องการของลูกค้าอยากจะได้ทุกชนิดละคงต้องประเมินเวลาไปอย่างน้อย 1 เดือนสำหรับการที่ทำ Report ให้ออกมาตามประเภทไฟล์ข้างต้นนี้ทั้งหมด แต่ลูกค้าที่น่ารักใจร้อนอยากจะได้เร็ว ๆ ให้เวลาช้าที่สุด 1 สัปดาห์ได้ไหม บทความนี้ช่วยคุณแก้ปัญหานี้ได้แน่นอน

สำหรับบทความนี้ก็เหมาะสำหรับนักพัฒนาที่มีพื้นฐาน Java สักหน่อย เริ่มกันเลย

    1. สร้าง Maven Project ขึ้นมา (จะเป็น Java Web หรือ Java Application ก็แล้วแต่ลักษณะงานของคุณ)
    2. เพิ่ม .pom ตามนี้
      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
          <groupId>com.poolsawat</groupId>
          <artifactId>MyDynamicReport</artifactId>
          <version>1.0-SNAPSHOT</version>
          <packaging>jar</packaging>
          <properties>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
              <maven.compiler.source>1.7</maven.compiler.source>
              <maven.compiler.target>1.7</maven.compiler.target>
              
              <slf4j.version>1.7.7</slf4j.version>
              
              <jasperreports.version>6.3.1</jasperreports.version>
              <dynamicreport.version>5.0.0</dynamicreport.version>
              <groovy.version>1.8.6</groovy.version>
              <poi.version>3.10.1</poi.version>
              
          </properties>
          <dependencies>        
      
              <dependency>
                  <groupId>org.codehaus.groovy</groupId>
                  <artifactId>groovy-all</artifactId>
                  <version>${groovy.version}</version>
              </dependency>        
              <dependency>
                  <groupId>net.sourceforge.dynamicreports</groupId>
                  <artifactId>dynamicreports-core</artifactId>
                  <version>${dynamicreport.version}</version>
              </dependency>        
              <dependency>
                  <groupId>net.sf.jasperreports</groupId>
                  <artifactId>jasperreports</artifactId>
                  <version>${jasperreports.version}</version>
                  <exclusions>
                      <exclusion>
                          <groupId>commons-logging</groupId>
                          <artifactId>commons-logging</artifactId>
                      </exclusion>
                  </exclusions>
              </dependency>
              
              <!--Poi-->
              <dependency>
                  <groupId>org.apache.poi</groupId>
                  <artifactId>poi</artifactId>
                  <version>${poi.version}</version>
              </dependency>
              
              
              <!-- Logging -->
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-api</artifactId>
                  <version>${slf4j.version}</version>
                  <scope>compile</scope>
              </dependency>
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-log4j12</artifactId>
                  <version>${slf4j.version}</version>
              </dependency>
              <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
              <dependency>
                  <groupId>commons-logging</groupId>
                  <artifactId>commons-logging</artifactId>
                  <version>1.2</version>
              </dependency>
      
          </dependencies>
      </project>




  1. สร้าง GenarateReport.java
    package com.poolsawat.dynamic;
    
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.util.Date;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import static net.sf.dynamicreports.report.builder.DynamicReports.*;
    import net.sf.dynamicreports.report.builder.style.StyleBuilder;
    import net.sf.dynamicreports.report.constant.HorizontalTextAlignment;
    import net.sf.dynamicreports.report.datasource.DRDataSource;
    import net.sf.dynamicreports.report.exception.DRException;
    import net.sf.jasperreports.engine.JRDataSource;
    
    /**
     *
     * @author poola410
     */
    public class GenarateReport {
    
        public GenarateReport() {
            build();
        }
    
        private void build() {
            try {
                report()//create new report design
                        //.setDefaultFont(stl.font().setFontName("TH SarabunPSK").setFontSize(16))                    
                        .columns(
                                col.column("PostId", "id", type.integerType())
                                        .setStyle(getTableStyle())
                                        .setTitleStyle(getTableStyle()),
                                col.column("PostTitle", "title", type.stringType())
                                        .setStyle(getTableStyle())
                                        .setTitleStyle(getTableStyle()),
                                col.column("PostAuthor", "author", type.stringType())
                                        .setStyle(getTableStyle())
                                        .setTitleStyle(getTableStyle()),
                                col.column("PostPublicDate", "public_date", type.dateType())
                                        .setStyle(getTableStyle())
                                        .setTitleStyle(getTableStyle()))
                        .title(cmp.text("PoolsawatBlogs"))//shows report title
                        .pageFooter(cmp.pageXofY())//shows number of page at page footer
                        .setDataSource(createDataSource())//set datasource
                        //.show();//create and show report
                        .toPdf(new FileOutputStream(new File("./src/main/resources/output/poolsawat.pdf")))
                        .toXls(new FileOutputStream(new File("./src/main/resources/output/poolsawat.xls")))
                        .toCsv(new FileOutputStream(new File("./src/main/resources/output/poolsawat.csv")))
                        .toXlsx(new FileOutputStream(new File("./src/main/resources/output/poolsawat.xlsx")))
                        .toHtml(new FileOutputStream(new File("./src/main/resources/output/poolsawat.html")))
                        .toPptx(new FileOutputStream(new File("./src/main/resources/output/poolsawat.pptx")))
                        .toText(new FileOutputStream(new File("./src/main/resources/output/poolsawat.text")))
                        .toXml(new FileOutputStream(new File("./src/main/resources/output/poolsawat.xml")));
                        
            } catch (DRException e) {
                e.printStackTrace();
            } catch (FileNotFoundException ex) {
                Logger.getLogger(GenarateReport.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        
        private StyleBuilder getTableStyle(){
            return stl.style().setBorder(stl.border(stl.pen1Point()))
                    .setHorizontalTextAlignment(HorizontalTextAlignment.LEFT);
        }
    
        private JRDataSource createDataSource() {
            DRDataSource dataSource = new DRDataSource("id", "title","author","public_date");
            dataSource.add(1215,"Quartz Scheduler มันคืออะไร การใช้งานเบื้องต้น", "admin", new Date());
            dataSource.add(1150,"Gson Open Source Library สำหรับจัดการ JSON Formatter", "admin", new Date());
            dataSource.add(1132,"สร้าง Spring MVC 4 ร่วมกับ Apache Tiles 3", "admin", new Date());
            dataSource.add(1099,"[ES6] Promise คืออะไร", "admin", new Date());
            dataSource.add(1072,"ประสบการณ์ทำเว็บไซต์ให้ปลอดภัยด้วย HTTPS ไม่ยากอย่างที่คิด", "admin", new Date());
            dataSource.add(1025,"เล่าประสบการณ์แข่งขัน Hackathon ครั้งแรกให้ชีวิต", "admin", new Date());
            dataSource.add(1002,"สรุปสิ่งที่ได้รับจากงาน Cloud Functions for Firebase and Next Generation of Web", "admin", new Date());
            return dataSource;
        }
    
        public static void main(String[] args) {
            new GenarateReport();
        }
    }
    
  2. สั่ง RUN File GenarateReport.java
  3. ดู output ตาม Path

 DynamicReport สามารถทำ Report ที่แสดงในรูปแบบของกราฟได้ด้วย ศึกษาเพิ่มเติมจากลิ้งนี้

Github source ZIP , Github Source Repository

Quartz Scheduler มันคืออะไร การใช้งานเบื้องต้น

Quartz Scheduler เป็น Open Source Project นิยมนำมาใช้งานกับโปรเจคระดับองค์กร ทำหน้าที่เป็นเครื่องมือจัดการเรื่อง Job Scheduler คล้ายกับ Cron ในระบบปฏิบัติการ linux ช่วยจัดการเรื่อง Job ที่มีการทำงานตั้งแต่ 1 Process ขึ้นไปใช้กับงาน Process ที่ต้องใช้เวลาในการ Processing ยาวนานกินเวลาหลายชั่วโมง ภายใน quartz-scheduler จะมี Class CronTrigger ซึ่งช่วยทำหน้าที่จัดการเวลาการทำงาน เช่น “ตอน 8.00 น.ทุกวันจันทร์ถึงวันศุกร์” หรือ “เวลา 1.30 น. ทุกวันศุกร์สุดท้ายของเดือน” ช่วยเพิ่มประสิทธิภาพการจัด scheduler ได้ออย่างดี

เริ่มต้นการใช้งาน

เพิ่ม dependency ในไฟล์ pom.xml

<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>1.8.6</version>
</dependency>

สร้าง Class Job Execute

public class DumbJob implements Job{

  public void execute(JobExecutionContext arg0) throws JobExecutionException {
    System.out.println("Poolsawat.com Run Scheduler Job");		
  }

}

ตัวอย่าง ตั้งเวลาการทำงานทุกนาที โดยเริ่มทำงานตอนทำงานในนาทีถัดไปหลังจากสั่งเริ่มการทำงาน

try {	
  SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
  
  Scheduler sched = schedFact.getScheduler();

  sched.start();

  JobDetail jobDetail = new JobDetail("myJob", null, DumbJob.class);

  Trigger trigger = TriggerUtils.makeMinutelyTrigger();
  trigger.setStartTime(TriggerUtils.getEvenMinuteDate(new Date()));
  trigger.setName("myTrigger");

  sched.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
  e.printStackTrace();
}

ตัวอย่าง ตั้งเวลาการทำงานช่วงเวลา 00:00 น. โดยเริ่มทำงานทันที่

try {
  SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();

  Scheduler sched = schedFact.getScheduler();

  JobDetail jobDetail = new JobDetail("myJob",
      Scheduler.DEFAULT_GROUP, 
      DumbJob.class);
  
  Trigger trigger = TriggerUtils.makeDailyTrigger(0, 0);
  trigger.setStartTime(new Date());
  trigger.setName("myTrigger");

  sched.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
  e.printStackTrace();
}

สร้าง Class Job ที่รับ Parameter

public class DataMapJob implements Job{
  public void execute(JobExecutionContext context) throws JobExecutionException {
    JobDetail detail = context.getJobDetail();
    JobDataMap mapData = detail.getJobDataMap();		
    System.out.println("param1 ::=="+mapData.getString("param1"));
    System.out.println("param2 ::=="+mapData.getString("param2"));
  }
}

ตัวอย่าง การส่งค่า Parameter เข้า Process ภายใน Class Job

try {
  SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();

  Scheduler sched = schedFact.getScheduler();

  sched.start();

  JobDetail jobDetail = new JobDetail("myJob", null, DataMapJob.class);
  
  
  JobDataMap dataMap = new JobDataMap();
  dataMap.put("param1", "poolsawat.com");
  dataMap.put("param2", "quartz");
  jobDetail.setJobDataMap(dataMap);
  
  Trigger trigger = TriggerUtils.makeMinutelyTrigger();
  trigger.setStartTime(TriggerUtils.getEvenMinuteDate(new Date()));
  trigger.setName("myTrigger");

  sched.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
  e.printStackTrace();
}

ตัวอย่าง ตั้งค่าใน Job ทำงานแค่ครั้งเดียว

try {
  SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();

  Scheduler sched = schedFact.getScheduler();

  sched.start();

  JobDetail jobDetail = new JobDetail("myJob", null, DataMapJob.class);
  
  SimpleTrigger trigger = new SimpleTrigger("myTrigger",
      null,
      new Date(),
      null,
      0,
      0L);
  
  sched.scheduleJob(jobDetail, trigger);
} catch (Exception e) {
  e.printStackTrace();
}

เพิ่มเติม…

Gson Open Source Library สำหรับจัดการ JSON Formatter

“JSON” ไม่ใช่เรื่องใหม่อะไร เพราะปัจจุบันมีการใช้งานกันอย่างแพร่หลาย สำหรับเอาไว้ใช้จัดการข้อมูลรับส่งระหว่าง frontend และ backend เพราะด้วยความง่ายและดูเป็นมาตฐาน มีรูปแบบที่ตายตัว ที่ใคร ๆ ก็สามารถเข้าใจได้เพราะเป็นการเก็บข้อมูลแบบ Key และ value

Java Library ที่ใช้สำหรับจัดการ JSON Data ก็มีมากมายให้ได้เลือกใช้งาน เช่น Google-Gson Library ,Flexjson ,Json-io ,Genson ,JSONiJ ,Jackson ,JSON-lib เป็นต้น ผมเองเป็นคนนึงที่นิยมใช้งาน Library ที่เป็น Open Source และต้องมีกลุ่ม Community ที่ใหญ่ และ Gson เองก็ได้รับความนิยมมาก เพราะด้วยความง่ายในการใช้งาน

การเริ่มใช้งาน Gson

เพิ่ม dependency

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.6.2</version>
</dependency>




การใช้งานเบื้องต้นของ Gson จะมีด้วยกัน 2 รูปแบบ ด้วยกัน คือ Serialization ,Deserialization

Serialization การแปลงข้อมูลที่อยู่ในรูปแบบ Object ไปเป็น Json String Format

Gson gson = new Gson();
gson.toJson(1);            // ==> 1
gson.toJson("abcd");       // ==> "abcd"
gson.toJson(new Long(10)); // ==> 10
int[] values = { 1 };
gson.toJson(values);       // ==> [1]

Deserialization  การแปลงข้อมูลในรูปแบบ Json String Format ไปเป็น Object

int one = gson.fromJson("1", int.class);
Integer one = gson.fromJson("1", Integer.class);
Long one = gson.fromJson("1", Long.class);
Boolean false = gson.fromJson("false", Boolean.class);
String str = gson.fromJson("\"abc\"", String.class);
String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class);

ตัวอย่างการใช้งานง่าย ๆ

สร้าง POJO Class

public class EasyObject {
    private String valueString;
    private int valueInteger;
    private Date valueDate;
    private boolean valueBoolean;
    // getter , setter

    @[email protected] public String toString() { return "EasyObject [valueString=" + valueString + ", valueInteger=" + valueInteger + ", valueDate=" + valueDate + ", valueBoolean=" + valueBoolean + "]"; }
}

ตัวอย่าง Method Serialization

private void exampleSerialization(){
  EasyObject easy = new EasyObject();
  easy.setValueBoolean(true);
  easy.setValueDate(new Date());
  easy.setValueInteger(999);
  easy.setValueString("poolsawat.com");
  //Serialization 
  System.out.println("serialization :: "+new Gson().toJson(easy));  
}
/*
serialization :: {"valueString":"poolsawat.com","valueInteger":999,"valueDate":"Oct 4, 2017 9:35:34 PM","valueBoolean":true} 
*/

ตัวอย่าง Method Deserialization

private void exampleDeserialization(){
  String json = "{\"valueString\":\"poolsawat.com\",\"valueInteger\":999,\"valueDate\":\"Oct 4, 2017 9:30:08 PM\",\"valueBoolean\":true}";
  EasyObject easy = new Gson().fromJson(json, EasyObject.class);
  System.out.println("deserialization :: "+easy.toString());  
}
/* 
deserialization :: EasyObject [valueString=poolsawat.com, valueInteger=999, valueDate=Wed Oct 04 21:30:08 ICT 2017, valueBoolean=true] 
*/

เมื่อใช้งาน Gson ไปสักระยะก็จะพบปัญหา หรือ สิ่งที่อยากปรับแต่งกับข้อมูลผลลัพธ์ให้เป็นไปตามความต้องการ Gson เองก็มี  GsonBuilder Class สำหรับให้เอาไว้ Custom Serialization and Deserialization

ตัวอย่างการแสดงผลลัพธ์ทาง Console ให้อยู่ในรูปแบบที่สวยงานโดยการ กำหนด PrettyPrinting

private void examplePrettyPrinting(){
  EasyObject easy = new EasyObject();
  easy.setValueBoolean(true);
  easy.setValueDate(new Date());
  easy.setValueInteger(999);
  easy.setValueString("poolsawat.com");
  //Serialization 
  Gson gson = new GsonBuilder().setPrettyPrinting().create();		
  System.out.println("serialization :: "+gson.toJson(easy));
}

/*
serialization :: {
  "valueString": "poolsawat.com",
  "valueInteger": 999,
  "valueDate": "Oct 4, 2017 9:54:58 PM",
  "valueBoolean": true
}*/

ตัวอย่างต้องการ custom field ที่มี value เป็น null อยากที่จะให้แสดงผลลัพธ์ field ที่เป็น null นั้นออกมาด้วยจำเป็นต้องกำหนด ให้  .serializeNulls()

private void exampleSerializeNulls(){
  EasyObject easy = new EasyObject();
  easy.setValueBoolean(true);
  //easy.setValueDate(new Date());
  easy.setValueInteger(999);
  easy.setValueString("poolsawat.com");
  
  //Serialization exclude null
  Gson gson = new GsonBuilder().setPrettyPrinting()				
      .create();		
  System.out.println("serialization exlude null:: "+gson.toJson(easy));
  
  //Serialization exclude null
  gson = new GsonBuilder().setPrettyPrinting()				
      .serializeNulls()
      .create();		
  System.out.println("serialization want null :: "+gson.toJson(easy));
}
/*
serialization exlude null:: {
  "valueString": "poolsawat.com",
  "valueInteger": 999,
  "valueBoolean": true
}
serialization want null :: {
  "valueString": "poolsawat.com",
  "valueInteger": 999,
  "valueDate": null,
  "valueBoolean": true
}*/




หากต้องการที่จะกำหนดชื่อใหม่ให้กับ key name ก็สามารถทำได้ โดยไม่ด้วยกัน 2 วิธี คือการกำหนดผ่าน annotation @SerializedName และ anonymous inner type การ setFieldNamingStrategy

@SerializedName("rename_valueBoolean") // เพิ่มเข้า
private boolean valueBoolean;

private void exampleRenameFieldKeyWithAnnotation(){
  EasyObject easy = new EasyObject();
  easy.setValueBoolean(true);
  //easy.setValueDate(new Date());
  easy.setValueInteger(999);
  easy.setValueString("poolsawat.com");
  
  Gson gson = new GsonBuilder().setPrettyPrinting()					
      .create();		
  System.out.println("serialization :: "+gson.toJson(easy));
}
/*
serialization :: {
  "valueString": "poolsawat.com",
  "valueInteger": 999,
  "rename_valueBoolean": true
}
*/

private boolean valueBoolean; // ไม่ต้องมี annotation แล้ว

private void exampleRenameFieldKeyWithAnonymous(){
  EasyObject easy = new EasyObject();
  easy.setValueBoolean(true);
  //easy.setValueDate(new Date());
  easy.setValueInteger(999);
  easy.setValueString("poolsawat.com");
  
  Gson gson = new GsonBuilder().setPrettyPrinting()		
      .setFieldNamingStrategy(new FieldNamingStrategy() {
        
        public String translateName(Field f) {
          if (f.getName().equals("valueBoolean"))
            return "rename_valueBoolean";
          else
            return f.getName();
        }
      })
      .create();		
  System.out.println("serialization :: "+gson.toJson(easy));
}

/*
serialization :: {
  "valueString": "poolsawat.com",
  "valueInteger": 999,
  "rename_valueBoolean": true
}
*/

สำหรับการกำหนดรุปแบบ format ให้ field ที่เป็น type date ก็สามารถกำหนดให้ได้ด้วยการกำหนด setDateFormat ตาม format ที่เรากำหนดเองได้เลย

private void exampleDateFormat(){
  EasyObject easy = new EasyObject();
  easy.setValueBoolean(true);
  easy.setValueDate(new Date());
  easy.setValueInteger(999);
  easy.setValueString("poolsawat.com");
  
  Gson gson = new GsonBuilder().setPrettyPrinting()		
      .setDateFormat("dd/MM/yyyy HH:mm:ss")
      .create();
  System.out.println("serialization dd/MM/yyyy HH:mm:ss :: "+gson.toJson(easy));		
  
  gson = new GsonBuilder().setPrettyPrinting()		
      .setDateFormat("yyyy/MM/dd")
      .create();
  System.out.println("serialization yyyy/MM/dd :: "+gson.toJson(easy));		
}
/*
serialization dd/MM/yyyy HH:mm:ss :: {
  "valueString": "poolsawat.com",
  "valueInteger": 999,
  "valueDate": "04/10/2017 22:29:53",
  "rename_valueBoolean": true
}
serialization yyyy/MM/dd :: {
  "valueString": "poolsawat.com",
  "valueInteger": 999,
  "valueDate": "2017/10/04",
  "rename_valueBoolean": true
}
*/

ในบางครั้งเราก็ไม่ต้องการสร้าง POJO สำหรับการเก็บ Data ชุดใหม่ Gson ก็มี Class สำหรับสร้าง Json Object เองได้สำหรับคนที่อยากกำหนด key ที่หลากหลาย

private void exampleJsonObject(){
  JsonObject object = new JsonObject();
  object.addProperty("blog_title", "Gson Open Source Library สำหรับจัดการ JSON Formatter");
  object.addProperty("blog_content", "“JSON” ไม่ใช่เรื่องใหม่อะไร เพราะปัจจุบันมีการ...");
  object.addProperty("blog_date", "04/10/2017");
  object.addProperty("blog_author", "poolsawat apin");
  Gson gson = new GsonBuilder().setPrettyPrinting().create();		
  System.out.println("serialization JsonObject :: "+gson.toJson(object));		
}
/*
serialization JsonObject :: {
  "blog_title": "Gson Open Source Library สำหรับจัดการ JSON Formatter",
  "blog_content": "“JSON” ไม่ใช่เรื่องใหม่อะไร เพราะปัจจุบันมีการ...",
  "blog_date": "04/10/2017",
  "blog_author": "poolsawat apin"
}
*/

อยากสร้างในรูปแบบของ Json Array ก็สามารถทำได้

private void exampleJsonArray(){
  JsonArray array = new JsonArray();
  
  JsonObject object1 = new JsonObject();
  object1.addProperty("blog_title", "Gson Open Source Library สำหรับจัดการ JSON Formatter");
  object1.addProperty("blog_content", "“JSON” ไม่ใช่เรื่องใหม่อะไร เพราะปัจจุบันมีการ...");
  object1.addProperty("blog_date", "04/10/2017");
  object1.addProperty("blog_author", "poolsawat apin");
  array.add(object1);
  
  JsonObject object2 = new JsonObject();
  object2.addProperty("blog_title", "สร้าง Spring MVC 4 ร่วมกับ Apache Tiles 3");
  object2.addProperty("blog_content", "สำหรับงานพัฒนาเว็บไซต์ด้วยภาษา Java มี Frameworks ให้เลือกอย่างมากมาย ...");
  object2.addProperty("blog_date", "15/09/2017");
  object2.addProperty("blog_author", "poolsawat apin");
  array.add(object2);
  
  Gson gson = new GsonBuilder().setPrettyPrinting().create();		
  System.out.println("serialization JsonArray :: "+gson.toJson(array));					
}
/*
serialization JsonArray :: [
  {
    "blog_title": "Gson Open Source Library สำหรับจัดการ JSON Formatter",
    "blog_content": "“JSON” ไม่ใช่เรื่องใหม่อะไร เพราะปัจจุบันมีการ...",
    "blog_date": "04/10/2017",
    "blog_author": "poolsawat apin"
  },
  {
    "blog_title": "สร้าง Spring MVC 4 ร่วมกับ Apache Tiles 3",
    "blog_content": "สำหรับงานพัฒนาเว็บไซต์ด้วยภาษา Java มี Frameworks ให้เลือกอย่างมากมาย ...",
    "blog_date": "15/09/2017",
    "blog_author": "poolsawat apin"
  }
]
*/

เหล่านี้เป็นตัวอย่างการใช้งานเบื้องต้นส่วนหนึ่งเท่านั้น มี UserGuide ให้ได้เข้าไปดูวิธีการใช้งานเพิ่มเติมได้